CheckstyleTestMakeupCheck.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.design;
- import java.io.File;
- import java.util.HashMap;
- import java.util.HashSet;
- import java.util.Map;
- import java.util.Set;
- import java.util.regex.Pattern;
- import com.github.sevntu.checkstyle.SevntuUtil;
- import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
- import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
- import com.puppycrawl.tools.checkstyle.api.Configuration;
- import com.puppycrawl.tools.checkstyle.api.DetailAST;
- import com.puppycrawl.tools.checkstyle.api.TokenTypes;
- import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
- import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
- /**
- * <p>
- * Custom check to ensure Checkstyle tests are designed correctly.
- * </p>
- *
- * <p>Rationale: This check was made to ensure tests follow a specific design implementation
- * so 3rd party utilities like the regression utility can parse the tests for information
- * used in creating regression reports.
- *
- * <p>
- * Check have following options:
- * </p>
- * <ul>
- * <li>
- * createMethodRegexp - Regular expression for matching a create configuration method by name. This
- * is the name of the method that starts creating a custom module configuration to be used for
- * verifying results for regression purposes.
- * Default value is {@code create(Root|Module)Config|getModuleConfig}.</li>
- *
- * <li>
- * verifyMethodRegexp - Regular expression for matching a verify method by name. This is the name
- * of the method that verifies the execution results of the custom configuration created for
- * regression. As such, it should accept the custom configuration as a parameter.
- * Default value is {@code verify(Warns|Suppressed)?}.</li>
- * </ul>
- *
- * <p>
- * To configure the check to report incorrectly made checkstyle tests:
- * </p>
- *
- * <pre>
- * <module name="CheckstyleTestMakeup"/>
- * </pre>
- *
- * @author Richard Veach
- * @since 1.25.0
- */
- public class CheckstyleTestMakeupCheck extends AbstractCheck {
- /** Violations message. */
- public static final String MSG_KEY_CONFIG_NOT_ASSIGNED = "tester.config.not.assigned";
- /** Violations message. */
- public static final String MSG_KEY_CONFIG_NOT_ASSIGNED_WITH = "tester.config.not.assigned.with";
- /** Violations message. */
- public static final String MSG_KEY_CONFIG_NOT_ASSIGNED_PROPERLY =
- "tester.config.not.assigned.properly";
- /** Violations message. */
- public static final String MSG_KEY_UNKNOWN_PROPERTY = "tester.unknown.property";
- /** Violations message. */
- public static final String MSG_KEY_CONFIG_NOT_FOUND = "tester.config.not.found";
- /** Name of 'getPath' method. */
- private static final String METHOD_GET_PATH = "getPath";
- /** AST of method that is currently being examined. */
- private DetailAST methodAst;
- /** List of variable names that reference a file. */
- private final Set<String> fileVariableNames = new HashSet<>();
- /** List of variable names that reference a configuration. */
- private final Set<String> checkConfigNames = new HashSet<>();
- /** {@code true} if the 'verify' method was found in the method. */
- private boolean foundVerify;
- /** List of violations generated for a method. */
- private final Map<DetailAST, String> violations = new HashMap<>();
- /** Regular expression for matching a create method by name. */
- private Pattern createMethodRegexp = Pattern
- .compile("create(Root|Module)Config|getModuleConfig");
- /** Regular expression for matching a verify method by name. */
- private Pattern verifyMethodRegexp = Pattern.compile("verify(Warns|Suppressed)?");
- /**
- * Setter for {@link #createMethodRegexp}.
- * @param createMethodRegexp The value to set.
- */
- public void setCreateMethodRegexp(Pattern createMethodRegexp) {
- this.createMethodRegexp = createMethodRegexp;
- }
- /**
- * Setter for {@link #verifyMethodRegexp}.
- * @param verifyMethodRegexp The value to set.
- */
- public void setVerifyMethodRegexp(Pattern verifyMethodRegexp) {
- this.verifyMethodRegexp = verifyMethodRegexp;
- }
- @Override
- public int[] getDefaultTokens() {
- return new int[] {
- TokenTypes.METHOD_DEF,
- TokenTypes.VARIABLE_DEF,
- TokenTypes.METHOD_CALL,
- };
- }
- @Override
- public int[] getAcceptableTokens() {
- return getDefaultTokens();
- }
- @Override
- public int[] getRequiredTokens() {
- return getDefaultTokens();
- }
- @Override
- public void beginTree(DetailAST rootAST) {
- resetInternalFields();
- }
- @Override
- public void visitToken(DetailAST ast) {
- switch (ast.getType()) {
- case TokenTypes.METHOD_DEF:
- checkMethod(ast);
- break;
- case TokenTypes.VARIABLE_DEF:
- checkVariable(ast);
- break;
- case TokenTypes.METHOD_CALL:
- checkMethodCall(ast);
- break;
- default:
- SevntuUtil.reportInvalidToken(ast.getType());
- break;
- }
- }
- /**
- * Examines the method to see if it is part of a Test.
- * @param ast The method to examine.
- */
- private void checkMethod(DetailAST ast) {
- if (methodAst == null && AnnotationUtil.containsAnnotation(ast, "Test")
- || AnnotationUtil.containsAnnotation(ast, "org.junit.Test")) {
- methodAst = ast;
- }
- }
- /**
- * Examines the variable declaration to see if it is a specific variable type to track.
- * Variables of type {@link Configuration} or {@link DefaultConfiguration} need to be assigned
- * a {@code null}, createModuleConfig, or createRootConfig and is tracked for future purposes.
- * Variables of type {@link File} with the modifier {@code final} are tracked for future
- * purposes.
- * @param ast The variable to examine.
- */
- private void checkVariable(DetailAST ast) {
- if (methodAst != null && ScopeUtil.isLocalVariableDef(ast)) {
- final DetailAST type = ast.findFirstToken(TokenTypes.TYPE).findFirstToken(
- TokenTypes.IDENT);
- if (type != null) {
- final String typeText = type.getText();
- if ("DefaultConfiguration".equals(typeText) || "Configuration".equals(typeText)) {
- checkConfigurationVariable(ast);
- }
- else if ("File".equals(typeText)
- && ast.findFirstToken(TokenTypes.MODIFIERS)
- .findFirstToken(TokenTypes.FINAL) != null) {
- fileVariableNames.add(ast.findFirstToken(TokenTypes.IDENT).getText());
- }
- }
- }
- }
- /**
- * Examines the configuration variable to see if it is defined as described in
- * {@link #checkVariable(DetailAST)}.
- * @param ast The variable to examine.
- */
- private void checkConfigurationVariable(DetailAST ast) {
- checkConfigNames.add(ast.findFirstToken(TokenTypes.IDENT).getText());
- final DetailAST assignment = ast.findFirstToken(TokenTypes.ASSIGN);
- if (assignment == null) {
- violations.put(ast, MSG_KEY_CONFIG_NOT_ASSIGNED);
- }
- else if (assignment.getFirstChild().getFirstChild().getType() == TokenTypes.METHOD_CALL) {
- final DetailAST assignmentMethod = assignment.getFirstChild()
- .getFirstChild().findFirstToken(TokenTypes.IDENT);
- if (assignmentMethod != null
- && !createMethodRegexp.matcher(assignmentMethod.getText()).matches()) {
- violations.put(assignment, MSG_KEY_CONFIG_NOT_ASSIGNED_WITH);
- }
- }
- else if (assignment.getFirstChild().getFirstChild().getType() != TokenTypes.LITERAL_NULL) {
- violations.put(ast, MSG_KEY_CONFIG_NOT_ASSIGNED_PROPERLY);
- }
- }
- /**
- * Examines the method call and verify it is defined correctly.
- * addAttribute method which is called by one of the configurations found earlier, must have
- * all it's parameters be acceptable to {@link #isValidMethodCallExpression(DetailAST)}.
- * Any method that matches {@link #verifyMethodRegexp} are tracked for future purposes.
- * @param ast The method call to examine.
- */
- private void checkMethodCall(DetailAST ast) {
- if (methodAst != null) {
- final DetailAST firstChild = ast.getFirstChild();
- final String methodCallName = getMethodCallName(firstChild);
- final String methodCallerName = getMethodCallerName(firstChild);
- if ("addAttribute".equals(methodCallName)
- && checkConfigNames.contains(methodCallerName)) {
- final DetailAST elist = ast.findFirstToken(TokenTypes.ELIST);
- for (DetailAST expression = elist.getFirstChild(); expression != null;
- expression = expression.getNextSibling()) {
- if (expression.getType() == TokenTypes.EXPR
- && !isValidMethodCallExpression(expression.getFirstChild())) {
- violations.put(expression, MSG_KEY_UNKNOWN_PROPERTY);
- }
- }
- }
- else if (methodCallerName.equals(methodCallName)
- && ast.getParent().getParent().getType() != TokenTypes.METHOD_CALL
- && verifyMethodRegexp.matcher(methodCallName).matches()) {
- foundVerify = true;
- }
- }
- }
- /**
- * Retrieves the name of the method being called.
- * @param ast The method call token to examine.
- * @return The name of the method.
- */
- private String getMethodCallName(DetailAST ast) {
- final String result;
- if (ast.getType() == TokenTypes.DOT) {
- result = getMethodCallName(ast.getFirstChild().getNextSibling());
- }
- else {
- result = ast.getText();
- }
- return result;
- }
- /**
- * Retrieves the name of the variable calling the method.
- * @param ast The method call token to examine.
- * @return The name of who is calling the method.
- */
- private String getMethodCallerName(DetailAST ast) {
- final String result;
- if (ast.getType() == TokenTypes.DOT) {
- result = getMethodCallName(ast.getFirstChild());
- }
- else {
- result = ast.getText();
- }
- return result;
- }
- /**
- * Identifies if the parameter of the method call is valid.
- * Plain string literal is allowed.
- * Adding multiple string literals is allowed because of line length limits.
- * Plain {@code null} is allowed due to backward compatibility.
- * Method calls are allowed only if they are any form of getPath, converting an enum to a
- * string, or retrieving the path of a final {@link File} variable.
- * @param expression The expression to examine.
- * @return {@code true} if the method call is defined correctly.
- */
- private boolean isValidMethodCallExpression(DetailAST expression) {
- boolean result = false;
- final DetailAST firstChild = expression.getFirstChild();
- switch (expression.getType()) {
- case TokenTypes.STRING_LITERAL:
- result = true;
- break;
- case TokenTypes.METHOD_CALL:
- result = isValidMethodCallExpressionMethodCall(firstChild);
- break;
- case TokenTypes.PLUS:
- result = isValidMethodCallExpression(firstChild)
- && isValidMethodCallExpression(firstChild.getNextSibling());
- break;
- case TokenTypes.LITERAL_NULL:
- result = true;
- break;
- default:
- break;
- }
- return result;
- }
- /**
- * Identifies if the inner method call of a method call is valid as defined in
- * {@link #isValidMethodCallExpression(DetailAST)}.
- * @param firstChild The first child of the method call.
- * @return {@code true} if the method call is defined correctly.
- */
- private boolean isValidMethodCallExpressionMethodCall(DetailAST firstChild) {
- boolean result = false;
- if (firstChild.getType() == TokenTypes.DOT) {
- if (firstChild.getFirstChild().getType() == TokenTypes.DOT) {
- result = isEnumerationCall(firstChild);
- }
- else if (isFileVariable(firstChild.getFirstChild())) {
- result = true;
- }
- }
- else {
- final String methodName = firstChild.getText();
- if (isMethodGetPath(methodName)) {
- result = true;
- }
- }
- return result;
- }
- /**
- * Checks if the method call is calling toString, getName, or name on an enumeration.
- * @param ast The AST to examine.
- * @return {@code true} if the method call is on a enumeration.
- */
- private static boolean isEnumerationCall(DetailAST ast) {
- boolean result = false;
- final DetailAST firstChild = ast.getFirstChild();
- final DetailAST methodCalled = firstChild.getNextSibling();
- final DetailAST parameters = ast.getNextSibling();
- if (firstChild.getFirstChild().getType() == TokenTypes.IDENT
- && ("toString".equals(methodCalled.getText())
- || "getName".equals(methodCalled.getText())
- || "name".equals(methodCalled.getText()))
- && parameters.getChildCount() == 0) {
- result = true;
- }
- return result;
- }
- /**
- * Checks if the method call is 'getPath' on a {@link File} variable.
- * @param firstChild The AST to examine.
- * @return {@code true} if the method call is on a file variable.
- */
- private boolean isFileVariable(DetailAST firstChild) {
- return METHOD_GET_PATH.equals(firstChild.getNextSibling().getText())
- && fileVariableNames.contains(firstChild.getText());
- }
- /**
- * Checks if the method name is a form of 'getPath'.
- * @param methodName The name to examine.
- * @return {@code true} if the method is of the form.
- */
- private static boolean isMethodGetPath(String methodName) {
- return METHOD_GET_PATH.equals(methodName)
- || "getNonCompilablePath".equals(methodName)
- || "getUriString".equals(methodName)
- || "getResourcePath".equals(methodName);
- }
- @Override
- public void leaveToken(DetailAST ast) {
- if (ast == methodAst) {
- if (foundVerify) {
- if (checkConfigNames.isEmpty()) {
- violations.put(ast, MSG_KEY_CONFIG_NOT_FOUND);
- }
- for (Map.Entry<DetailAST, String> entry : violations.entrySet()) {
- log(entry.getKey(), entry.getValue());
- }
- }
- resetInternalFields();
- }
- }
- /** Resets the internal fields when a new file/method is to be processed. */
- private void resetInternalFields() {
- methodAst = null;
- fileVariableNames.clear();
- checkConfigNames.clear();
- foundVerify = false;
- violations.clear();
- }
- }