blob: bf377ec23df28b77f4de84ca1cafb68b8e67350e [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
// 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 &amp; 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, &sect;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>
* &lt;module name=&quot;AnnotationUseStyle&quot;&gt;
* &lt;property name=&quot;ElementStyle&quot;
* value=&quot;EXPANDED&quot;/&gt;
* &lt;property name=&quot;TrailingArrayComma&quot;
* value=&quot;NEVER&quot;/&gt;
* &lt;property name=&quot;ClosingParens&quot;
* value=&quot;ALWAYS&quot;/&gt;
* &lt;/module&gt;
* </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);
}
}
}
}