| //////////////////////////////////////////////////////////////////////////////// |
| // checkstyle: Checks Java source code for adherence to a set of rules. |
| // Copyright (C) 2001-2017 the original author or authors. |
| // |
| // This library is free software; you can redistribute it and/or |
| // modify it under the terms of the GNU Lesser General Public |
| // License as published by the Free Software Foundation; either |
| // version 2.1 of the License, or (at your option) any later version. |
| // |
| // This library is distributed in the hope that it will be useful, |
| // but WITHOUT ANY WARRANTY; without even the implied warranty of |
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| // Lesser General Public License for more details. |
| // |
| // You should have received a copy of the GNU Lesser General Public |
| // License along with this library; if not, write to the Free Software |
| // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| package com.puppycrawl.tools.checkstyle.checks.annotation; |
| |
| import java.util.Locale; |
| |
| import org.apache.commons.beanutils.ConversionException; |
| |
| import com.puppycrawl.tools.checkstyle.StatelessCheck; |
| import com.puppycrawl.tools.checkstyle.api.AbstractCheck; |
| import com.puppycrawl.tools.checkstyle.api.DetailAST; |
| import com.puppycrawl.tools.checkstyle.api.TokenTypes; |
| |
| /** |
| * This check controls the style with the usage of annotations. |
| * |
| * <p>Annotations have three element styles starting with the least verbose. |
| * <ul> |
| * <li>{@link ElementStyle#COMPACT_NO_ARRAY COMPACT_NO_ARRAY}</li> |
| * <li>{@link ElementStyle#COMPACT COMPACT}</li> |
| * <li>{@link ElementStyle#EXPANDED EXPANDED}</li> |
| * </ul> |
| * To not enforce an element style |
| * a {@link ElementStyle#IGNORE IGNORE} type is provided. The desired style |
| * can be set through the {@code elementStyle} property. |
| * |
| * <p>Using the EXPANDED style is more verbose. The expanded version |
| * is sometimes referred to as "named parameters" in other languages. |
| * |
| * <p>Using the COMPACT style is less verbose. This style can only |
| * be used when there is an element called 'value' which is either |
| * the sole element or all other elements have default values. |
| * |
| * <p>Using the COMPACT_NO_ARRAY style is less verbose. It is similar |
| * to the COMPACT style but single value arrays are flagged. With |
| * annotations a single value array does not need to be placed in an |
| * array initializer. This style can only be used when there is an |
| * element called 'value' which is either the sole element or all other |
| * elements have default values. |
| * |
| * <p>The ending parenthesis are optional when using annotations with no elements. |
| * To always require ending parenthesis use the |
| * {@link ClosingParens#ALWAYS ALWAYS} type. To never have ending parenthesis |
| * use the {@link ClosingParens#NEVER NEVER} type. To not enforce a |
| * closing parenthesis preference a {@link ClosingParens#IGNORE IGNORE} type is |
| * provided. Set this through the {@code closingParens} property. |
| * |
| * <p>Annotations also allow you to specify arrays of elements in a standard |
| * format. As with normal arrays, a trailing comma is optional. To always |
| * require a trailing comma use the {@link TrailingArrayComma#ALWAYS ALWAYS} |
| * type. To never have a trailing comma use the |
| * {@link TrailingArrayComma#NEVER NEVER} type. To not enforce a trailing |
| * array comma preference a {@link TrailingArrayComma#IGNORE IGNORE} type |
| * is provided. Set this through the {@code trailingArrayComma} property. |
| * |
| * <p>By default the ElementStyle is set to COMPACT_NO_ARRAY, the |
| * TrailingArrayComma is set to NEVER, and the ClosingParens is set to NEVER. |
| * |
| * <p>According to the JLS, it is legal to include a trailing comma |
| * in arrays used in annotations but Sun's Java 5 & 6 compilers will not |
| * compile with this syntax. This may in be a bug in Sun's compilers |
| * since eclipse 3.4's built-in compiler does allow this syntax as |
| * defined in the JLS. Note: this was tested with compilers included with |
| * JDK versions 1.5.0.17 and 1.6.0.11 and the compiler included with eclipse |
| * 3.4.1. |
| * |
| * <p>See <a |
| * href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-9.html#jls-9.7"> |
| * Java Language specification, §9.7</a>. |
| * |
| * <p>An example shown below is set to enforce an EXPANDED style, with a |
| * trailing array comma set to NEVER and always including the closing |
| * parenthesis. |
| * |
| * <pre> |
| * <module name="AnnotationUseStyle"> |
| * <property name="ElementStyle" |
| * value="EXPANDED"/> |
| * <property name="TrailingArrayComma" |
| * value="NEVER"/> |
| * <property name="ClosingParens" |
| * value="ALWAYS"/> |
| * </module> |
| * </pre> |
| * |
| * @author Travis Schneeberger |
| */ |
| @StatelessCheck |
| public final class AnnotationUseStyleCheck extends AbstractCheck { |
| |
| /** |
| * Defines the styles for defining elements in an annotation. |
| * @author Travis Schneeberger |
| */ |
| public enum ElementStyle { |
| |
| /** |
| * Expanded example |
| * |
| * <pre>@SuppressWarnings(value={"unchecked","unused",})</pre>. |
| */ |
| EXPANDED, |
| |
| /** |
| * Compact example |
| * |
| * <pre>@SuppressWarnings({"unchecked","unused",})</pre> |
| * <br>or<br> |
| * <pre>@SuppressWarnings("unchecked")</pre>. |
| */ |
| COMPACT, |
| |
| /** |
| * Compact example.] |
| * |
| * <pre>@SuppressWarnings("unchecked")</pre>. |
| */ |
| COMPACT_NO_ARRAY, |
| |
| /** |
| * Mixed styles. |
| */ |
| IGNORE, |
| } |
| |
| /** |
| * Defines the two styles for defining |
| * elements in an annotation. |
| * |
| * @author Travis Schneeberger |
| */ |
| public enum TrailingArrayComma { |
| |
| /** |
| * With comma example |
| * |
| * <pre>@SuppressWarnings(value={"unchecked","unused",})</pre>. |
| */ |
| ALWAYS, |
| |
| /** |
| * Without comma example |
| * |
| * <pre>@SuppressWarnings(value={"unchecked","unused"})</pre>. |
| */ |
| NEVER, |
| |
| /** |
| * Mixed styles. |
| */ |
| IGNORE, |
| } |
| |
| /** |
| * Defines the two styles for defining |
| * elements in an annotation. |
| * |
| * @author Travis Schneeberger |
| */ |
| public enum ClosingParens { |
| |
| /** |
| * With parens example |
| * |
| * <pre>@Deprecated()</pre>. |
| */ |
| ALWAYS, |
| |
| /** |
| * Without parens example |
| * |
| * <pre>@Deprecated</pre>. |
| */ |
| NEVER, |
| |
| /** |
| * Mixed styles. |
| */ |
| IGNORE, |
| } |
| |
| /** |
| * A key is pointing to the warning message text in "messages.properties" |
| * file. |
| */ |
| public static final String MSG_KEY_ANNOTATION_INCORRECT_STYLE = |
| "annotation.incorrect.style"; |
| |
| /** |
| * A key is pointing to the warning message text in "messages.properties" |
| * file. |
| */ |
| public static final String MSG_KEY_ANNOTATION_PARENS_MISSING = |
| "annotation.parens.missing"; |
| |
| /** |
| * A key is pointing to the warning message text in "messages.properties" |
| * file. |
| */ |
| public static final String MSG_KEY_ANNOTATION_PARENS_PRESENT = |
| "annotation.parens.present"; |
| |
| /** |
| * A key is pointing to the warning message text in "messages.properties" |
| * file. |
| */ |
| public static final String MSG_KEY_ANNOTATION_TRAILING_COMMA_MISSING = |
| "annotation.trailing.comma.missing"; |
| |
| /** |
| * A key is pointing to the warning message text in "messages.properties" |
| * file. |
| */ |
| public static final String MSG_KEY_ANNOTATION_TRAILING_COMMA_PRESENT = |
| "annotation.trailing.comma.present"; |
| |
| /** |
| * The element name used to receive special linguistic support |
| * for annotation use. |
| */ |
| private static final String ANNOTATION_ELEMENT_SINGLE_NAME = |
| "value"; |
| |
| /** |
| * ElementStyle option. |
| * @see #setElementStyle(String) |
| */ |
| private ElementStyle elementStyle = ElementStyle.COMPACT_NO_ARRAY; |
| |
| //defaulting to NEVER because of the strange compiler behavior |
| /** |
| * Trailing array comma option. |
| * @see #setTrailingArrayComma(String) |
| */ |
| private TrailingArrayComma trailingArrayComma = TrailingArrayComma.NEVER; |
| |
| /** |
| * Closing parens option. |
| * @see #setClosingParens(String) |
| */ |
| private ClosingParens closingParens = ClosingParens.NEVER; |
| |
| /** |
| * Sets the ElementStyle from a string. |
| * |
| * @param style string representation |
| * @throws ConversionException if cannot convert string. |
| */ |
| public void setElementStyle(final String style) { |
| elementStyle = getOption(ElementStyle.class, style); |
| } |
| |
| /** |
| * Sets the TrailingArrayComma from a string. |
| * |
| * @param comma string representation |
| * @throws ConversionException if cannot convert string. |
| */ |
| public void setTrailingArrayComma(final String comma) { |
| trailingArrayComma = getOption(TrailingArrayComma.class, comma); |
| } |
| |
| /** |
| * Sets the ClosingParens from a string. |
| * |
| * @param parens string representation |
| * @throws ConversionException if cannot convert string. |
| */ |
| public void setClosingParens(final String parens) { |
| closingParens = getOption(ClosingParens.class, parens); |
| } |
| |
| /** |
| * Retrieves an {@link Enum Enum} type from a @{link String String}. |
| * @param <T> the enum type |
| * @param enumClass the enum class |
| * @param value the string representing the enum |
| * @return the enum type |
| */ |
| private static <T extends Enum<T>> T getOption(final Class<T> enumClass, |
| final String value) { |
| try { |
| return Enum.valueOf(enumClass, value.trim().toUpperCase(Locale.ENGLISH)); |
| } |
| catch (final IllegalArgumentException iae) { |
| throw new IllegalArgumentException("unable to parse " + value, iae); |
| } |
| } |
| |
| @Override |
| public int[] getDefaultTokens() { |
| return getRequiredTokens(); |
| } |
| |
| @Override |
| public int[] getRequiredTokens() { |
| return new int[] { |
| TokenTypes.ANNOTATION, |
| }; |
| } |
| |
| @Override |
| public int[] getAcceptableTokens() { |
| return getRequiredTokens(); |
| } |
| |
| @Override |
| public void visitToken(final DetailAST ast) { |
| checkStyleType(ast); |
| checkCheckClosingParens(ast); |
| checkTrailingComma(ast); |
| } |
| |
| /** |
| * Checks to see if the |
| * {@link ElementStyle AnnotationElementStyle} |
| * is correct. |
| * |
| * @param annotation the annotation token |
| */ |
| private void checkStyleType(final DetailAST annotation) { |
| |
| switch (elementStyle) { |
| case COMPACT_NO_ARRAY: |
| checkCompactNoArrayStyle(annotation); |
| break; |
| case COMPACT: |
| checkCompactStyle(annotation); |
| break; |
| case EXPANDED: |
| checkExpandedStyle(annotation); |
| break; |
| case IGNORE: |
| default: |
| break; |
| } |
| } |
| |
| /** |
| * Checks for expanded style type violations. |
| * |
| * @param annotation the annotation token |
| */ |
| private void checkExpandedStyle(final DetailAST annotation) { |
| final int valuePairCount = |
| annotation.getChildCount(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR); |
| |
| if (valuePairCount == 0 |
| && annotation.branchContains(TokenTypes.EXPR)) { |
| log(annotation.getLineNo(), MSG_KEY_ANNOTATION_INCORRECT_STYLE, |
| ElementStyle.EXPANDED); |
| } |
| } |
| |
| /** |
| * Checks for compact style type violations. |
| * |
| * @param annotation the annotation token |
| */ |
| private void checkCompactStyle(final DetailAST annotation) { |
| final int valuePairCount = |
| annotation.getChildCount( |
| TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR); |
| |
| final DetailAST valuePair = |
| annotation.findFirstToken( |
| TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR); |
| |
| if (valuePairCount == 1 |
| && ANNOTATION_ELEMENT_SINGLE_NAME.equals( |
| valuePair.getFirstChild().getText())) { |
| log(annotation.getLineNo(), MSG_KEY_ANNOTATION_INCORRECT_STYLE, |
| ElementStyle.COMPACT); |
| } |
| } |
| |
| /** |
| * Checks for compact no array style type violations. |
| * |
| * @param annotation the annotation token |
| */ |
| private void checkCompactNoArrayStyle(final DetailAST annotation) { |
| final DetailAST arrayInit = |
| annotation.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT); |
| |
| final int valuePairCount = |
| annotation.getChildCount(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR); |
| |
| //in compact style with one value |
| if (arrayInit != null |
| && arrayInit.getChildCount(TokenTypes.EXPR) == 1) { |
| log(annotation.getLineNo(), MSG_KEY_ANNOTATION_INCORRECT_STYLE, |
| ElementStyle.COMPACT_NO_ARRAY); |
| } |
| //in expanded style with one value and the correct element name |
| else if (valuePairCount == 1) { |
| final DetailAST valuePair = |
| annotation.findFirstToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR); |
| final DetailAST nestedArrayInit = |
| valuePair.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT); |
| |
| if (nestedArrayInit != null |
| && ANNOTATION_ELEMENT_SINGLE_NAME.equals( |
| valuePair.getFirstChild().getText()) |
| && nestedArrayInit.getChildCount(TokenTypes.EXPR) == 1) { |
| log(annotation.getLineNo(), MSG_KEY_ANNOTATION_INCORRECT_STYLE, |
| ElementStyle.COMPACT_NO_ARRAY); |
| } |
| } |
| } |
| |
| /** |
| * Checks to see if the trailing comma is present if required or |
| * prohibited. |
| * |
| * @param annotation the annotation token |
| */ |
| private void checkTrailingComma(final DetailAST annotation) { |
| if (trailingArrayComma != TrailingArrayComma.IGNORE) { |
| DetailAST child = annotation.getFirstChild(); |
| |
| while (child != null) { |
| DetailAST arrayInit = null; |
| |
| if (child.getType() == TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR) { |
| arrayInit = child.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT); |
| } |
| else if (child.getType() == TokenTypes.ANNOTATION_ARRAY_INIT) { |
| arrayInit = child; |
| } |
| |
| if (arrayInit != null) { |
| logCommaViolation(arrayInit); |
| } |
| child = child.getNextSibling(); |
| } |
| } |
| } |
| |
| /** |
| * Logs a trailing array comma violation if one exists. |
| * |
| * @param ast the array init |
| * {@link TokenTypes#ANNOTATION_ARRAY_INIT ANNOTATION_ARRAY_INIT}. |
| */ |
| private void logCommaViolation(final DetailAST ast) { |
| final DetailAST rCurly = ast.findFirstToken(TokenTypes.RCURLY); |
| |
| //comma can be null if array is empty |
| final DetailAST comma = rCurly.getPreviousSibling(); |
| |
| if (trailingArrayComma == TrailingArrayComma.ALWAYS |
| && (comma == null || comma.getType() != TokenTypes.COMMA)) { |
| log(rCurly.getLineNo(), |
| rCurly.getColumnNo(), MSG_KEY_ANNOTATION_TRAILING_COMMA_MISSING); |
| } |
| else if (trailingArrayComma == TrailingArrayComma.NEVER |
| && comma != null && comma.getType() == TokenTypes.COMMA) { |
| log(comma.getLineNo(), |
| comma.getColumnNo(), MSG_KEY_ANNOTATION_TRAILING_COMMA_PRESENT); |
| } |
| } |
| |
| /** |
| * Checks to see if the closing parenthesis are present if required or |
| * prohibited. |
| * |
| * @param ast the annotation token |
| */ |
| private void checkCheckClosingParens(final DetailAST ast) { |
| if (closingParens != ClosingParens.IGNORE) { |
| final DetailAST paren = ast.getLastChild(); |
| final boolean parenExists = paren.getType() == TokenTypes.RPAREN; |
| |
| if (closingParens == ClosingParens.ALWAYS |
| && !parenExists) { |
| log(ast.getLineNo(), MSG_KEY_ANNOTATION_PARENS_MISSING); |
| } |
| else if (closingParens == ClosingParens.NEVER |
| && parenExists |
| && !ast.branchContains(TokenTypes.EXPR) |
| && !ast.branchContains(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR) |
| && !ast.branchContains(TokenTypes.ANNOTATION_ARRAY_INIT)) { |
| log(ast.getLineNo(), MSG_KEY_ANNOTATION_PARENS_PRESENT); |
| } |
| } |
| } |
| } |