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