View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 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 {@literal &} 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, &#167;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: {@code @SuppressWarnings(value={"unchecked","unused",})}.
122          */
123         EXPANDED,
124 
125         /**
126          * Compact example
127          * <br>
128          * {@code @SuppressWarnings({"unchecked","unused",})}
129          * <br>or<br>
130          * {@code @SuppressWarnings("unchecked")}.
131          */
132         COMPACT,
133 
134         /**
135          * Compact example: {@code @SuppressWarnings("unchecked")}.
136          */
137         COMPACT_NO_ARRAY,
138 
139         /**
140          * Mixed styles.
141          */
142         IGNORE,
143 
144     }
145 
146     /**
147      * Defines the two styles for defining
148      * elements in an annotation.
149      *
150      */
151     public enum TrailingArrayCommaOption {
152 
153         /**
154          * With comma example: {@code @SuppressWarnings(value={"unchecked","unused",})}.
155          */
156         ALWAYS,
157 
158         /**
159          * Without comma example: {@code @SuppressWarnings(value={"unchecked","unused"})}.
160          */
161         NEVER,
162 
163         /**
164          * Mixed styles.
165          */
166         IGNORE,
167 
168     }
169 
170     /**
171      * Defines the two styles for defining
172      * elements in an annotation.
173      *
174      */
175     public enum ClosingParensOption {
176 
177         /**
178          * With parens example :
179          * {@code @Deprecated()}.
180          */
181         ALWAYS,
182 
183         /**
184          * Without parens example: {@code @Deprecated}.
185          */
186         NEVER,
187 
188         /**
189          * Mixed styles.
190          */
191         IGNORE,
192 
193     }
194 
195     /**
196      * A key is pointing to the warning message text in "messages.properties"
197      * file.
198      */
199     public static final String MSG_KEY_ANNOTATION_INCORRECT_STYLE =
200         "annotation.incorrect.style";
201 
202     /**
203      * A key is pointing to the warning message text in "messages.properties"
204      * file.
205      */
206     public static final String MSG_KEY_ANNOTATION_PARENS_MISSING =
207         "annotation.parens.missing";
208 
209     /**
210      * A key is pointing to the warning message text in "messages.properties"
211      * file.
212      */
213     public static final String MSG_KEY_ANNOTATION_PARENS_PRESENT =
214         "annotation.parens.present";
215 
216     /**
217      * A key is pointing to the warning message text in "messages.properties"
218      * file.
219      */
220     public static final String MSG_KEY_ANNOTATION_TRAILING_COMMA_MISSING =
221         "annotation.trailing.comma.missing";
222 
223     /**
224      * A key is pointing to the warning message text in "messages.properties"
225      * file.
226      */
227     public static final String MSG_KEY_ANNOTATION_TRAILING_COMMA_PRESENT =
228         "annotation.trailing.comma.present";
229 
230     /**
231      * The element name used to receive special linguistic support
232      * for annotation use.
233      */
234     private static final String ANNOTATION_ELEMENT_SINGLE_NAME =
235             "value";
236 
237     /**
238      * Define the annotation element styles.
239      */
240     private ElementStyleOption elementStyle = ElementStyleOption.COMPACT_NO_ARRAY;
241 
242     // defaulting to NEVER because of the strange compiler behavior
243     /**
244      * Define the policy for trailing comma in arrays.
245      */
246     private TrailingArrayCommaOption trailingArrayComma = TrailingArrayCommaOption.NEVER;
247 
248     /**
249      * Define the policy for ending parenthesis.
250      */
251     private ClosingParensOption closingParens = ClosingParensOption.NEVER;
252 
253     /**
254      * Creates a new {@code AnnotationUseStyleCheck} instance.
255      */
256     public AnnotationUseStyleCheck() {
257         // no code by default
258     }
259 
260     /**
261      * Setter to define the annotation element styles.
262      *
263      * @param style string representation
264      * @since 5.0
265      */
266     public void setElementStyle(final String style) {
267         elementStyle = getOption(ElementStyleOption.class, style);
268     }
269 
270     /**
271      * Setter to define the policy for trailing comma in arrays.
272      *
273      * @param comma string representation
274      * @since 5.0
275      */
276     public void setTrailingArrayComma(final String comma) {
277         trailingArrayComma = getOption(TrailingArrayCommaOption.class, comma);
278     }
279 
280     /**
281      * Setter to define the policy for ending parenthesis.
282      *
283      * @param parens string representation
284      * @since 5.0
285      */
286     public void setClosingParens(final String parens) {
287         closingParens = getOption(ClosingParensOption.class, parens);
288     }
289 
290     /**
291      * Retrieves an {@code Enum} type from a {@code String}.
292      *
293      * @param <T> the enum type
294      * @param enumClass the enum class
295      * @param value the string representing the enum
296      * @return the enum type
297      * @throws IllegalArgumentException when unable to parse value
298      */
299     private static <T extends Enum<T>> T getOption(final Class<T> enumClass,
300         final String value) {
301         try {
302             return Enum.valueOf(enumClass, value.trim().toUpperCase(Locale.ENGLISH));
303         }
304         catch (final IllegalArgumentException iae) {
305             throw new IllegalArgumentException("unable to parse " + value, iae);
306         }
307     }
308 
309     @Override
310     public int[] getDefaultTokens() {
311         return getRequiredTokens();
312     }
313 
314     @Override
315     public int[] getRequiredTokens() {
316         return new int[] {
317             TokenTypes.ANNOTATION,
318         };
319     }
320 
321     @Override
322     public int[] getAcceptableTokens() {
323         return getRequiredTokens();
324     }
325 
326     @Override
327     public void visitToken(final DetailAST ast) {
328         checkStyleType(ast);
329         checkCheckClosingParensOption(ast);
330         checkTrailingComma(ast);
331     }
332 
333     /**
334      * Checks to see if the
335      * {@link ElementStyleOption AnnotationElementStyleOption}
336      * is correct.
337      *
338      * @param annotation the annotation token
339      */
340     private void checkStyleType(final DetailAST annotation) {
341         if (elementStyle == ElementStyleOption.COMPACT_NO_ARRAY) {
342             checkCompactNoArrayStyle(annotation);
343         }
344         else if (elementStyle == ElementStyleOption.COMPACT) {
345             checkCompactStyle(annotation);
346         }
347         else if (elementStyle == ElementStyleOption.EXPANDED) {
348             checkExpandedStyle(annotation);
349         }
350     }
351 
352     /**
353      * Checks for expanded style type violations.
354      *
355      * @param annotation the annotation token
356      */
357     private void checkExpandedStyle(final DetailAST annotation) {
358         final int valuePairCount =
359             annotation.getChildCount(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
360 
361         if (valuePairCount == 0 && hasArguments(annotation)) {
362             log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE, ElementStyleOption.EXPANDED);
363         }
364     }
365 
366     /**
367      * Checks that annotation has arguments.
368      *
369      * @param annotation to check
370      * @return true if annotation has arguments, false otherwise
371      */
372     private static boolean hasArguments(DetailAST annotation) {
373         final DetailAST firstToken = annotation.findFirstToken(TokenTypes.LPAREN);
374         return firstToken != null && firstToken.getNextSibling().getType() != TokenTypes.RPAREN;
375     }
376 
377     /**
378      * Checks for compact style type violations.
379      *
380      * @param annotation the annotation token
381      */
382     private void checkCompactStyle(final DetailAST annotation) {
383         final int valuePairCount =
384             annotation.getChildCount(
385                 TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
386 
387         final DetailAST valuePair =
388             annotation.findFirstToken(
389                 TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
390 
391         if (valuePairCount == 1
392             && ANNOTATION_ELEMENT_SINGLE_NAME.equals(
393                 valuePair.getFirstChild().getText())) {
394             log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE,
395                 ElementStyleOption.COMPACT);
396         }
397     }
398 
399     /**
400      * Checks for compact no array style type violations.
401      *
402      * @param annotation the annotation token
403      */
404     private void checkCompactNoArrayStyle(final DetailAST annotation) {
405         final DetailAST arrayInit =
406             annotation.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
407 
408         // in compact style with one value
409         if (arrayInit != null
410             && arrayInit.getChildCount(TokenTypes.EXPR) == 1) {
411             log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE,
412                 ElementStyleOption.COMPACT_NO_ARRAY);
413         }
414         // in expanded style with pairs
415         else {
416             DetailAST ast = annotation.getFirstChild();
417             while (ast != null) {
418                 final DetailAST nestedArrayInit =
419                     ast.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
420                 if (nestedArrayInit != null
421                     && nestedArrayInit.getChildCount(TokenTypes.EXPR) == 1) {
422                     log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE,
423                         ElementStyleOption.COMPACT_NO_ARRAY);
424                 }
425                 ast = ast.getNextSibling();
426             }
427         }
428     }
429 
430     /**
431      * Checks to see if the trailing comma is present if required or
432      * prohibited.
433      *
434      * @param annotation the annotation token
435      */
436     private void checkTrailingComma(final DetailAST annotation) {
437         if (trailingArrayComma != TrailingArrayCommaOption.IGNORE) {
438             DetailAST child = annotation.getFirstChild();
439 
440             while (child != null) {
441                 DetailAST arrayInit = null;
442 
443                 if (child.getType() == TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR) {
444                     arrayInit = child.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
445                 }
446                 else if (child.getType() == TokenTypes.ANNOTATION_ARRAY_INIT) {
447                     arrayInit = child;
448                 }
449 
450                 if (arrayInit != null) {
451                     logCommaViolation(arrayInit);
452                 }
453                 child = child.getNextSibling();
454             }
455         }
456     }
457 
458     /**
459      * Logs a trailing array comma violation if one exists.
460      *
461      * @param ast the array init
462      *     {@link TokenTypes#ANNOTATION_ARRAY_INIT ANNOTATION_ARRAY_INIT}.
463      */
464     private void logCommaViolation(final DetailAST ast) {
465         final DetailAST rCurly = ast.findFirstToken(TokenTypes.RCURLY);
466 
467         // comma can be null if array is empty
468         final DetailAST comma = rCurly.getPreviousSibling();
469 
470         if (trailingArrayComma == TrailingArrayCommaOption.NEVER) {
471             if (comma != null && comma.getType() == TokenTypes.COMMA) {
472                 log(comma, MSG_KEY_ANNOTATION_TRAILING_COMMA_PRESENT);
473             }
474         }
475         else if (comma == null || comma.getType() != TokenTypes.COMMA) {
476             log(rCurly, MSG_KEY_ANNOTATION_TRAILING_COMMA_MISSING);
477         }
478     }
479 
480     /**
481      * Checks to see if the closing parenthesis are present if required or
482      * prohibited.
483      *
484      * @param ast the annotation token
485      */
486     private void checkCheckClosingParensOption(final DetailAST ast) {
487         if (closingParens != ClosingParensOption.IGNORE) {
488             final DetailAST paren = ast.getLastChild();
489 
490             if (closingParens == ClosingParensOption.NEVER) {
491                 if (paren.getPreviousSibling().getType() == TokenTypes.LPAREN) {
492                     log(ast, MSG_KEY_ANNOTATION_PARENS_PRESENT);
493                 }
494             }
495             else if (paren.getType() != TokenTypes.RPAREN) {
496                 log(ast, MSG_KEY_ANNOTATION_PARENS_MISSING);
497             }
498         }
499     }
500 
501 }