001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 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     * 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}