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