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