001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2023 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;SuppressWarnings("unchecked") // OK
130 * &#64;Deprecated // OK
131 * &#64;SomeArrays({"unchecked","unused"}) // OK
132 * public class TestOne
133 * {
134 *
135 * }
136 *
137 * &#64;SuppressWarnings(value={"unchecked"}) // Violation - parameter 'value' shouldn't be used
138 * &#64;Deprecated() // Violation - cannot have a closing parenthesis
139 * &#64;SomeArrays(value={"unchecked","unused",}) // Violation - cannot have a trailing array comma
140 * class TestTwo {
141 *
142 * }
143 * </pre>
144 * <p>
145 * To configure the check to enforce an {@code expanded} style,
146 *             with a closing parenthesis and a trailing array comma set to {@code never}.
147 * </p>
148 * <pre>
149 * &lt;module name=&quot;AnnotationUseStyle&quot;&gt;
150 *  &lt;property name=&quot;elementStyle&quot; value=&quot;expanded&quot;/&gt;
151 *  &lt;property name=&quot;closingParens&quot; value=&quot;never&quot;/&gt;
152 *  &lt;property name=&quot;trailingArrayComma&quot; value=&quot;never&quot;/&gt;
153 * &lt;/module&gt;
154 * </pre>
155 * <p>
156 * Example:
157 * </p>
158 * <pre>
159 * &#64;SuppressWarnings("unchecked") // Violation - parameters should be referenced
160 * &#64;Deprecated // OK
161 * &#64;SomeArrays({"unchecked","unused"}) // Violation - parameters should be referenced
162 * public class TestOne
163 * {
164 *
165 * }
166 *
167 * &#64;SuppressWarnings(value={"unchecked"}) // OK
168 * &#64;Deprecated() // Violation - cannot have a closing parenthesis
169 * &#64;SomeArrays(value={"unchecked","unused",}) // Violation - cannot have a trailing array comma
170 * class TestTwo {
171 *
172 * }
173 * </pre>
174 * <p>
175 * To configure the check to enforce a {@code compact} style,
176 *             with always including a closing parenthesis and ignoring a trailing array comma.
177 * </p>
178 * <pre>
179 * &lt;module name=&quot;AnnotationUseStyle&quot;&gt;
180 *  &lt;property name=&quot;elementStyle&quot; value=&quot;compact&quot;/&gt;
181 *  &lt;property name=&quot;closingParens&quot; value=&quot;always&quot;/&gt;
182 *  &lt;property name=&quot;trailingArrayComma&quot; value=&quot;ignore&quot;/&gt;
183 * &lt;/module&gt;
184 * </pre>
185 * <p>
186 * Example:
187 * </p>
188 * <pre>
189 * &#64;SuppressWarnings("unchecked") // OK
190 * &#64;Deprecated // Violation - must have a closing parenthesis
191 * &#64;SomeArrays({"unchecked","unused"}) // OK
192 * public class TestOne
193 * {
194 *
195 * }
196 *
197 * &#64;SuppressWarnings(value={"unchecked"}) // Violation - parameter 'value' shouldn't be used
198 * &#64;Deprecated() // OK
199 * &#64;SomeArrays(value={"unchecked","unused",}) // Violation - parameter 'value' shouldn't be used
200 * class TestTwo {
201 *
202 * }
203 * </pre>
204 * <p>
205 * To configure the check to enforce a trailing array comma,
206 *             with ignoring the elementStyle and a closing parenthesis.
207 * </p>
208 * <pre>
209 * &lt;module name=&quot;AnnotationUseStyle&quot;&gt;
210 *  &lt;property name=&quot;elementStyle&quot; value=&quot;ignore&quot;/&gt;
211 *  &lt;property name=&quot;closingParens&quot; value=&quot;ignore&quot;/&gt;
212 *  &lt;property name=&quot;trailingArrayComma&quot; value=&quot;always&quot;/&gt;
213 * &lt;/module&gt;
214 * </pre>
215 * <p>
216 * Example:
217 * </p>
218 * <pre>
219 * &#64;SuppressWarnings("unchecked") // OK
220 * &#64;Deprecated // OK
221 * &#64;SomeArrays({"unchecked","unused"}) // Violation - must have a trailing array comma
222 * public class TestOne
223 * {
224 *
225 * }
226 *
227 * &#64;SuppressWarnings(value={"unchecked"}) // Violation - must have a trailing array comma
228 * &#64;Deprecated() // OK
229 * &#64;SomeArrays(value={"unchecked","unused",})  // OK
230 * class TestTwo {
231 *
232 * }
233 * </pre>
234 * <p>
235 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
236 * </p>
237 * <p>
238 * Violation Message Keys:
239 * </p>
240 * <ul>
241 * <li>
242 * {@code annotation.incorrect.style}
243 * </li>
244 * <li>
245 * {@code annotation.parens.missing}
246 * </li>
247 * <li>
248 * {@code annotation.parens.present}
249 * </li>
250 * <li>
251 * {@code annotation.trailing.comma.missing}
252 * </li>
253 * <li>
254 * {@code annotation.trailing.comma.present}
255 * </li>
256 * </ul>
257 *
258 * @since 5.0
259 *
260 */
261@StatelessCheck
262public final class AnnotationUseStyleCheck extends AbstractCheck {
263
264    /**
265     * Defines the styles for defining elements in an annotation.
266     */
267    public enum ElementStyleOption {
268
269        /**
270         * Expanded example
271         *
272         * <pre>@SuppressWarnings(value={"unchecked","unused",})</pre>.
273         */
274        EXPANDED,
275
276        /**
277         * Compact example
278         *
279         * <pre>@SuppressWarnings({"unchecked","unused",})</pre>
280         * <br>or<br>
281         * <pre>@SuppressWarnings("unchecked")</pre>.
282         */
283        COMPACT,
284
285        /**
286         * Compact example
287         *
288         * <pre>@SuppressWarnings("unchecked")</pre>.
289         */
290        COMPACT_NO_ARRAY,
291
292        /**
293         * Mixed styles.
294         */
295        IGNORE,
296
297    }
298
299    /**
300     * Defines the two styles for defining
301     * elements in an annotation.
302     *
303     */
304    public enum TrailingArrayCommaOption {
305
306        /**
307         * With comma example
308         *
309         * <pre>@SuppressWarnings(value={"unchecked","unused",})</pre>.
310         */
311        ALWAYS,
312
313        /**
314         * Without comma example
315         *
316         * <pre>@SuppressWarnings(value={"unchecked","unused"})</pre>.
317         */
318        NEVER,
319
320        /**
321         * Mixed styles.
322         */
323        IGNORE,
324
325    }
326
327    /**
328     * Defines the two styles for defining
329     * elements in an annotation.
330     *
331     */
332    public enum ClosingParensOption {
333
334        /**
335         * With parens example
336         *
337         * <pre>@Deprecated()</pre>.
338         */
339        ALWAYS,
340
341        /**
342         * Without parens example
343         *
344         * <pre>@Deprecated</pre>.
345         */
346        NEVER,
347
348        /**
349         * Mixed styles.
350         */
351        IGNORE,
352
353    }
354
355    /**
356     * A key is pointing to the warning message text in "messages.properties"
357     * file.
358     */
359    public static final String MSG_KEY_ANNOTATION_INCORRECT_STYLE =
360        "annotation.incorrect.style";
361
362    /**
363     * A key is pointing to the warning message text in "messages.properties"
364     * file.
365     */
366    public static final String MSG_KEY_ANNOTATION_PARENS_MISSING =
367        "annotation.parens.missing";
368
369    /**
370     * A key is pointing to the warning message text in "messages.properties"
371     * file.
372     */
373    public static final String MSG_KEY_ANNOTATION_PARENS_PRESENT =
374        "annotation.parens.present";
375
376    /**
377     * A key is pointing to the warning message text in "messages.properties"
378     * file.
379     */
380    public static final String MSG_KEY_ANNOTATION_TRAILING_COMMA_MISSING =
381        "annotation.trailing.comma.missing";
382
383    /**
384     * A key is pointing to the warning message text in "messages.properties"
385     * file.
386     */
387    public static final String MSG_KEY_ANNOTATION_TRAILING_COMMA_PRESENT =
388        "annotation.trailing.comma.present";
389
390    /**
391     * The element name used to receive special linguistic support
392     * for annotation use.
393     */
394    private static final String ANNOTATION_ELEMENT_SINGLE_NAME =
395            "value";
396
397    /**
398     * Define the annotation element styles.
399     */
400    private ElementStyleOption elementStyle = ElementStyleOption.COMPACT_NO_ARRAY;
401
402    // defaulting to NEVER because of the strange compiler behavior
403    /**
404     * Define the policy for trailing comma in arrays.
405     */
406    private TrailingArrayCommaOption trailingArrayComma = TrailingArrayCommaOption.NEVER;
407
408    /**
409     * Define the policy for ending parenthesis.
410     */
411    private ClosingParensOption closingParens = ClosingParensOption.NEVER;
412
413    /**
414     * Setter to define the annotation element styles.
415     *
416     * @param style string representation
417     */
418    public void setElementStyle(final String style) {
419        elementStyle = getOption(ElementStyleOption.class, style);
420    }
421
422    /**
423     * Setter to define the policy for trailing comma in arrays.
424     *
425     * @param comma string representation
426     */
427    public void setTrailingArrayComma(final String comma) {
428        trailingArrayComma = getOption(TrailingArrayCommaOption.class, comma);
429    }
430
431    /**
432     * Setter to define the policy for ending parenthesis.
433     *
434     * @param parens string representation
435     */
436    public void setClosingParens(final String parens) {
437        closingParens = getOption(ClosingParensOption.class, parens);
438    }
439
440    /**
441     * Retrieves an {@link Enum Enum} type from a @{link String String}.
442     *
443     * @param <T> the enum type
444     * @param enumClass the enum class
445     * @param value the string representing the enum
446     * @return the enum type
447     * @throws IllegalArgumentException when unable to parse value
448     */
449    private static <T extends Enum<T>> T getOption(final Class<T> enumClass,
450        final String value) {
451        try {
452            return Enum.valueOf(enumClass, value.trim().toUpperCase(Locale.ENGLISH));
453        }
454        catch (final IllegalArgumentException iae) {
455            throw new IllegalArgumentException("unable to parse " + value, iae);
456        }
457    }
458
459    @Override
460    public int[] getDefaultTokens() {
461        return getRequiredTokens();
462    }
463
464    @Override
465    public int[] getRequiredTokens() {
466        return new int[] {
467            TokenTypes.ANNOTATION,
468        };
469    }
470
471    @Override
472    public int[] getAcceptableTokens() {
473        return getRequiredTokens();
474    }
475
476    @Override
477    public void visitToken(final DetailAST ast) {
478        checkStyleType(ast);
479        checkCheckClosingParensOption(ast);
480        checkTrailingComma(ast);
481    }
482
483    /**
484     * Checks to see if the
485     * {@link ElementStyleOption AnnotationElementStyleOption}
486     * is correct.
487     *
488     * @param annotation the annotation token
489     */
490    private void checkStyleType(final DetailAST annotation) {
491        switch (elementStyle) {
492            case COMPACT_NO_ARRAY:
493                checkCompactNoArrayStyle(annotation);
494                break;
495            case COMPACT:
496                checkCompactStyle(annotation);
497                break;
498            case EXPANDED:
499                checkExpandedStyle(annotation);
500                break;
501            case IGNORE:
502            default:
503                break;
504        }
505    }
506
507    /**
508     * Checks for expanded style type violations.
509     *
510     * @param annotation the annotation token
511     */
512    private void checkExpandedStyle(final DetailAST annotation) {
513        final int valuePairCount =
514            annotation.getChildCount(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
515
516        if (valuePairCount == 0 && hasArguments(annotation)) {
517            log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE, ElementStyleOption.EXPANDED);
518        }
519    }
520
521    /**
522     * Checks that annotation has arguments.
523     *
524     * @param annotation to check
525     * @return true if annotation has arguments, false otherwise
526     */
527    private static boolean hasArguments(DetailAST annotation) {
528        final DetailAST firstToken = annotation.findFirstToken(TokenTypes.LPAREN);
529        return firstToken != null && firstToken.getNextSibling().getType() != TokenTypes.RPAREN;
530    }
531
532    /**
533     * Checks for compact style type violations.
534     *
535     * @param annotation the annotation token
536     */
537    private void checkCompactStyle(final DetailAST annotation) {
538        final int valuePairCount =
539            annotation.getChildCount(
540                TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
541
542        final DetailAST valuePair =
543            annotation.findFirstToken(
544                TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
545
546        if (valuePairCount == 1
547            && ANNOTATION_ELEMENT_SINGLE_NAME.equals(
548                valuePair.getFirstChild().getText())) {
549            log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE,
550                ElementStyleOption.COMPACT);
551        }
552    }
553
554    /**
555     * Checks for compact no array style type violations.
556     *
557     * @param annotation the annotation token
558     */
559    private void checkCompactNoArrayStyle(final DetailAST annotation) {
560        final DetailAST arrayInit =
561            annotation.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
562
563        // in compact style with one value
564        if (arrayInit != null
565            && arrayInit.getChildCount(TokenTypes.EXPR) == 1) {
566            log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE,
567                ElementStyleOption.COMPACT_NO_ARRAY);
568        }
569        // in expanded style with pairs
570        else {
571            DetailAST ast = annotation.getFirstChild();
572            while (ast != null) {
573                final DetailAST nestedArrayInit =
574                    ast.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
575                if (nestedArrayInit != null
576                    && nestedArrayInit.getChildCount(TokenTypes.EXPR) == 1) {
577                    log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE,
578                        ElementStyleOption.COMPACT_NO_ARRAY);
579                }
580                ast = ast.getNextSibling();
581            }
582        }
583    }
584
585    /**
586     * Checks to see if the trailing comma is present if required or
587     * prohibited.
588     *
589     * @param annotation the annotation token
590     */
591    private void checkTrailingComma(final DetailAST annotation) {
592        if (trailingArrayComma != TrailingArrayCommaOption.IGNORE) {
593            DetailAST child = annotation.getFirstChild();
594
595            while (child != null) {
596                DetailAST arrayInit = null;
597
598                if (child.getType() == TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR) {
599                    arrayInit = child.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
600                }
601                else if (child.getType() == TokenTypes.ANNOTATION_ARRAY_INIT) {
602                    arrayInit = child;
603                }
604
605                if (arrayInit != null) {
606                    logCommaViolation(arrayInit);
607                }
608                child = child.getNextSibling();
609            }
610        }
611    }
612
613    /**
614     * Logs a trailing array comma violation if one exists.
615     *
616     * @param ast the array init
617     *     {@link TokenTypes#ANNOTATION_ARRAY_INIT ANNOTATION_ARRAY_INIT}.
618     */
619    private void logCommaViolation(final DetailAST ast) {
620        final DetailAST rCurly = ast.findFirstToken(TokenTypes.RCURLY);
621
622        // comma can be null if array is empty
623        final DetailAST comma = rCurly.getPreviousSibling();
624
625        if (trailingArrayComma == TrailingArrayCommaOption.NEVER) {
626            if (comma != null && comma.getType() == TokenTypes.COMMA) {
627                log(comma, MSG_KEY_ANNOTATION_TRAILING_COMMA_PRESENT);
628            }
629        }
630        else if (comma == null || comma.getType() != TokenTypes.COMMA) {
631            log(rCurly, MSG_KEY_ANNOTATION_TRAILING_COMMA_MISSING);
632        }
633    }
634
635    /**
636     * Checks to see if the closing parenthesis are present if required or
637     * prohibited.
638     *
639     * @param ast the annotation token
640     */
641    private void checkCheckClosingParensOption(final DetailAST ast) {
642        if (closingParens != ClosingParensOption.IGNORE) {
643            final DetailAST paren = ast.getLastChild();
644
645            if (closingParens == ClosingParensOption.NEVER) {
646                if (paren.getPreviousSibling().getType() == TokenTypes.LPAREN) {
647                    log(ast, MSG_KEY_ANNOTATION_PARENS_PRESENT);
648                }
649            }
650            else if (paren.getType() != TokenTypes.RPAREN) {
651                log(ast, MSG_KEY_ANNOTATION_PARENS_MISSING);
652            }
653        }
654    }
655
656}