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