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