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