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