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