001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2015 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.Check;
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 Check {
105    /**
106     * A key is pointing to the warning message text in "messages.properties"
107     * file.
108     */
109    public static final String MSG_KEY_ANNOTATION_INCORRECT_STYLE =
110        "annotation.incorrect.style";
111
112    /**
113     * A key is pointing to the warning message text in "messages.properties"
114     * file.
115     */
116    public static final String MSG_KEY_ANNOTATION_PARENS_MISSING =
117        "annotation.parens.missing";
118
119    /**
120     * A key is pointing to the warning message text in "messages.properties"
121     * file.
122     */
123    public static final String MSG_KEY_ANNOTATION_PARENS_PRESENT =
124        "annotation.parens.present";
125
126    /**
127     * A key is pointing to the warning message text in "messages.properties"
128     * file.
129     */
130    public static final String MSG_KEY_ANNOTATION_TRAILING_COMMA_MISSING =
131        "annotation.trailing.comma.missing";
132
133    /**
134     * A key is pointing to the warning message text in "messages.properties"
135     * file.
136     */
137    public static final String MSG_KEY_ANNOTATION_TRAILING_COMMA_PRESENT =
138        "annotation.trailing.comma.present";
139
140    /**
141     * The element name used to receive special linguistic support
142     * for annotation use.
143     */
144    private static final String ANNOTATION_ELEMENT_SINGLE_NAME =
145            "value";
146
147    //not extending AbstractOptionCheck because check
148    //has more than one option type.
149
150    /**
151     * ElementStyle option.
152     * @see #setElementStyle(String)
153     */
154    private ElementStyle elementStyle = ElementStyle.COMPACT_NO_ARRAY;
155
156    //defaulting to NEVER because of the strange compiler behavior
157    /**
158     * Trailing array comma option.
159     * @see #setTrailingArrayComma(String)
160     */
161    private TrailingArrayComma trailingArrayComma = TrailingArrayComma.NEVER;
162
163    /**
164     * Closing parens option.
165     * @see #setClosingParens(String)
166     */
167    private ClosingParens closingParens = ClosingParens.NEVER;
168
169    /**
170     * Sets the ElementStyle from a string.
171     *
172     * @param style string representation
173     * @throws ConversionException if cannot convert string.
174     */
175    public void setElementStyle(final String style) {
176        elementStyle = getOption(ElementStyle.class, style);
177    }
178
179    /**
180     * Sets the TrailingArrayComma from a string.
181     *
182     * @param comma string representation
183     * @throws ConversionException if cannot convert string.
184     */
185    public void setTrailingArrayComma(final String comma) {
186        trailingArrayComma = getOption(TrailingArrayComma.class, comma);
187    }
188
189    /**
190     * Sets the ClosingParens from a string.
191     *
192     * @param parens string representation
193     * @throws ConversionException if cannot convert string.
194     */
195    public void setClosingParens(final String parens) {
196        closingParens = getOption(ClosingParens.class, parens);
197    }
198
199    /**
200     * Retrieves an {@link Enum Enum} type from a @{link String String}.
201     * @param <T> the enum type
202     * @param enumClass the enum class
203     * @param value the string representing the enum
204     * @return the enum type
205     */
206    private static <T extends Enum<T>> T getOption(final Class<T> enumClass,
207        final String value) {
208        try {
209            return Enum.valueOf(enumClass, value.trim().toUpperCase(Locale.ENGLISH));
210        }
211        catch (final IllegalArgumentException iae) {
212            throw new ConversionException("unable to parse " + value, iae);
213        }
214    }
215
216    @Override
217    public int[] getDefaultTokens() {
218        return getRequiredTokens();
219    }
220
221    @Override
222    public int[] getRequiredTokens() {
223        return new int[] {
224            TokenTypes.ANNOTATION,
225        };
226    }
227
228    @Override
229    public int[] getAcceptableTokens() {
230        return getRequiredTokens();
231    }
232
233    @Override
234    public void visitToken(final DetailAST ast) {
235        checkStyleType(ast);
236        checkCheckClosingParens(ast);
237        checkTrailingComma(ast);
238    }
239
240    /**
241     * Checks to see if the
242     * {@link ElementStyle AnnotationElementStyle}
243     * is correct.
244     *
245     * @param annotation the annotation token
246     */
247    private void checkStyleType(final DetailAST annotation) {
248
249        switch (elementStyle) {
250            case COMPACT_NO_ARRAY:
251                checkCompactNoArrayStyle(annotation);
252                break;
253            case COMPACT:
254                checkCompactStyle(annotation);
255                break;
256            case EXPANDED:
257                checkExpandedStyle(annotation);
258                break;
259            case IGNORE:
260            default:
261                break;
262        }
263    }
264
265    /**
266     * Checks for expanded style type violations.
267     *
268     * @param annotation the annotation token
269     */
270    private void checkExpandedStyle(final DetailAST annotation) {
271        final int valuePairCount =
272            annotation.getChildCount(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
273
274        if (valuePairCount == 0
275            && annotation.branchContains(TokenTypes.EXPR)) {
276            log(annotation.getLineNo(), MSG_KEY_ANNOTATION_INCORRECT_STYLE,
277                ElementStyle.EXPANDED);
278        }
279    }
280
281    /**
282     * Checks for compact style type violations.
283     *
284     * @param annotation the annotation token
285     */
286    private void checkCompactStyle(final DetailAST annotation) {
287        final int valuePairCount =
288            annotation.getChildCount(
289                TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
290
291        final DetailAST valuePair =
292            annotation.findFirstToken(
293                TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
294
295        if (valuePairCount == 1
296            && ANNOTATION_ELEMENT_SINGLE_NAME.equals(
297                valuePair.getFirstChild().getText())) {
298            log(annotation.getLineNo(), MSG_KEY_ANNOTATION_INCORRECT_STYLE,
299                ElementStyle.COMPACT);
300        }
301    }
302
303    /**
304     * Checks for compact no array style type violations.
305     *
306     * @param annotation the annotation token
307     */
308    private void checkCompactNoArrayStyle(final DetailAST annotation) {
309        final DetailAST arrayInit =
310            annotation.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
311
312        final int valuePairCount =
313            annotation.getChildCount(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
314
315        final DetailAST valuePair =
316            annotation.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
317
318        //in compact style with one value
319        if (arrayInit != null
320            && arrayInit.getChildCount(TokenTypes.EXPR) == 1) {
321            log(annotation.getLineNo(), MSG_KEY_ANNOTATION_INCORRECT_STYLE,
322                ElementStyle.COMPACT_NO_ARRAY);
323        }
324        //in expanded style with one value and the correct element name
325        else if (valuePairCount == 1) {
326            final DetailAST nestedArrayInit =
327                valuePair.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
328
329            if (nestedArrayInit != null
330                && ANNOTATION_ELEMENT_SINGLE_NAME.equals(
331                    valuePair.getFirstChild().getText())
332                    && nestedArrayInit.getChildCount(TokenTypes.EXPR) == 1) {
333                log(annotation.getLineNo(), MSG_KEY_ANNOTATION_INCORRECT_STYLE,
334                    ElementStyle.COMPACT_NO_ARRAY);
335            }
336        }
337    }
338
339    /**
340     * Checks to see if the trailing comma is present if required or
341     * prohibited.
342     *
343     * @param annotation the annotation token
344     */
345    private void checkTrailingComma(final DetailAST annotation) {
346        if (trailingArrayComma == TrailingArrayComma.IGNORE) {
347            return;
348        }
349
350        DetailAST child = annotation.getFirstChild();
351
352        while (child != null) {
353            DetailAST arrayInit = null;
354
355            if (child.getType() == TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR) {
356                arrayInit = child.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
357            }
358            else if (child.getType() == TokenTypes.ANNOTATION_ARRAY_INIT) {
359                arrayInit = child;
360            }
361
362            if (arrayInit != null) {
363                logCommaViolation(arrayInit);
364            }
365            child = child.getNextSibling();
366        }
367    }
368
369    /**
370     * Logs a trailing array comma violation if one exists.
371     *
372     * @param ast the array init
373     * {@link TokenTypes#ANNOTATION_ARRAY_INIT ANNOTATION_ARRAY_INIT}.
374     */
375    private void logCommaViolation(final DetailAST ast) {
376        final DetailAST rCurly = ast.findFirstToken(TokenTypes.RCURLY);
377
378        //comma can be null if array is empty
379        final DetailAST comma = rCurly.getPreviousSibling();
380
381        if (trailingArrayComma == TrailingArrayComma.ALWAYS
382            && (comma == null || comma.getType() != TokenTypes.COMMA)) {
383            log(rCurly.getLineNo(),
384                rCurly.getColumnNo(), MSG_KEY_ANNOTATION_TRAILING_COMMA_MISSING);
385        }
386        else if (trailingArrayComma == TrailingArrayComma.NEVER
387            && comma != null && comma.getType() == TokenTypes.COMMA) {
388            log(comma.getLineNo(),
389                comma.getColumnNo(), MSG_KEY_ANNOTATION_TRAILING_COMMA_PRESENT);
390        }
391    }
392
393    /**
394     * Checks to see if the closing parenthesis are present if required or
395     * prohibited.
396     *
397     * @param ast the annotation token
398     */
399    private void checkCheckClosingParens(final DetailAST ast) {
400        if (closingParens == ClosingParens.IGNORE) {
401            return;
402        }
403
404        final DetailAST paren = ast.getLastChild();
405        final boolean parenExists = paren.getType() == TokenTypes.RPAREN;
406
407        if (closingParens == ClosingParens.ALWAYS
408            && !parenExists) {
409            log(ast.getLineNo(), MSG_KEY_ANNOTATION_PARENS_MISSING);
410        }
411        else if (closingParens == ClosingParens.NEVER
412            && !ast.branchContains(TokenTypes.EXPR)
413            && !ast.branchContains(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR)
414            && !ast.branchContains(TokenTypes.ANNOTATION_ARRAY_INIT)
415            && parenExists) {
416            log(ast.getLineNo(), MSG_KEY_ANNOTATION_PARENS_PRESENT);
417        }
418    }
419
420    /**
421     * Defines the styles for defining elements in an annotation.
422     * @author Travis Schneeberger
423     */
424    public enum ElementStyle {
425
426        /**
427         * Expanded example
428         *
429         * <pre>@SuppressWarnings(value={"unchecked","unused",})</pre>.
430         */
431        EXPANDED,
432
433        /**
434         * Compact example
435         *
436         * <pre>@SuppressWarnings({"unchecked","unused",})</pre>
437         * <br>or<br>
438         * <pre>@SuppressWarnings("unchecked")</pre>.
439         */
440        COMPACT,
441
442        /**
443         * Compact example.]
444         *
445         * <pre>@SuppressWarnings("unchecked")</pre>.
446         */
447        COMPACT_NO_ARRAY,
448
449        /**
450         * Mixed styles.
451         */
452        IGNORE,
453    }
454
455    /**
456     * Defines the two styles for defining
457     * elements in an annotation.
458     *
459     * @author Travis Schneeberger
460     */
461    public enum TrailingArrayComma {
462
463        /**
464         * With comma example
465         *
466         * <pre>@SuppressWarnings(value={"unchecked","unused",})</pre>.
467         */
468        ALWAYS,
469
470        /**
471         * Without comma example
472         *
473         * <pre>@SuppressWarnings(value={"unchecked","unused"})</pre>.
474         */
475        NEVER,
476
477        /**
478         * Mixed styles.
479         */
480        IGNORE,
481    }
482
483    /**
484     * Defines the two styles for defining
485     * elements in an annotation.
486     *
487     * @author Travis Schneeberger
488     */
489    public enum ClosingParens {
490
491        /**
492         * With parens example
493         *
494         * <pre>@Deprecated()</pre>.
495         */
496        ALWAYS,
497
498        /**
499         * Without parens example
500         *
501         * <pre>@Deprecated</pre>.
502         */
503        NEVER,
504
505        /**
506         * Mixed styles.
507         */
508        IGNORE,
509    }
510}