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 EXPANDED, the TrailingArrayComma
073 * is set to NEVER, and the ClosingParens is set to ALWAYS.
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    //not extending AbstractOptionCheck because check
240    //has more than one option type.
241
242    /**
243     * ElementStyle option.
244     * @see #setElementStyle(String)
245     */
246    private ElementStyle elementStyle = ElementStyle.COMPACT_NO_ARRAY;
247
248    //defaulting to NEVER because of the strange compiler behavior
249    /**
250     * Trailing array comma option.
251     * @see #setTrailingArrayComma(String)
252     */
253    private TrailingArrayComma trailingArrayComma = TrailingArrayComma.NEVER;
254
255    /**
256     * Closing parens option.
257     * @see #setClosingParens(String)
258     */
259    private ClosingParens closingParens = ClosingParens.NEVER;
260
261    /**
262     * Sets the ElementStyle from a string.
263     *
264     * @param style string representation
265     * @throws ConversionException if cannot convert string.
266     */
267    public void setElementStyle(final String style) {
268        elementStyle = getOption(ElementStyle.class, style);
269    }
270
271    /**
272     * Sets the TrailingArrayComma from a string.
273     *
274     * @param comma string representation
275     * @throws ConversionException if cannot convert string.
276     */
277    public void setTrailingArrayComma(final String comma) {
278        trailingArrayComma = getOption(TrailingArrayComma.class, comma);
279    }
280
281    /**
282     * Sets the ClosingParens from a string.
283     *
284     * @param parens string representation
285     * @throws ConversionException if cannot convert string.
286     */
287    public void setClosingParens(final String parens) {
288        closingParens = getOption(ClosingParens.class, parens);
289    }
290
291    /**
292     * Retrieves an {@link Enum Enum} type from a @{link String String}.
293     * @param <T> the enum type
294     * @param enumClass the enum class
295     * @param value the string representing the enum
296     * @return the enum type
297     */
298    private static <T extends Enum<T>> T getOption(final Class<T> enumClass,
299        final String value) {
300        try {
301            return Enum.valueOf(enumClass, value.trim().toUpperCase(Locale.ENGLISH));
302        }
303        catch (final IllegalArgumentException iae) {
304            throw new IllegalArgumentException("unable to parse " + value, iae);
305        }
306    }
307
308    @Override
309    public int[] getDefaultTokens() {
310        return getRequiredTokens();
311    }
312
313    @Override
314    public int[] getRequiredTokens() {
315        return new int[] {
316            TokenTypes.ANNOTATION,
317        };
318    }
319
320    @Override
321    public int[] getAcceptableTokens() {
322        return getRequiredTokens();
323    }
324
325    @Override
326    public void visitToken(final DetailAST ast) {
327        checkStyleType(ast);
328        checkCheckClosingParens(ast);
329        checkTrailingComma(ast);
330    }
331
332    /**
333     * Checks to see if the
334     * {@link ElementStyle AnnotationElementStyle}
335     * is correct.
336     *
337     * @param annotation the annotation token
338     */
339    private void checkStyleType(final DetailAST annotation) {
340
341        switch (elementStyle) {
342            case COMPACT_NO_ARRAY:
343                checkCompactNoArrayStyle(annotation);
344                break;
345            case COMPACT:
346                checkCompactStyle(annotation);
347                break;
348            case EXPANDED:
349                checkExpandedStyle(annotation);
350                break;
351            case IGNORE:
352            default:
353                break;
354        }
355    }
356
357    /**
358     * Checks for expanded style type violations.
359     *
360     * @param annotation the annotation token
361     */
362    private void checkExpandedStyle(final DetailAST annotation) {
363        final int valuePairCount =
364            annotation.getChildCount(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
365
366        if (valuePairCount == 0
367            && annotation.branchContains(TokenTypes.EXPR)) {
368            log(annotation.getLineNo(), MSG_KEY_ANNOTATION_INCORRECT_STYLE,
369                ElementStyle.EXPANDED);
370        }
371    }
372
373    /**
374     * Checks for compact style type violations.
375     *
376     * @param annotation the annotation token
377     */
378    private void checkCompactStyle(final DetailAST annotation) {
379        final int valuePairCount =
380            annotation.getChildCount(
381                TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
382
383        final DetailAST valuePair =
384            annotation.findFirstToken(
385                TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
386
387        if (valuePairCount == 1
388            && ANNOTATION_ELEMENT_SINGLE_NAME.equals(
389                valuePair.getFirstChild().getText())) {
390            log(annotation.getLineNo(), MSG_KEY_ANNOTATION_INCORRECT_STYLE,
391                ElementStyle.COMPACT);
392        }
393    }
394
395    /**
396     * Checks for compact no array style type violations.
397     *
398     * @param annotation the annotation token
399     */
400    private void checkCompactNoArrayStyle(final DetailAST annotation) {
401        final DetailAST arrayInit =
402            annotation.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
403
404        final int valuePairCount =
405            annotation.getChildCount(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
406
407        //in compact style with one value
408        if (arrayInit != null
409            && arrayInit.getChildCount(TokenTypes.EXPR) == 1) {
410            log(annotation.getLineNo(), MSG_KEY_ANNOTATION_INCORRECT_STYLE,
411                ElementStyle.COMPACT_NO_ARRAY);
412        }
413        //in expanded style with one value and the correct element name
414        else if (valuePairCount == 1) {
415            final DetailAST valuePair =
416                    annotation.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
417            final DetailAST nestedArrayInit =
418                valuePair.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
419
420            if (nestedArrayInit != null
421                && ANNOTATION_ELEMENT_SINGLE_NAME.equals(
422                    valuePair.getFirstChild().getText())
423                    && nestedArrayInit.getChildCount(TokenTypes.EXPR) == 1) {
424                log(annotation.getLineNo(), MSG_KEY_ANNOTATION_INCORRECT_STYLE,
425                    ElementStyle.COMPACT_NO_ARRAY);
426            }
427        }
428    }
429
430    /**
431     * Checks to see if the trailing comma is present if required or
432     * prohibited.
433     *
434     * @param annotation the annotation token
435     */
436    private void checkTrailingComma(final DetailAST annotation) {
437        if (trailingArrayComma != TrailingArrayComma.IGNORE) {
438            DetailAST child = annotation.getFirstChild();
439
440            while (child != null) {
441                DetailAST arrayInit = null;
442
443                if (child.getType() == TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR) {
444                    arrayInit = child.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
445                }
446                else if (child.getType() == TokenTypes.ANNOTATION_ARRAY_INIT) {
447                    arrayInit = child;
448                }
449
450                if (arrayInit != null) {
451                    logCommaViolation(arrayInit);
452                }
453                child = child.getNextSibling();
454            }
455        }
456    }
457
458    /**
459     * Logs a trailing array comma violation if one exists.
460     *
461     * @param ast the array init
462     * {@link TokenTypes#ANNOTATION_ARRAY_INIT ANNOTATION_ARRAY_INIT}.
463     */
464    private void logCommaViolation(final DetailAST ast) {
465        final DetailAST rCurly = ast.findFirstToken(TokenTypes.RCURLY);
466
467        //comma can be null if array is empty
468        final DetailAST comma = rCurly.getPreviousSibling();
469
470        if (trailingArrayComma == TrailingArrayComma.ALWAYS
471            && (comma == null || comma.getType() != TokenTypes.COMMA)) {
472            log(rCurly.getLineNo(),
473                rCurly.getColumnNo(), MSG_KEY_ANNOTATION_TRAILING_COMMA_MISSING);
474        }
475        else if (trailingArrayComma == TrailingArrayComma.NEVER
476            && comma != null && comma.getType() == TokenTypes.COMMA) {
477            log(comma.getLineNo(),
478                comma.getColumnNo(), MSG_KEY_ANNOTATION_TRAILING_COMMA_PRESENT);
479        }
480    }
481
482    /**
483     * Checks to see if the closing parenthesis are present if required or
484     * prohibited.
485     *
486     * @param ast the annotation token
487     */
488    private void checkCheckClosingParens(final DetailAST ast) {
489        if (closingParens != ClosingParens.IGNORE) {
490            final DetailAST paren = ast.getLastChild();
491            final boolean parenExists = paren.getType() == TokenTypes.RPAREN;
492
493            if (closingParens == ClosingParens.ALWAYS
494                && !parenExists) {
495                log(ast.getLineNo(), MSG_KEY_ANNOTATION_PARENS_MISSING);
496            }
497            else if (closingParens == ClosingParens.NEVER
498                     && parenExists
499                     && !ast.branchContains(TokenTypes.EXPR)
500                     && !ast.branchContains(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR)
501                     && !ast.branchContains(TokenTypes.ANNOTATION_ARRAY_INIT)) {
502                log(ast.getLineNo(), MSG_KEY_ANNOTATION_PARENS_PRESENT);
503            }
504        }
505    }
506}