1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2025 the original author or authors.
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ///////////////////////////////////////////////////////////////////////////////////////////////
19
20 package com.puppycrawl.tools.checkstyle.checks.annotation;
21
22 import java.util.Locale;
23
24 import com.puppycrawl.tools.checkstyle.StatelessCheck;
25 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
26 import com.puppycrawl.tools.checkstyle.api.DetailAST;
27 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
28
29 /**
30 * <div>
31 * Checks the style of elements in annotations.
32 * </div>
33 *
34 * <p>
35 * Annotations have three element styles starting with the least verbose.
36 * </p>
37 * <ul>
38 * <li>
39 * {@code ElementStyleOption.COMPACT_NO_ARRAY}
40 * </li>
41 * <li>
42 * {@code ElementStyleOption.COMPACT}
43 * </li>
44 * <li>
45 * {@code ElementStyleOption.EXPANDED}
46 * </li>
47 * </ul>
48 *
49 * <p>
50 * To not enforce an element style a {@code ElementStyleOption.IGNORE} type is provided.
51 * The desired style can be set through the {@code elementStyle} property.
52 * </p>
53 *
54 * <p>
55 * Using the {@code ElementStyleOption.EXPANDED} style is more verbose.
56 * The expanded version is sometimes referred to as "named parameters" in other languages.
57 * </p>
58 *
59 * <p>
60 * Using the {@code ElementStyleOption.COMPACT} style is less verbose.
61 * This style can only be used when there is an element called 'value' which is either
62 * the sole element or all other elements have default values.
63 * </p>
64 *
65 * <p>
66 * Using the {@code ElementStyleOption.COMPACT_NO_ARRAY} style is less verbose.
67 * It is similar to the {@code ElementStyleOption.COMPACT} style but single value arrays are
68 * flagged.
69 * With annotations a single value array does not need to be placed in an array initializer.
70 * </p>
71 *
72 * <p>
73 * The ending parenthesis are optional when using annotations with no elements.
74 * To always require ending parenthesis use the {@code ClosingParensOption.ALWAYS} type.
75 * To never have ending parenthesis use the {@code ClosingParensOption.NEVER} type.
76 * To not enforce a closing parenthesis preference a {@code ClosingParensOption.IGNORE} type is
77 * provided.
78 * Set this through the {@code closingParens} property.
79 * </p>
80 *
81 * <p>
82 * Annotations also allow you to specify arrays of elements in a standard format.
83 * As with normal arrays, a trailing comma is optional.
84 * To always require a trailing comma use the {@code TrailingArrayCommaOption.ALWAYS} type.
85 * To never have a trailing comma use the {@code TrailingArrayCommaOption.NEVER} type.
86 * To not enforce a trailing array comma preference a {@code TrailingArrayCommaOption.IGNORE} type
87 * is provided. Set this through the {@code trailingArrayComma} property.
88 * </p>
89 *
90 * <p>
91 * By default, the {@code ElementStyleOption} is set to {@code COMPACT_NO_ARRAY},
92 * the {@code TrailingArrayCommaOption} is set to {@code NEVER},
93 * and the {@code ClosingParensOption} is set to {@code NEVER}.
94 * </p>
95 *
96 * <p>
97 * According to the JLS, it is legal to include a trailing comma
98 * in arrays used in annotations but Sun's Java 5 & 6 compilers will not
99 * compile with this syntax. This may in be a bug in Sun's compilers
100 * since eclipse 3.4's built-in compiler does allow this syntax as
101 * defined in the JLS. Note: this was tested with compilers included with
102 * JDK versions 1.5.0.17 and 1.6.0.11 and the compiler included with eclipse 3.4.1.
103 * </p>
104 *
105 * <p>
106 * See <a href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-9.html#jls-9.7">
107 * Java Language specification, §9.7</a>.
108 * </p>
109 *
110 * @since 5.0
111 */
112 @StatelessCheck
113 public final class AnnotationUseStyleCheck extends AbstractCheck {
114
115 /**
116 * Defines the styles for defining elements in an annotation.
117 */
118 public enum ElementStyleOption {
119
120 /**
121 * Expanded example
122 *
123 * <pre>@SuppressWarnings(value={"unchecked","unused",})</pre>.
124 */
125 EXPANDED,
126
127 /**
128 * Compact example
129 *
130 * <pre>@SuppressWarnings({"unchecked","unused",})</pre>
131 * <br>or<br>
132 * <pre>@SuppressWarnings("unchecked")</pre>.
133 */
134 COMPACT,
135
136 /**
137 * Compact example
138 *
139 * <pre>@SuppressWarnings("unchecked")</pre>.
140 */
141 COMPACT_NO_ARRAY,
142
143 /**
144 * Mixed styles.
145 */
146 IGNORE,
147
148 }
149
150 /**
151 * Defines the two styles for defining
152 * elements in an annotation.
153 *
154 */
155 public enum TrailingArrayCommaOption {
156
157 /**
158 * With comma example
159 *
160 * <pre>@SuppressWarnings(value={"unchecked","unused",})</pre>.
161 */
162 ALWAYS,
163
164 /**
165 * Without comma example
166 *
167 * <pre>@SuppressWarnings(value={"unchecked","unused"})</pre>.
168 */
169 NEVER,
170
171 /**
172 * Mixed styles.
173 */
174 IGNORE,
175
176 }
177
178 /**
179 * Defines the two styles for defining
180 * elements in an annotation.
181 *
182 */
183 public enum ClosingParensOption {
184
185 /**
186 * With parens example
187 *
188 * <pre>@Deprecated()</pre>.
189 */
190 ALWAYS,
191
192 /**
193 * Without parens example
194 *
195 * <pre>@Deprecated</pre>.
196 */
197 NEVER,
198
199 /**
200 * Mixed styles.
201 */
202 IGNORE,
203
204 }
205
206 /**
207 * A key is pointing to the warning message text in "messages.properties"
208 * file.
209 */
210 public static final String MSG_KEY_ANNOTATION_INCORRECT_STYLE =
211 "annotation.incorrect.style";
212
213 /**
214 * A key is pointing to the warning message text in "messages.properties"
215 * file.
216 */
217 public static final String MSG_KEY_ANNOTATION_PARENS_MISSING =
218 "annotation.parens.missing";
219
220 /**
221 * A key is pointing to the warning message text in "messages.properties"
222 * file.
223 */
224 public static final String MSG_KEY_ANNOTATION_PARENS_PRESENT =
225 "annotation.parens.present";
226
227 /**
228 * A key is pointing to the warning message text in "messages.properties"
229 * file.
230 */
231 public static final String MSG_KEY_ANNOTATION_TRAILING_COMMA_MISSING =
232 "annotation.trailing.comma.missing";
233
234 /**
235 * A key is pointing to the warning message text in "messages.properties"
236 * file.
237 */
238 public static final String MSG_KEY_ANNOTATION_TRAILING_COMMA_PRESENT =
239 "annotation.trailing.comma.present";
240
241 /**
242 * The element name used to receive special linguistic support
243 * for annotation use.
244 */
245 private static final String ANNOTATION_ELEMENT_SINGLE_NAME =
246 "value";
247
248 /**
249 * Define the annotation element styles.
250 */
251 private ElementStyleOption elementStyle = ElementStyleOption.COMPACT_NO_ARRAY;
252
253 // defaulting to NEVER because of the strange compiler behavior
254 /**
255 * Define the policy for trailing comma in arrays.
256 */
257 private TrailingArrayCommaOption trailingArrayComma = TrailingArrayCommaOption.NEVER;
258
259 /**
260 * Define the policy for ending parenthesis.
261 */
262 private ClosingParensOption closingParens = ClosingParensOption.NEVER;
263
264 /**
265 * Setter to define the annotation element styles.
266 *
267 * @param style string representation
268 * @since 5.0
269 */
270 public void setElementStyle(final String style) {
271 elementStyle = getOption(ElementStyleOption.class, style);
272 }
273
274 /**
275 * Setter to define the policy for trailing comma in arrays.
276 *
277 * @param comma string representation
278 * @since 5.0
279 */
280 public void setTrailingArrayComma(final String comma) {
281 trailingArrayComma = getOption(TrailingArrayCommaOption.class, comma);
282 }
283
284 /**
285 * Setter to define the policy for ending parenthesis.
286 *
287 * @param parens string representation
288 * @since 5.0
289 */
290 public void setClosingParens(final String parens) {
291 closingParens = getOption(ClosingParensOption.class, parens);
292 }
293
294 /**
295 * Retrieves an {@link Enum Enum} type from a @{link String String}.
296 *
297 * @param <T> the enum type
298 * @param enumClass the enum class
299 * @param value the string representing the enum
300 * @return the enum type
301 * @throws IllegalArgumentException when unable to parse value
302 */
303 private static <T extends Enum<T>> T getOption(final Class<T> enumClass,
304 final String value) {
305 try {
306 return Enum.valueOf(enumClass, value.trim().toUpperCase(Locale.ENGLISH));
307 }
308 catch (final IllegalArgumentException iae) {
309 throw new IllegalArgumentException("unable to parse " + value, iae);
310 }
311 }
312
313 @Override
314 public int[] getDefaultTokens() {
315 return getRequiredTokens();
316 }
317
318 @Override
319 public int[] getRequiredTokens() {
320 return new int[] {
321 TokenTypes.ANNOTATION,
322 };
323 }
324
325 @Override
326 public int[] getAcceptableTokens() {
327 return getRequiredTokens();
328 }
329
330 @Override
331 public void visitToken(final DetailAST ast) {
332 checkStyleType(ast);
333 checkCheckClosingParensOption(ast);
334 checkTrailingComma(ast);
335 }
336
337 /**
338 * Checks to see if the
339 * {@link ElementStyleOption AnnotationElementStyleOption}
340 * is correct.
341 *
342 * @param annotation the annotation token
343 * @noinspection EnhancedSwitchMigration
344 * @noinspectionreason Until #17674
345 */
346 private void checkStyleType(final DetailAST annotation) {
347 switch (elementStyle) {
348 case COMPACT_NO_ARRAY:
349 checkCompactNoArrayStyle(annotation);
350 break;
351 case COMPACT:
352 checkCompactStyle(annotation);
353 break;
354 case EXPANDED:
355 checkExpandedStyle(annotation);
356 break;
357 case IGNORE:
358 default:
359 break;
360 }
361 }
362
363 /**
364 * Checks for expanded style type violations.
365 *
366 * @param annotation the annotation token
367 */
368 private void checkExpandedStyle(final DetailAST annotation) {
369 final int valuePairCount =
370 annotation.getChildCount(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
371
372 if (valuePairCount == 0 && hasArguments(annotation)) {
373 log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE, ElementStyleOption.EXPANDED);
374 }
375 }
376
377 /**
378 * Checks that annotation has arguments.
379 *
380 * @param annotation to check
381 * @return true if annotation has arguments, false otherwise
382 */
383 private static boolean hasArguments(DetailAST annotation) {
384 final DetailAST firstToken = annotation.findFirstToken(TokenTypes.LPAREN);
385 return firstToken != null && firstToken.getNextSibling().getType() != TokenTypes.RPAREN;
386 }
387
388 /**
389 * Checks for compact style type violations.
390 *
391 * @param annotation the annotation token
392 */
393 private void checkCompactStyle(final DetailAST annotation) {
394 final int valuePairCount =
395 annotation.getChildCount(
396 TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
397
398 final DetailAST valuePair =
399 annotation.findFirstToken(
400 TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
401
402 if (valuePairCount == 1
403 && ANNOTATION_ELEMENT_SINGLE_NAME.equals(
404 valuePair.getFirstChild().getText())) {
405 log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE,
406 ElementStyleOption.COMPACT);
407 }
408 }
409
410 /**
411 * Checks for compact no array style type violations.
412 *
413 * @param annotation the annotation token
414 */
415 private void checkCompactNoArrayStyle(final DetailAST annotation) {
416 final DetailAST arrayInit =
417 annotation.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
418
419 // in compact style with one value
420 if (arrayInit != null
421 && arrayInit.getChildCount(TokenTypes.EXPR) == 1) {
422 log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE,
423 ElementStyleOption.COMPACT_NO_ARRAY);
424 }
425 // in expanded style with pairs
426 else {
427 DetailAST ast = annotation.getFirstChild();
428 while (ast != null) {
429 final DetailAST nestedArrayInit =
430 ast.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
431 if (nestedArrayInit != null
432 && nestedArrayInit.getChildCount(TokenTypes.EXPR) == 1) {
433 log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE,
434 ElementStyleOption.COMPACT_NO_ARRAY);
435 }
436 ast = ast.getNextSibling();
437 }
438 }
439 }
440
441 /**
442 * Checks to see if the trailing comma is present if required or
443 * prohibited.
444 *
445 * @param annotation the annotation token
446 */
447 private void checkTrailingComma(final DetailAST annotation) {
448 if (trailingArrayComma != TrailingArrayCommaOption.IGNORE) {
449 DetailAST child = annotation.getFirstChild();
450
451 while (child != null) {
452 DetailAST arrayInit = null;
453
454 if (child.getType() == TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR) {
455 arrayInit = child.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
456 }
457 else if (child.getType() == TokenTypes.ANNOTATION_ARRAY_INIT) {
458 arrayInit = child;
459 }
460
461 if (arrayInit != null) {
462 logCommaViolation(arrayInit);
463 }
464 child = child.getNextSibling();
465 }
466 }
467 }
468
469 /**
470 * Logs a trailing array comma violation if one exists.
471 *
472 * @param ast the array init
473 * {@link TokenTypes#ANNOTATION_ARRAY_INIT ANNOTATION_ARRAY_INIT}.
474 */
475 private void logCommaViolation(final DetailAST ast) {
476 final DetailAST rCurly = ast.findFirstToken(TokenTypes.RCURLY);
477
478 // comma can be null if array is empty
479 final DetailAST comma = rCurly.getPreviousSibling();
480
481 if (trailingArrayComma == TrailingArrayCommaOption.NEVER) {
482 if (comma != null && comma.getType() == TokenTypes.COMMA) {
483 log(comma, MSG_KEY_ANNOTATION_TRAILING_COMMA_PRESENT);
484 }
485 }
486 else if (comma == null || comma.getType() != TokenTypes.COMMA) {
487 log(rCurly, MSG_KEY_ANNOTATION_TRAILING_COMMA_MISSING);
488 }
489 }
490
491 /**
492 * Checks to see if the closing parenthesis are present if required or
493 * prohibited.
494 *
495 * @param ast the annotation token
496 */
497 private void checkCheckClosingParensOption(final DetailAST ast) {
498 if (closingParens != ClosingParensOption.IGNORE) {
499 final DetailAST paren = ast.getLastChild();
500
501 if (closingParens == ClosingParensOption.NEVER) {
502 if (paren.getPreviousSibling().getType() == TokenTypes.LPAREN) {
503 log(ast, MSG_KEY_ANNOTATION_PARENS_PRESENT);
504 }
505 }
506 else if (paren.getType() != TokenTypes.RPAREN) {
507 log(ast, MSG_KEY_ANNOTATION_PARENS_MISSING);
508 }
509 }
510 }
511
512 }