001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2018 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.annotation;
021
022import java.util.Locale;
023
024import org.apache.commons.beanutils.ConversionException;
025
026import com.puppycrawl.tools.checkstyle.StatelessCheck;
027import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
028import com.puppycrawl.tools.checkstyle.api.DetailAST;
029import com.puppycrawl.tools.checkstyle.api.TokenTypes;
030
031/**
032 * This check controls the style with the usage of annotations.
033 *
034 * <p>Annotations have three element styles starting with the least verbose.
035 * <ul>
036 * <li>{@link ElementStyle#COMPACT_NO_ARRAY COMPACT_NO_ARRAY}</li>
037 * <li>{@link ElementStyle#COMPACT COMPACT}</li>
038 * <li>{@link ElementStyle#EXPANDED EXPANDED}</li>
039 * </ul>
040 * To not enforce an element style
041 * a {@link ElementStyle#IGNORE IGNORE} type is provided.  The desired style
042 * can be set through the {@code elementStyle} property.
043 *
044 * <p>Using the EXPANDED style is more verbose. The expanded version
045 * is sometimes referred to as "named parameters" in other languages.
046 *
047 * <p>Using the COMPACT style is less verbose. This style can only
048 * be used when there is an element called 'value' which is either
049 * the sole element or all other elements have default values.
050 *
051 * <p>Using the COMPACT_NO_ARRAY style is less verbose. It is similar
052 * to the COMPACT style but single value arrays are flagged. With
053 * annotations a single value array does not need to be placed in an
054 * array initializer. This style can only be used when there is an
055 * element called 'value' which is either the sole element or all other
056 * elements have default values.
057 *
058 * <p>The ending parenthesis are optional when using annotations with no elements.
059 * To always require ending parenthesis use the
060 * {@link ClosingParens#ALWAYS ALWAYS} type.  To never have ending parenthesis
061 * use the {@link ClosingParens#NEVER NEVER} type. To not enforce a
062 * closing parenthesis preference a {@link ClosingParens#IGNORE IGNORE} type is
063 * provided. Set this through the {@code closingParens} property.
064 *
065 * <p>Annotations also allow you to specify arrays of elements in a standard
066 * format.  As with normal arrays, a trailing comma is optional. To always
067 * require a trailing comma use the {@link TrailingArrayComma#ALWAYS ALWAYS}
068 * type. To never have a trailing comma use the
069 * {@link TrailingArrayComma#NEVER NEVER} type. To not enforce a trailing
070 * array comma preference a {@link TrailingArrayComma#IGNORE IGNORE} type
071 * is provided.  Set this through the {@code trailingArrayComma} property.
072 *
073 * <p>By default the ElementStyle is set to COMPACT_NO_ARRAY, the
074 * TrailingArrayComma is set to NEVER, and the ClosingParens is set to NEVER.
075 *
076 * <p>According to the JLS, it is legal to include a trailing comma
077 * in arrays used in annotations but Sun's Java 5 &amp; 6 compilers will not
078 * compile with this syntax. This may in be a bug in Sun's compilers
079 * since eclipse 3.4's built-in compiler does allow this syntax as
080 * defined in the JLS. Note: this was tested with compilers included with
081 * JDK versions 1.5.0.17 and 1.6.0.11 and the compiler included with eclipse
082 * 3.4.1.
083 *
084 * <p>See <a
085 * href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-9.html#jls-9.7">
086 * Java Language specification, &sect;9.7</a>.
087 *
088 * <p>An example shown below is set to enforce an EXPANDED style, with a
089 * trailing array comma set to NEVER and always including the closing
090 * parenthesis.
091 *
092 * <pre>
093 * &lt;module name=&quot;AnnotationUseStyle&quot;&gt;
094 *    &lt;property name=&quot;ElementStyle&quot;
095 *        value=&quot;EXPANDED&quot;/&gt;
096 *    &lt;property name=&quot;TrailingArrayComma&quot;
097 *        value=&quot;NEVER&quot;/&gt;
098 *    &lt;property name=&quot;ClosingParens&quot;
099 *        value=&quot;ALWAYS&quot;/&gt;
100 * &lt;/module&gt;
101 * </pre>
102 *
103 * @author Travis Schneeberger
104 */
105@StatelessCheck
106public 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    /**
145     * Defines the two styles for defining
146     * elements in an annotation.
147     *
148     * @author Travis Schneeberger
149     */
150    public enum TrailingArrayComma {
151
152        /**
153         * With comma example
154         *
155         * <pre>@SuppressWarnings(value={"unchecked","unused",})</pre>.
156         */
157        ALWAYS,
158
159        /**
160         * Without comma example
161         *
162         * <pre>@SuppressWarnings(value={"unchecked","unused"})</pre>.
163         */
164        NEVER,
165
166        /**
167         * Mixed styles.
168         */
169        IGNORE,
170
171    }
172
173    /**
174     * Defines the two styles for defining
175     * elements in an annotation.
176     *
177     * @author Travis Schneeberger
178     */
179    public enum ClosingParens {
180
181        /**
182         * With parens example
183         *
184         * <pre>@Deprecated()</pre>.
185         */
186        ALWAYS,
187
188        /**
189         * Without parens example
190         *
191         * <pre>@Deprecated</pre>.
192         */
193        NEVER,
194
195        /**
196         * Mixed styles.
197         */
198        IGNORE,
199
200    }
201
202    /**
203     * A key is pointing to the warning message text in "messages.properties"
204     * file.
205     */
206    public static final String MSG_KEY_ANNOTATION_INCORRECT_STYLE =
207        "annotation.incorrect.style";
208
209    /**
210     * A key is pointing to the warning message text in "messages.properties"
211     * file.
212     */
213    public static final String MSG_KEY_ANNOTATION_PARENS_MISSING =
214        "annotation.parens.missing";
215
216    /**
217     * A key is pointing to the warning message text in "messages.properties"
218     * file.
219     */
220    public static final String MSG_KEY_ANNOTATION_PARENS_PRESENT =
221        "annotation.parens.present";
222
223    /**
224     * A key is pointing to the warning message text in "messages.properties"
225     * file.
226     */
227    public static final String MSG_KEY_ANNOTATION_TRAILING_COMMA_MISSING =
228        "annotation.trailing.comma.missing";
229
230    /**
231     * A key is pointing to the warning message text in "messages.properties"
232     * file.
233     */
234    public static final String MSG_KEY_ANNOTATION_TRAILING_COMMA_PRESENT =
235        "annotation.trailing.comma.present";
236
237    /**
238     * The element name used to receive special linguistic support
239     * for annotation use.
240     */
241    private static final String ANNOTATION_ELEMENT_SINGLE_NAME =
242            "value";
243
244    /**
245     * ElementStyle option.
246     * @see #setElementStyle(String)
247     */
248    private ElementStyle elementStyle = ElementStyle.COMPACT_NO_ARRAY;
249
250    //defaulting to NEVER because of the strange compiler behavior
251    /**
252     * Trailing array comma option.
253     * @see #setTrailingArrayComma(String)
254     */
255    private TrailingArrayComma trailingArrayComma = TrailingArrayComma.NEVER;
256
257    /**
258     * Closing parens option.
259     * @see #setClosingParens(String)
260     */
261    private ClosingParens closingParens = ClosingParens.NEVER;
262
263    /**
264     * Sets the ElementStyle from a string.
265     *
266     * @param style string representation
267     * @throws ConversionException if cannot convert string.
268     */
269    public void setElementStyle(final String style) {
270        elementStyle = getOption(ElementStyle.class, style);
271    }
272
273    /**
274     * Sets the TrailingArrayComma from a string.
275     *
276     * @param comma string representation
277     * @throws ConversionException if cannot convert string.
278     */
279    public void setTrailingArrayComma(final String comma) {
280        trailingArrayComma = getOption(TrailingArrayComma.class, comma);
281    }
282
283    /**
284     * Sets the ClosingParens from a string.
285     *
286     * @param parens string representation
287     * @throws ConversionException if cannot convert string.
288     */
289    public void setClosingParens(final String parens) {
290        closingParens = getOption(ClosingParens.class, parens);
291    }
292
293    /**
294     * Retrieves an {@link Enum Enum} type from a @{link String String}.
295     * @param <T> the enum type
296     * @param enumClass the enum class
297     * @param value the string representing the enum
298     * @return the enum type
299     */
300    private static <T extends Enum<T>> T getOption(final Class<T> enumClass,
301        final String value) {
302        try {
303            return Enum.valueOf(enumClass, value.trim().toUpperCase(Locale.ENGLISH));
304        }
305        catch (final IllegalArgumentException iae) {
306            throw new IllegalArgumentException("unable to parse " + value, iae);
307        }
308    }
309
310    @Override
311    public int[] getDefaultTokens() {
312        return getRequiredTokens();
313    }
314
315    @Override
316    public int[] getRequiredTokens() {
317        return new int[] {
318            TokenTypes.ANNOTATION,
319        };
320    }
321
322    @Override
323    public int[] getAcceptableTokens() {
324        return getRequiredTokens();
325    }
326
327    @Override
328    public void visitToken(final DetailAST ast) {
329        checkStyleType(ast);
330        checkCheckClosingParens(ast);
331        checkTrailingComma(ast);
332    }
333
334    /**
335     * Checks to see if the
336     * {@link ElementStyle AnnotationElementStyle}
337     * is correct.
338     *
339     * @param annotation the annotation token
340     */
341    private void checkStyleType(final DetailAST annotation) {
342        switch (elementStyle) {
343            case COMPACT_NO_ARRAY:
344                checkCompactNoArrayStyle(annotation);
345                break;
346            case COMPACT:
347                checkCompactStyle(annotation);
348                break;
349            case EXPANDED:
350                checkExpandedStyle(annotation);
351                break;
352            case IGNORE:
353            default:
354                break;
355        }
356    }
357
358    /**
359     * Checks for expanded style type violations.
360     *
361     * @param annotation the annotation token
362     */
363    private void checkExpandedStyle(final DetailAST annotation) {
364        final int valuePairCount =
365            annotation.getChildCount(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
366
367        if (valuePairCount == 0
368            && annotation.branchContains(TokenTypes.EXPR)) {
369            log(annotation.getLineNo(), MSG_KEY_ANNOTATION_INCORRECT_STYLE,
370                ElementStyle.EXPANDED);
371        }
372    }
373
374    /**
375     * Checks for compact style type violations.
376     *
377     * @param annotation the annotation token
378     */
379    private void checkCompactStyle(final DetailAST annotation) {
380        final int valuePairCount =
381            annotation.getChildCount(
382                TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
383
384        final DetailAST valuePair =
385            annotation.findFirstToken(
386                TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
387
388        if (valuePairCount == 1
389            && ANNOTATION_ELEMENT_SINGLE_NAME.equals(
390                valuePair.getFirstChild().getText())) {
391            log(annotation.getLineNo(), MSG_KEY_ANNOTATION_INCORRECT_STYLE,
392                ElementStyle.COMPACT);
393        }
394    }
395
396    /**
397     * Checks for compact no array style type violations.
398     *
399     * @param annotation the annotation token
400     */
401    private void checkCompactNoArrayStyle(final DetailAST annotation) {
402        final DetailAST arrayInit =
403            annotation.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
404
405        final int valuePairCount =
406            annotation.getChildCount(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
407
408        //in compact style with one value
409        if (arrayInit != null
410            && arrayInit.getChildCount(TokenTypes.EXPR) == 1) {
411            log(annotation.getLineNo(), MSG_KEY_ANNOTATION_INCORRECT_STYLE,
412                ElementStyle.COMPACT_NO_ARRAY);
413        }
414        //in expanded style with one value and the correct element name
415        else if (valuePairCount == 1) {
416            final DetailAST valuePair =
417                    annotation.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
418            final DetailAST nestedArrayInit =
419                valuePair.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
420
421            if (nestedArrayInit != null
422                && ANNOTATION_ELEMENT_SINGLE_NAME.equals(
423                    valuePair.getFirstChild().getText())
424                    && nestedArrayInit.getChildCount(TokenTypes.EXPR) == 1) {
425                log(annotation.getLineNo(), MSG_KEY_ANNOTATION_INCORRECT_STYLE,
426                    ElementStyle.COMPACT_NO_ARRAY);
427            }
428        }
429    }
430
431    /**
432     * Checks to see if the trailing comma is present if required or
433     * prohibited.
434     *
435     * @param annotation the annotation token
436     */
437    private void checkTrailingComma(final DetailAST annotation) {
438        if (trailingArrayComma != TrailingArrayComma.IGNORE) {
439            DetailAST child = annotation.getFirstChild();
440
441            while (child != null) {
442                DetailAST arrayInit = null;
443
444                if (child.getType() == TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR) {
445                    arrayInit = child.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
446                }
447                else if (child.getType() == TokenTypes.ANNOTATION_ARRAY_INIT) {
448                    arrayInit = child;
449                }
450
451                if (arrayInit != null) {
452                    logCommaViolation(arrayInit);
453                }
454                child = child.getNextSibling();
455            }
456        }
457    }
458
459    /**
460     * Logs a trailing array comma violation if one exists.
461     *
462     * @param ast the array init
463     * {@link TokenTypes#ANNOTATION_ARRAY_INIT ANNOTATION_ARRAY_INIT}.
464     */
465    private void logCommaViolation(final DetailAST ast) {
466        final DetailAST rCurly = ast.findFirstToken(TokenTypes.RCURLY);
467
468        //comma can be null if array is empty
469        final DetailAST comma = rCurly.getPreviousSibling();
470
471        if (trailingArrayComma == TrailingArrayComma.ALWAYS
472            && (comma == null || comma.getType() != TokenTypes.COMMA)) {
473            log(rCurly.getLineNo(),
474                rCurly.getColumnNo(), MSG_KEY_ANNOTATION_TRAILING_COMMA_MISSING);
475        }
476        else if (trailingArrayComma == TrailingArrayComma.NEVER
477            && comma != null && comma.getType() == TokenTypes.COMMA) {
478            log(comma.getLineNo(),
479                comma.getColumnNo(), MSG_KEY_ANNOTATION_TRAILING_COMMA_PRESENT);
480        }
481    }
482
483    /**
484     * Checks to see if the closing parenthesis are present if required or
485     * prohibited.
486     *
487     * @param ast the annotation token
488     */
489    private void checkCheckClosingParens(final DetailAST ast) {
490        if (closingParens != ClosingParens.IGNORE) {
491            final DetailAST paren = ast.getLastChild();
492            final boolean parenExists = paren.getType() == TokenTypes.RPAREN;
493
494            if (closingParens == ClosingParens.ALWAYS
495                && !parenExists) {
496                log(ast.getLineNo(), MSG_KEY_ANNOTATION_PARENS_MISSING);
497            }
498            else if (closingParens == ClosingParens.NEVER
499                     && parenExists
500                     && !ast.branchContains(TokenTypes.EXPR)
501                     && !ast.branchContains(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR)
502                     && !ast.branchContains(TokenTypes.ANNOTATION_ARRAY_INIT)) {
503                log(ast.getLineNo(), MSG_KEY_ANNOTATION_PARENS_PRESENT);
504            }
505        }
506    }
507
508}