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