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