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 {@literal &} 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: {@code @SuppressWarnings(value={"unchecked","unused",})}.
122         */
123        EXPANDED,
124
125        /**
126         * Compact example
127         * <br>
128         * {@code @SuppressWarnings({"unchecked","unused",})}
129         * <br>or<br>
130         * {@code @SuppressWarnings("unchecked")}.
131         */
132        COMPACT,
133
134        /**
135         * Compact example: {@code @SuppressWarnings("unchecked")}.
136         */
137        COMPACT_NO_ARRAY,
138
139        /**
140         * Mixed styles.
141         */
142        IGNORE,
143
144    }
145
146    /**
147     * Defines the two styles for defining elements in an annotation.
148     *
149     */
150    public enum TrailingArrayCommaOption {
151
152        /**
153         * With comma example: {@code @SuppressWarnings(value={"unchecked","unused",})}.
154         */
155        ALWAYS,
156
157        /**
158         * Without comma example: {@code @SuppressWarnings(value={"unchecked","unused"})}.
159         */
160        NEVER,
161
162        /**
163         * Mixed styles.
164         */
165        IGNORE,
166
167    }
168
169    /**
170     * Defines the two styles for defining elements in an annotation.
171     *
172     */
173    public enum ClosingParensOption {
174
175        /**
176         * With parens example :
177         * {@code @Deprecated()}.
178         */
179        ALWAYS,
180
181        /**
182         * Without parens example: {@code @Deprecated}.
183         */
184        NEVER,
185
186        /**
187         * Mixed styles.
188         */
189        IGNORE,
190
191    }
192
193    /**
194     * A key is pointing to the warning message text in "messages.properties" file.
195     */
196    public static final String MSG_KEY_ANNOTATION_INCORRECT_STYLE =
197        "annotation.incorrect.style";
198
199    /**
200     * A key is pointing to the warning message text in "messages.properties" file.
201     */
202    public static final String MSG_KEY_ANNOTATION_PARENS_MISSING =
203        "annotation.parens.missing";
204
205    /**
206     * A key is pointing to the warning message text in "messages.properties" file.
207     */
208    public static final String MSG_KEY_ANNOTATION_PARENS_PRESENT =
209        "annotation.parens.present";
210
211    /**
212     * A key is pointing to the warning message text in "messages.properties" file.
213     */
214    public static final String MSG_KEY_ANNOTATION_TRAILING_COMMA_MISSING =
215        "annotation.trailing.comma.missing";
216
217    /**
218     * A key is pointing to the warning message text in "messages.properties" file.
219     */
220    public static final String MSG_KEY_ANNOTATION_TRAILING_COMMA_PRESENT =
221        "annotation.trailing.comma.present";
222
223    /**
224     * The element name used to receive special linguistic support for annotation use.
225     */
226    private static final String ANNOTATION_ELEMENT_SINGLE_NAME =
227            "value";
228
229    /**
230     * Define the annotation element styles.
231     */
232    private ElementStyleOption elementStyle = ElementStyleOption.COMPACT_NO_ARRAY;
233
234    // defaulting to NEVER because of the strange compiler behavior
235    /**
236     * Define the policy for trailing comma in arrays.
237     */
238    private TrailingArrayCommaOption trailingArrayComma = TrailingArrayCommaOption.NEVER;
239
240    /**
241     * Define the policy for ending parenthesis.
242     */
243    private ClosingParensOption closingParens = ClosingParensOption.NEVER;
244
245    /**
246     * Creates a new {@code AnnotationUseStyleCheck} instance.
247     */
248    public AnnotationUseStyleCheck() {
249        // no code by default
250    }
251
252    /**
253     * Setter to define the annotation element styles.
254     *
255     * @param style string representation
256     * @since 5.0
257     */
258    public void setElementStyle(final String style) {
259        elementStyle = getOption(ElementStyleOption.class, style);
260    }
261
262    /**
263     * Setter to define the policy for trailing comma in arrays.
264     *
265     * @param comma string representation
266     * @since 5.0
267     */
268    public void setTrailingArrayComma(final String comma) {
269        trailingArrayComma = getOption(TrailingArrayCommaOption.class, comma);
270    }
271
272    /**
273     * Setter to define the policy for ending parenthesis.
274     *
275     * @param parens string representation
276     * @since 5.0
277     */
278    public void setClosingParens(final String parens) {
279        closingParens = getOption(ClosingParensOption.class, parens);
280    }
281
282    /**
283     * Retrieves an {@code Enum} type from a {@code String}.
284     *
285     * @param <T> the enum type
286     * @param enumClass the enum class
287     * @param value the string representing the enum
288     * @return the enum type
289     * @throws IllegalArgumentException when unable to parse value
290     */
291    private static <T extends Enum<T>> T getOption(final Class<T> enumClass,
292        final String value) {
293        try {
294            return Enum.valueOf(enumClass, value.trim().toUpperCase(Locale.ENGLISH));
295        }
296        catch (final IllegalArgumentException iae) {
297            throw new IllegalArgumentException("unable to parse " + value, iae);
298        }
299    }
300
301    @Override
302    public int[] getDefaultTokens() {
303        return getRequiredTokens();
304    }
305
306    @Override
307    public int[] getRequiredTokens() {
308        return new int[] {
309            TokenTypes.ANNOTATION,
310        };
311    }
312
313    @Override
314    public int[] getAcceptableTokens() {
315        return getRequiredTokens();
316    }
317
318    @Override
319    public void visitToken(final DetailAST ast) {
320        checkStyleType(ast);
321        checkCheckClosingParensOption(ast);
322        checkTrailingComma(ast);
323    }
324
325    /**
326     * Checks to see if the
327     * {@link ElementStyleOption AnnotationElementStyleOption} is correct.
328     *
329     * @param annotation the annotation token
330     */
331    private void checkStyleType(final DetailAST annotation) {
332        if (elementStyle == ElementStyleOption.COMPACT_NO_ARRAY) {
333            checkCompactNoArrayStyle(annotation);
334        }
335        else if (elementStyle == ElementStyleOption.COMPACT) {
336            checkCompactStyle(annotation);
337        }
338        else if (elementStyle == ElementStyleOption.EXPANDED) {
339            checkExpandedStyle(annotation);
340        }
341    }
342
343    /**
344     * Checks for expanded style type violations.
345     *
346     * @param annotation the annotation token
347     */
348    private void checkExpandedStyle(final DetailAST annotation) {
349        final int valuePairCount =
350            annotation.getChildCount(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
351
352        if (valuePairCount == 0 && hasArguments(annotation)) {
353            log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE, ElementStyleOption.EXPANDED);
354        }
355    }
356
357    /**
358     * Checks that annotation has arguments.
359     *
360     * @param annotation to check
361     * @return true if annotation has arguments, false otherwise
362     */
363    private static boolean hasArguments(DetailAST annotation) {
364        final DetailAST firstToken = annotation.findFirstToken(TokenTypes.LPAREN);
365        return firstToken != null && firstToken.getNextSibling().getType() != TokenTypes.RPAREN;
366    }
367
368    /**
369     * Checks for compact style type violations.
370     *
371     * @param annotation the annotation token
372     */
373    private void checkCompactStyle(final DetailAST annotation) {
374        final int valuePairCount =
375            annotation.getChildCount(
376                TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
377
378        final DetailAST valuePair =
379            annotation.findFirstToken(
380                TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR);
381
382        if (valuePairCount == 1
383            && ANNOTATION_ELEMENT_SINGLE_NAME.equals(
384                valuePair.getFirstChild().getText())) {
385            log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE,
386                ElementStyleOption.COMPACT);
387        }
388    }
389
390    /**
391     * Checks for compact no array style type violations.
392     *
393     * @param annotation the annotation token
394     */
395    private void checkCompactNoArrayStyle(final DetailAST annotation) {
396        final DetailAST arrayInit =
397            annotation.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
398
399        // in compact style with one value
400        if (arrayInit != null
401            && arrayInit.getChildCount(TokenTypes.EXPR) == 1) {
402            log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE,
403                ElementStyleOption.COMPACT_NO_ARRAY);
404        }
405        // in expanded style with pairs
406        else {
407            DetailAST ast = annotation.getFirstChild();
408            while (ast != null) {
409                final DetailAST nestedArrayInit =
410                    ast.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
411                if (nestedArrayInit != null
412                    && nestedArrayInit.getChildCount(TokenTypes.EXPR) == 1) {
413                    log(annotation, MSG_KEY_ANNOTATION_INCORRECT_STYLE,
414                        ElementStyleOption.COMPACT_NO_ARRAY);
415                }
416                ast = ast.getNextSibling();
417            }
418        }
419    }
420
421    /**
422     * Checks to see if the trailing comma is present if required or prohibited.
423     *
424     * @param annotation the annotation token
425     */
426    private void checkTrailingComma(final DetailAST annotation) {
427        if (trailingArrayComma != TrailingArrayCommaOption.IGNORE) {
428            DetailAST child = annotation.getFirstChild();
429
430            while (child != null) {
431                DetailAST arrayInit = null;
432
433                if (child.getType() == TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR) {
434                    arrayInit = child.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
435                }
436                else if (child.getType() == TokenTypes.ANNOTATION_ARRAY_INIT) {
437                    arrayInit = child;
438                }
439
440                if (arrayInit != null) {
441                    logCommaViolation(arrayInit);
442                }
443                child = child.getNextSibling();
444            }
445        }
446    }
447
448    /**
449     * Logs a trailing array comma violation if one exists.
450     *
451     * @param ast the array init
452     *     {@link TokenTypes#ANNOTATION_ARRAY_INIT ANNOTATION_ARRAY_INIT}.
453     */
454    private void logCommaViolation(final DetailAST ast) {
455        final DetailAST rCurly = ast.findFirstToken(TokenTypes.RCURLY);
456
457        // comma can be null if array is empty
458        final DetailAST comma = rCurly.getPreviousSibling();
459
460        if (trailingArrayComma == TrailingArrayCommaOption.NEVER) {
461            if (comma != null && comma.getType() == TokenTypes.COMMA) {
462                log(comma, MSG_KEY_ANNOTATION_TRAILING_COMMA_PRESENT);
463            }
464        }
465        else if (comma == null || comma.getType() != TokenTypes.COMMA) {
466            log(rCurly, MSG_KEY_ANNOTATION_TRAILING_COMMA_MISSING);
467        }
468    }
469
470    /**
471     * Checks to see if the closing parenthesis are present if required or prohibited.
472     *
473     * @param ast the annotation token
474     */
475    private void checkCheckClosingParensOption(final DetailAST ast) {
476        if (closingParens != ClosingParensOption.IGNORE) {
477            final DetailAST paren = ast.getLastChild();
478
479            if (closingParens == ClosingParensOption.NEVER) {
480                if (paren.getPreviousSibling().getType() == TokenTypes.LPAREN) {
481                    log(ast, MSG_KEY_ANNOTATION_PARENS_PRESENT);
482                }
483            }
484            else if (paren.getType() != TokenTypes.RPAREN) {
485                log(ast, MSG_KEY_ANNOTATION_PARENS_MISSING);
486            }
487        }
488    }
489
490}