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