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 */
104@StatelessCheck
105public final class AnnotationUseStyleCheck extends AbstractCheck {
106
107    /**
108     * Defines the styles for defining elements in an annotation.
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    /**
143     * Defines the two styles for defining
144     * elements in an annotation.
145     *
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    /**
171     * Defines the two styles for defining
172     * elements in an annotation.
173     *
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    /**
199     * A key is pointing to the warning message text in "messages.properties"
200     * file.
201     */
202    public static final String MSG_KEY_ANNOTATION_INCORRECT_STYLE =
203        "annotation.incorrect.style";
204
205    /**
206     * A key is pointing to the warning message text in "messages.properties"
207     * file.
208     */
209    public static final String MSG_KEY_ANNOTATION_PARENS_MISSING =
210        "annotation.parens.missing";
211
212    /**
213     * A key is pointing to the warning message text in "messages.properties"
214     * file.
215     */
216    public static final String MSG_KEY_ANNOTATION_PARENS_PRESENT =
217        "annotation.parens.present";
218
219    /**
220     * A key is pointing to the warning message text in "messages.properties"
221     * file.
222     */
223    public static final String MSG_KEY_ANNOTATION_TRAILING_COMMA_MISSING =
224        "annotation.trailing.comma.missing";
225
226    /**
227     * A key is pointing to the warning message text in "messages.properties"
228     * file.
229     */
230    public static final String MSG_KEY_ANNOTATION_TRAILING_COMMA_PRESENT =
231        "annotation.trailing.comma.present";
232
233    /**
234     * The element name used to receive special linguistic support
235     * for annotation use.
236     */
237    private static final String ANNOTATION_ELEMENT_SINGLE_NAME =
238            "value";
239
240    /**
241     * ElementStyle option.
242     * @see #setElementStyle(String)
243     */
244    private ElementStyle elementStyle = ElementStyle.COMPACT_NO_ARRAY;
245
246    //defaulting to NEVER because of the strange compiler behavior
247    /**
248     * Trailing array comma option.
249     * @see #setTrailingArrayComma(String)
250     */
251    private TrailingArrayComma trailingArrayComma = TrailingArrayComma.NEVER;
252
253    /**
254     * Closing parens option.
255     * @see #setClosingParens(String)
256     */
257    private ClosingParens closingParens = ClosingParens.NEVER;
258
259    /**
260     * Sets the ElementStyle from a string.
261     *
262     * @param style string representation
263     * @throws ConversionException if cannot convert string.
264     */
265    public void setElementStyle(final String style) {
266        elementStyle = getOption(ElementStyle.class, style);
267    }
268
269    /**
270     * Sets the TrailingArrayComma from a string.
271     *
272     * @param comma string representation
273     * @throws ConversionException if cannot convert string.
274     */
275    public void setTrailingArrayComma(final String comma) {
276        trailingArrayComma = getOption(TrailingArrayComma.class, comma);
277    }
278
279    /**
280     * Sets the ClosingParens from a string.
281     *
282     * @param parens string representation
283     * @throws ConversionException if cannot convert string.
284     */
285    public void setClosingParens(final String parens) {
286        closingParens = getOption(ClosingParens.class, parens);
287    }
288
289    /**
290     * Retrieves an {@link Enum Enum} type from a @{link String String}.
291     * @param <T> the enum type
292     * @param enumClass the enum class
293     * @param value the string representing the enum
294     * @return the enum type
295     */
296    private static <T extends Enum<T>> T getOption(final Class<T> enumClass,
297        final String value) {
298        try {
299            return Enum.valueOf(enumClass, value.trim().toUpperCase(Locale.ENGLISH));
300        }
301        catch (final IllegalArgumentException iae) {
302            throw new IllegalArgumentException("unable to parse " + value, iae);
303        }
304    }
305
306    @Override
307    public int[] getDefaultTokens() {
308        return getRequiredTokens();
309    }
310
311    @Override
312    public int[] getRequiredTokens() {
313        return new int[] {
314            TokenTypes.ANNOTATION,
315        };
316    }
317
318    @Override
319    public int[] getAcceptableTokens() {
320        return getRequiredTokens();
321    }
322
323    @Override
324    public void visitToken(final DetailAST ast) {
325        checkStyleType(ast);
326        checkCheckClosingParens(ast);
327        checkTrailingComma(ast);
328    }
329
330    /**
331     * Checks to see if the
332     * {@link ElementStyle AnnotationElementStyle}
333     * is correct.
334     *
335     * @param annotation the annotation token
336     */
337    private void checkStyleType(final DetailAST annotation) {
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
504}