View Javadoc
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 &amp; 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
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 }