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