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