001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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     */
344    private void checkStyleType(final DetailAST annotation) {
345        if (elementStyle == ElementStyleOption.COMPACT_NO_ARRAY) {
346            checkCompactNoArrayStyle(annotation);
347        }
348        else if (elementStyle == ElementStyleOption.COMPACT) {
349            checkCompactStyle(annotation);
350        }
351        else if (elementStyle == ElementStyleOption.EXPANDED) {
352            checkExpandedStyle(annotation);
353        }
354    }
355
356    /**
357     * Checks for expanded style type violations.
358     *
359     * @param annotation the annotation token
360     */
361    private void checkExpandedStyle(final DetailAST annotation) {
362        final int valuePairCount =
363            annotation.getChildCount(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
364
365        if (valuePairCount == 0 && hasArguments(annotation)) {
366            log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE, ElementStyleOption.EXPANDED);
367        }
368    }
369
370    /**
371     * Checks that annotation has arguments.
372     *
373     * @param annotation to check
374     * @return true if annotation has arguments, false otherwise
375     */
376    private static boolean hasArguments(DetailAST annotation) {
377        final DetailAST firstToken = annotation.findFirstToken(TokenTypes.LPAREN);
378        return firstToken != null && firstToken.getNextSibling().getType() != TokenTypes.RPAREN;
379    }
380
381    /**
382     * Checks for compact style type violations.
383     *
384     * @param annotation the annotation token
385     */
386    private void checkCompactStyle(final DetailAST annotation) {
387        final int valuePairCount =
388            annotation.getChildCount(
389                TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
390
391        final DetailAST valuePair =
392            annotation.findFirstToken(
393                TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
394
395        if (valuePairCount == 1
396            && ANNOTATION_ELEMENT_SINGLE_NAME.equals(
397                valuePair.getFirstChild().getText())) {
398            log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE,
399                ElementStyleOption.COMPACT);
400        }
401    }
402
403    /**
404     * Checks for compact no array style type violations.
405     *
406     * @param annotation the annotation token
407     */
408    private void checkCompactNoArrayStyle(final DetailAST annotation) {
409        final DetailAST arrayInit =
410            annotation.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
411
412        // in compact style with one value
413        if (arrayInit != null
414            && arrayInit.getChildCount(TokenTypes.EXPR) == 1) {
415            log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE,
416                ElementStyleOption.COMPACT_NO_ARRAY);
417        }
418        // in expanded style with pairs
419        else {
420            DetailAST ast = annotation.getFirstChild();
421            while (ast != null) {
422                final DetailAST nestedArrayInit =
423                    ast.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
424                if (nestedArrayInit != null
425                    && nestedArrayInit.getChildCount(TokenTypes.EXPR) == 1) {
426                    log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE,
427                        ElementStyleOption.COMPACT_NO_ARRAY);
428                }
429                ast = ast.getNextSibling();
430            }
431        }
432    }
433
434    /**
435     * Checks to see if the trailing comma is present if required or
436     * prohibited.
437     *
438     * @param annotation the annotation token
439     */
440    private void checkTrailingComma(final DetailAST annotation) {
441        if (trailingArrayComma != TrailingArrayCommaOption.IGNORE) {
442            DetailAST child = annotation.getFirstChild();
443
444            while (child != null) {
445                DetailAST arrayInit = null;
446
447                if (child.getType() == TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR) {
448                    arrayInit = child.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
449                }
450                else if (child.getType() == TokenTypes.ANNOTATION_ARRAY_INIT) {
451                    arrayInit = child;
452                }
453
454                if (arrayInit != null) {
455                    logCommaViolation(arrayInit);
456                }
457                child = child.getNextSibling();
458            }
459        }
460    }
461
462    /**
463     * Logs a trailing array comma violation if one exists.
464     *
465     * @param ast the array init
466     *     {@link TokenTypes#ANNOTATION_ARRAY_INIT ANNOTATION_ARRAY_INIT}.
467     */
468    private void logCommaViolation(final DetailAST ast) {
469        final DetailAST rCurly = ast.findFirstToken(TokenTypes.RCURLY);
470
471        // comma can be null if array is empty
472        final DetailAST comma = rCurly.getPreviousSibling();
473
474        if (trailingArrayComma == TrailingArrayCommaOption.NEVER) {
475            if (comma != null && comma.getType() == TokenTypes.COMMA) {
476                log(comma, MSG_KEY_ANNOTATION_TRAILING_COMMA_PRESENT);
477            }
478        }
479        else if (comma == null || comma.getType() != TokenTypes.COMMA) {
480            log(rCurly, MSG_KEY_ANNOTATION_TRAILING_COMMA_MISSING);
481        }
482    }
483
484    /**
485     * Checks to see if the closing parenthesis are present if required or
486     * prohibited.
487     *
488     * @param ast the annotation token
489     */
490    private void checkCheckClosingParensOption(final DetailAST ast) {
491        if (closingParens != ClosingParensOption.IGNORE) {
492            final DetailAST paren = ast.getLastChild();
493
494            if (closingParens == ClosingParensOption.NEVER) {
495                if (paren.getPreviousSibling().getType() == TokenTypes.LPAREN) {
496                    log(ast, MSG_KEY_ANNOTATION_PARENS_PRESENT);
497                }
498            }
499            else if (paren.getType() != TokenTypes.RPAREN) {
500                log(ast, MSG_KEY_ANNOTATION_PARENS_MISSING);
501            }
502        }
503    }
504
505}