ForbidAnnotationElementValueCheck.java
////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2019 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.github.sevntu.checkstyle.checks.annotation;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.common.base.CharMatcher;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.FullIdent;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
/**
* <p>
* Forbids specific
* <a href= 'https://docs.oracle.com/javase/specs/jls/se7/html/jls-9.html#jls-9.6.1'>element
* value</a> for specific annotation. You can configure this check using following options:
* </p>
* <ul>
* <li>Annotation name</li>
* <li>Annotation element name</li>
* <li>Forbidden annotation element value pattern</li>
* </ul>
* Example of usage:<br>
* <p>
* Here is XML configs and according code samples needed to forbid.
* </p>
* <p>To configure the check to forbid junit Test annotations with the element name "expected":</p>
* <p>
* Config
* </p>
*
* <pre>
* <module name="ForbidAnnotationElementValue">
* </module>
* </pre>
* <p>
* Code
* </p>
*
* <pre>
* @Test(expected = Exception.class)
* </pre>
* <p>
* To configure the check to forbid
* <a href= 'https://docs.oracle.com/javase/specs/jls/se7/html/jls-9.html#jls-9.7.3'>single-element
* </a> annotation element value, like 'SuppressWarnings', elementName option should be specified
* as "value".
* </p>
* <p>
* Config
* </p>
*
* <pre>
* <module name="ForbidAnnotationElementValue">
* <property name="annotationName" value="SuppressWarnings"/>
* <property name="elementName" value="value"/>
* <property name="forbiddenElementValueRegexp" value="unchecked"/>
* </module>
* </pre>
* <p>
* Code
* </p>
*
* <pre>
* @SuppressWarnings("unchecked")
* </pre>
* <p>
* To forbid any array-valued element, forbiddenElementValueRegexp option should be: "\{.*\}".
* </p>
* <p>
* Config
* </p>
*
* <pre>
* <module name="ForbidAnnotationElementValue">
* <property name="annotationName" value="SuppressWarnings"/>
* <property name="elementName" value="value"/>
* <property name="forbiddenElementValueRegexp" value="\{.*\}"/>
* </module>
* </pre>
*
* <p>
* Code
* </p>
*
* <pre>
* @SuppressWarnings({"unused", "unchecked"})
* </pre>
*
* @author <a href="mailto:drozzds@gmail.com"> Sergey Drozd </a>
* @author Richard Veach
* @since 1.22.0
*/
public class ForbidAnnotationElementValueCheck extends AbstractCheck {
/** Message key. */
public static final String MSG_KEY = "annotation.forbid.element.value";
/** CharMatcher using to trimming quotes from Strings. */
private static final CharMatcher QUOTE_MATCHER = CharMatcher.is('\"');
/** Default annotation element name when none specified. */
private static final String ELEMENT_NAME_DEFAULT = "value";
/** Forbidden annotation name. */
private String annotationName = "Test";
/** Forbidden annotation element name. */
private String elementName = "expected";
/** Precompiled forbidden element value pattern. */
private Pattern forbiddenElementValueRegexp = Pattern.compile(".*");
/**
* Sets Annotation Name Check property.
*
* @param annotationName The annotation name.
*/
public void setAnnotationName(String annotationName) {
this.annotationName = annotationName;
}
/**
* Sets Annotation Element Check property.
*
* @param elementName The annotation element name.
*/
public void setElementName(String elementName) {
this.elementName = elementName;
}
/**
* Sets Forbidden Element Value Pattern Check property.
*
* @param forbiddenElementValueRegexp
* The forbidden element value pattern to set.
*/
public void setForbiddenElementValueRegexp(String forbiddenElementValueRegexp) {
this.forbiddenElementValueRegexp = Pattern.compile(forbiddenElementValueRegexp);
}
@Override
public int[] getDefaultTokens() {
return new int[] {
TokenTypes.ANNOTATION,
};
}
@Override
public int[] getAcceptableTokens() {
return getDefaultTokens();
}
@Override
public int[] getRequiredTokens() {
return getDefaultTokens();
}
@Override
public void visitToken(DetailAST ast) {
if (getAnnotationName(ast).equals(annotationName)) {
if (ELEMENT_NAME_DEFAULT.equals(elementName) && isSingleElementAnnotation(ast)) {
final DetailAST forbiddenElement = getSingleElementWithForbiddenValue(ast);
log(forbiddenElement, MSG_KEY, elementName, annotationName);
}
else {
for (DetailAST forbiddenElement : getForbiddenElements(ast)) {
log(forbiddenElement, MSG_KEY, elementName, annotationName);
}
}
}
}
/**
* Determining that annotation is single-element.
*
* @param annotation
* DetailAST node of type {@link TokenTypes#ANNOTATION}
* @return True if the annotation is a single-element.
*/
private boolean isSingleElementAnnotation(DetailAST annotation) {
return getSingleElementWithForbiddenValue(annotation) != null;
}
/**
* Returns single element of specified annotation that matches forbidden element value.
*
* @param annotation
* DetailAST node of type {@link TokenTypes#ANNOTATION}
* @return DetailAST node of type {@link TokenTypes#EXPR}
*/
private DetailAST getSingleElementWithForbiddenValue(DetailAST annotation) {
DetailAST singleElement = null;
DetailAST currentNode = annotation.getFirstChild();
while (currentNode != null) {
if (currentNode.getType() == TokenTypes.EXPR
|| currentNode.getType() == TokenTypes.ANNOTATION_ARRAY_INIT) {
final String elementValue = getSingleElementValue(currentNode);
if (forbiddenElementValueRegexp.matcher(elementValue).find()) {
singleElement = currentNode;
break;
}
}
currentNode = currentNode.getNextSibling();
}
return singleElement;
}
/**
* Gets all forbidden children one level below on the current DetailAST parent node.
*
* @param annotation
* DetailAST node of type {@link TokenTypes#ANNOTATION}
* @return List of forbidden elements.
*/
private List<DetailAST> getForbiddenElements(DetailAST annotation) {
final List<DetailAST> forbiddenElements = new LinkedList<>();
DetailAST currentNode = annotation.getFirstChild();
while (currentNode != null) {
if (currentNode.getType() == TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR
&& isElementForbidden(currentNode)) {
forbiddenElements.add(currentNode);
}
currentNode = currentNode.getNextSibling();
}
return forbiddenElements;
}
/**
* Checks a member-value pair in AST tree for forbidden element.
*
* @param memberValuePair
* DetailAST node of type {@link TokenTypes#ANNOTATION_MEMBER_VALUE_PAIR}
* @return True if element is forbidden.
*/
private boolean isElementForbidden(DetailAST memberValuePair) {
final String elementValue = getElementValue(memberValuePair);
final Matcher elementValueMatcher = forbiddenElementValueRegexp.matcher(elementValue);
return getElementName(memberValuePair).equals(elementName) && elementValueMatcher.find();
}
/**
* Gets annotation element value as String from member-value pair node of syntax tree.
*
* @param memberValuePair
* DetailAST node of type {@link TokenTypes#ANNOTATION_MEMBER_VALUE_PAIR}
* @return String-represented element value
*/
private static String getElementValue(DetailAST memberValuePair) {
final String elementValue;
DetailAST elementValueAst = memberValuePair.findFirstToken(TokenTypes.EXPR);
if (elementValueAst == null) {
elementValueAst = memberValuePair.findFirstToken(TokenTypes.ANNOTATION_ARRAY_INIT);
elementValue = getListOfValuesAsText(elementValueAst);
}
else {
elementValue = getExpressionText(elementValueAst);
}
return elementValue;
}
/**
* Returns parameter value for single-element annotation.
*
* @param parameter
* DetailAST node of type {@link TokenTypes#ANNOTATION}
* @return String-represented parameter value
*/
private static String getSingleElementValue(DetailAST parameter) {
final String parameterValue;
if (parameter.getType() == TokenTypes.EXPR) {
parameterValue = getExpressionText(parameter);
}
else {
parameterValue = getListOfValuesAsText(parameter);
}
return parameterValue;
}
/**
* Returns expression text.
*
* @param expression
* DetailAST node of type {@link TokenTypes#EXPR}
* @return String-represented expression
*/
private static String getExpressionText(DetailAST expression) {
final DetailAST expressionValue = expression.getFirstChild();
final String elementValue;
if (expressionValue.getType() == TokenTypes.DOT) {
final FullIdent fullExpression = FullIdent.createFullIdent(expressionValue);
elementValue = fullExpression.getText();
}
else {
elementValue = expressionValue.getText();
}
return trimQuotes(elementValue);
}
/**
* Gets String-represented array from provided left brace.
*
* @param brace
* DetailAST node of type {@link TokenTypes#ANNOTATION_ARRAY_INIT}
* @return String-represented array. For example "{1,2,3,4}"
*/
private static String getListOfValuesAsText(DetailAST brace) {
String fullText = "{";
DetailAST currentNode = brace.getFirstChild();
while (currentNode != null) {
if (currentNode.getType() == TokenTypes.EXPR) {
fullText += currentNode.getFirstChild().getText();
}
else {
fullText += currentNode.getText();
}
currentNode = currentNode.getNextSibling();
}
return fullText;
}
/**
* Trims quotes from input string.
*
* @param input
* string with quotes
* @return quotes trimmed
*/
private static String trimQuotes(String input) {
return QUOTE_MATCHER.trimFrom(input);
}
/**
* Gets annotation name as String value from annotation node of syntax tree.
*
* @param annotation
* DetailAST node of type {@link TokenTypes#ANNOTATION}
* @return String-represented annotation name
*/
private static String getAnnotationName(DetailAST annotation) {
DetailAST annotationName = annotation.findFirstToken(TokenTypes.IDENT);
if (annotationName == null) {
// full classpath
annotationName = annotation.findFirstToken(TokenTypes.DOT).getLastChild();
}
return annotationName.getText();
}
/**
* Gets annotation element name as String value from member-value pair node of syntax tree.
*
* @param memberValuePair
* DetailAST node of type {@link TokenTypes#ANNOTATION_MEMBER_VALUE_PAIR}
* @return String-represented parameter name
*/
private static String getElementName(DetailAST memberValuePair) {
final DetailAST elementName = memberValuePair.findFirstToken(TokenTypes.IDENT);
return elementName.getText();
}
}