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