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