001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2020 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 * Checks the style of elements in 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     *
315     * @param <T> the enum type
316     * @param enumClass the enum class
317     * @param value the string representing the enum
318     * @return the enum type
319     * @throws IllegalArgumentException when unable to parse value
320     */
321    private static <T extends Enum<T>> T getOption(final Class<T> enumClass,
322        final String value) {
323        try {
324            return Enum.valueOf(enumClass, value.trim().toUpperCase(Locale.ENGLISH));
325        }
326        catch (final IllegalArgumentException iae) {
327            throw new IllegalArgumentException("unable to parse " + value, iae);
328        }
329    }
330
331    @Override
332    public int[] getDefaultTokens() {
333        return getRequiredTokens();
334    }
335
336    @Override
337    public int[] getRequiredTokens() {
338        return new int[] {
339            TokenTypes.ANNOTATION,
340        };
341    }
342
343    @Override
344    public int[] getAcceptableTokens() {
345        return getRequiredTokens();
346    }
347
348    @Override
349    public void visitToken(final DetailAST ast) {
350        checkStyleType(ast);
351        checkCheckClosingParens(ast);
352        checkTrailingComma(ast);
353    }
354
355    /**
356     * Checks to see if the
357     * {@link ElementStyle AnnotationElementStyle}
358     * is correct.
359     *
360     * @param annotation the annotation token
361     */
362    private void checkStyleType(final DetailAST annotation) {
363        switch (elementStyle) {
364            case COMPACT_NO_ARRAY:
365                checkCompactNoArrayStyle(annotation);
366                break;
367            case COMPACT:
368                checkCompactStyle(annotation);
369                break;
370            case EXPANDED:
371                checkExpandedStyle(annotation);
372                break;
373            case IGNORE:
374            default:
375                break;
376        }
377    }
378
379    /**
380     * Checks for expanded style type violations.
381     *
382     * @param annotation the annotation token
383     */
384    private void checkExpandedStyle(final DetailAST annotation) {
385        final int valuePairCount =
386            annotation.getChildCount(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
387
388        if (valuePairCount == 0 && hasArguments(annotation)) {
389            log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE, ElementStyle.EXPANDED);
390        }
391    }
392
393    /**
394     * Checks that annotation has arguments.
395     *
396     * @param annotation to check
397     * @return true if annotation has arguments, false otherwise
398     */
399    private static boolean hasArguments(DetailAST annotation) {
400        final DetailAST firstToken = annotation.findFirstToken(TokenTypes.LPAREN);
401        return firstToken != null && firstToken.getNextSibling().getType() != TokenTypes.RPAREN;
402    }
403
404    /**
405     * Checks for compact style type violations.
406     *
407     * @param annotation the annotation token
408     */
409    private void checkCompactStyle(final DetailAST annotation) {
410        final int valuePairCount =
411            annotation.getChildCount(
412                TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
413
414        final DetailAST valuePair =
415            annotation.findFirstToken(
416                TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
417
418        if (valuePairCount == 1
419            && ANNOTATION_ELEMENT_SINGLE_NAME.equals(
420                valuePair.getFirstChild().getText())) {
421            log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE,
422                ElementStyle.COMPACT);
423        }
424    }
425
426    /**
427     * Checks for compact no array style type violations.
428     *
429     * @param annotation the annotation token
430     */
431    private void checkCompactNoArrayStyle(final DetailAST annotation) {
432        final DetailAST arrayInit =
433            annotation.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
434
435        // in compact style with one value
436        if (arrayInit != null
437            && arrayInit.getChildCount(TokenTypes.EXPR) == 1) {
438            log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE,
439                ElementStyle.COMPACT_NO_ARRAY);
440        }
441        // in expanded style with pairs
442        else {
443            DetailAST ast = annotation.getFirstChild();
444            while (ast != null) {
445                final DetailAST nestedArrayInit =
446                    ast.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
447                if (nestedArrayInit != null
448                    && nestedArrayInit.getChildCount(TokenTypes.EXPR) == 1) {
449                    log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE,
450                        ElementStyle.COMPACT_NO_ARRAY);
451                }
452                ast = ast.getNextSibling();
453            }
454        }
455    }
456
457    /**
458     * Checks to see if the trailing comma is present if required or
459     * prohibited.
460     *
461     * @param annotation the annotation token
462     */
463    private void checkTrailingComma(final DetailAST annotation) {
464        if (trailingArrayComma != TrailingArrayComma.IGNORE) {
465            DetailAST child = annotation.getFirstChild();
466
467            while (child != null) {
468                DetailAST arrayInit = null;
469
470                if (child.getType() == TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR) {
471                    arrayInit = child.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
472                }
473                else if (child.getType() == TokenTypes.ANNOTATION_ARRAY_INIT) {
474                    arrayInit = child;
475                }
476
477                if (arrayInit != null) {
478                    logCommaViolation(arrayInit);
479                }
480                child = child.getNextSibling();
481            }
482        }
483    }
484
485    /**
486     * Logs a trailing array comma violation if one exists.
487     *
488     * @param ast the array init
489     * {@link TokenTypes#ANNOTATION_ARRAY_INIT ANNOTATION_ARRAY_INIT}.
490     */
491    private void logCommaViolation(final DetailAST ast) {
492        final DetailAST rCurly = ast.findFirstToken(TokenTypes.RCURLY);
493
494        // comma can be null if array is empty
495        final DetailAST comma = rCurly.getPreviousSibling();
496
497        if (trailingArrayComma == TrailingArrayComma.ALWAYS) {
498            if (comma == null || comma.getType() != TokenTypes.COMMA) {
499                log(rCurly, MSG_KEY_ANNOTATION_TRAILING_COMMA_MISSING);
500            }
501        }
502        else if (comma != null && comma.getType() == TokenTypes.COMMA) {
503            log(comma, MSG_KEY_ANNOTATION_TRAILING_COMMA_PRESENT);
504        }
505    }
506
507    /**
508     * Checks to see if the closing parenthesis are present if required or
509     * prohibited.
510     *
511     * @param ast the annotation token
512     */
513    private void checkCheckClosingParens(final DetailAST ast) {
514        if (closingParens != ClosingParens.IGNORE) {
515            final DetailAST paren = ast.getLastChild();
516
517            if (closingParens == ClosingParens.ALWAYS) {
518                if (paren.getType() != TokenTypes.RPAREN) {
519                    log(ast, MSG_KEY_ANNOTATION_PARENS_MISSING);
520                }
521            }
522            else if (paren.getPreviousSibling().getType() == TokenTypes.LPAREN) {
523                log(ast, MSG_KEY_ANNOTATION_PARENS_PRESENT);
524            }
525        }
526    }
527
528}