CheckstyleTestMakeupCheck.java

  1. ////////////////////////////////////////////////////////////////////////////////
  2. // checkstyle: Checks Java source code for adherence to a set of rules.
  3. // Copyright (C) 2001-2019 the original author or authors.
  4. //
  5. // This library is free software; you can redistribute it and/or
  6. // modify it under the terms of the GNU Lesser General Public
  7. // License as published by the Free Software Foundation; either
  8. // version 2.1 of the License, or (at your option) any later version.
  9. //
  10. // This library is distributed in the hope that it will be useful,
  11. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  13. // Lesser General Public License for more details.
  14. //
  15. // You should have received a copy of the GNU Lesser General Public
  16. // License along with this library; if not, write to the Free Software
  17. // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  18. ////////////////////////////////////////////////////////////////////////////////

  19. package com.github.sevntu.checkstyle.checks.design;

  20. import java.io.File;
  21. import java.util.HashMap;
  22. import java.util.HashSet;
  23. import java.util.Map;
  24. import java.util.Set;
  25. import java.util.regex.Pattern;

  26. import com.github.sevntu.checkstyle.SevntuUtil;
  27. import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
  28. import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
  29. import com.puppycrawl.tools.checkstyle.api.Configuration;
  30. import com.puppycrawl.tools.checkstyle.api.DetailAST;
  31. import com.puppycrawl.tools.checkstyle.api.TokenTypes;
  32. import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
  33. import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;

  34. /**
  35.  * <p>
  36.  * Custom check to ensure Checkstyle tests are designed correctly.
  37.  * </p>
  38.  *
  39.  * <p>Rationale: This check was made to ensure tests follow a specific design implementation
  40.  * so 3rd party utilities like the regression utility can parse the tests for information
  41.  * used in creating regression reports.
  42.  *
  43.  * <p>
  44.  * Check have following options:
  45.  * </p>
  46.  * <ul>
  47.  * <li>
  48.  * createMethodRegexp - Regular expression for matching a create configuration method by name. This
  49.  * is the name of the method that starts creating a custom module configuration to be used for
  50.  * verifying results for regression purposes.
  51.  * Default value is {@code create(Root|Module)Config|getModuleConfig}.</li>
  52.  *
  53.  * <li>
  54.  * verifyMethodRegexp - Regular expression for matching a verify method by name. This is the name
  55.  * of the method that verifies the execution results of the custom configuration created for
  56.  * regression. As such, it should accept the custom configuration as a parameter.
  57.  * Default value is {@code verify(Warns|Suppressed)?}.</li>
  58.  * </ul>
  59.  *
  60.  * <p>
  61.  * To configure the check to report incorrectly made checkstyle tests:
  62.  * </p>
  63.  *
  64.  * <pre>
  65.  * &lt;module name=&quot;CheckstyleTestMakeup&quot;/&gt;
  66.  * </pre>
  67.  *
  68.  * @author Richard Veach
  69.  * @since 1.25.0
  70.  */
  71. public class CheckstyleTestMakeupCheck extends AbstractCheck {

  72.     /** Violations message. */
  73.     public static final String MSG_KEY_CONFIG_NOT_ASSIGNED = "tester.config.not.assigned";
  74.     /** Violations message. */
  75.     public static final String MSG_KEY_CONFIG_NOT_ASSIGNED_WITH = "tester.config.not.assigned.with";
  76.     /** Violations message. */
  77.     public static final String MSG_KEY_CONFIG_NOT_ASSIGNED_PROPERLY =
  78.             "tester.config.not.assigned.properly";
  79.     /** Violations message. */
  80.     public static final String MSG_KEY_UNKNOWN_PROPERTY = "tester.unknown.property";
  81.     /** Violations message. */
  82.     public static final String MSG_KEY_CONFIG_NOT_FOUND = "tester.config.not.found";

  83.     /** Name of 'getPath' method. */
  84.     private static final String METHOD_GET_PATH = "getPath";

  85.     /** AST of method that is currently being examined. */
  86.     private DetailAST methodAst;
  87.     /** List of variable names that reference a file. */
  88.     private final Set<String> fileVariableNames = new HashSet<>();
  89.     /** List of variable names that reference a configuration. */
  90.     private final Set<String> checkConfigNames = new HashSet<>();
  91.     /** {@code true} if the 'verify' method was found in the method. */
  92.     private boolean foundVerify;

  93.     /** List of violations generated for a method. */
  94.     private final Map<DetailAST, String> violations = new HashMap<>();

  95.     /** Regular expression for matching a create method by name. */
  96.     private Pattern createMethodRegexp = Pattern
  97.             .compile("create(Root|Module)Config|getModuleConfig");

  98.     /** Regular expression for matching a verify method by name. */
  99.     private Pattern verifyMethodRegexp = Pattern.compile("verify(Warns|Suppressed)?");

  100.     /**
  101.      * Setter for {@link #createMethodRegexp}.
  102.      * @param createMethodRegexp The value to set.
  103.      */
  104.     public void setCreateMethodRegexp(Pattern createMethodRegexp) {
  105.         this.createMethodRegexp = createMethodRegexp;
  106.     }

  107.     /**
  108.      * Setter for {@link #verifyMethodRegexp}.
  109.      * @param verifyMethodRegexp The value to set.
  110.      */
  111.     public void setVerifyMethodRegexp(Pattern verifyMethodRegexp) {
  112.         this.verifyMethodRegexp = verifyMethodRegexp;
  113.     }

  114.     @Override
  115.     public int[] getDefaultTokens() {
  116.         return new int[] {
  117.             TokenTypes.METHOD_DEF,
  118.             TokenTypes.VARIABLE_DEF,
  119.             TokenTypes.METHOD_CALL,
  120.         };
  121.     }

  122.     @Override
  123.     public int[] getAcceptableTokens() {
  124.         return getDefaultTokens();
  125.     }

  126.     @Override
  127.     public int[] getRequiredTokens() {
  128.         return getDefaultTokens();
  129.     }

  130.     @Override
  131.     public void beginTree(DetailAST rootAST) {
  132.         resetInternalFields();
  133.     }

  134.     @Override
  135.     public void visitToken(DetailAST ast) {
  136.         switch (ast.getType()) {
  137.             case TokenTypes.METHOD_DEF:
  138.                 checkMethod(ast);
  139.                 break;
  140.             case TokenTypes.VARIABLE_DEF:
  141.                 checkVariable(ast);
  142.                 break;
  143.             case TokenTypes.METHOD_CALL:
  144.                 checkMethodCall(ast);
  145.                 break;
  146.             default:
  147.                 SevntuUtil.reportInvalidToken(ast.getType());
  148.                 break;
  149.         }
  150.     }

  151.     /**
  152.      * Examines the method to see if it is part of a Test.
  153.      * @param ast The method to examine.
  154.      */
  155.     private void checkMethod(DetailAST ast) {
  156.         if (methodAst == null && AnnotationUtil.containsAnnotation(ast, "Test")
  157.                 || AnnotationUtil.containsAnnotation(ast, "org.junit.Test")) {
  158.             methodAst = ast;
  159.         }
  160.     }

  161.     /**
  162.      * Examines the variable declaration to see if it is a specific variable type to track.
  163.      * Variables of type {@link Configuration} or {@link  DefaultConfiguration} need to be assigned
  164.      * a {@code null}, createModuleConfig, or createRootConfig and is tracked for future purposes.
  165.      * Variables of type {@link File} with the modifier {@code final} are tracked for future
  166.      * purposes.
  167.      * @param ast The variable to examine.
  168.      */
  169.     private void checkVariable(DetailAST ast) {
  170.         if (methodAst != null && ScopeUtil.isLocalVariableDef(ast)) {
  171.             final DetailAST type = ast.findFirstToken(TokenTypes.TYPE).findFirstToken(
  172.                     TokenTypes.IDENT);

  173.             if (type != null) {
  174.                 final String typeText = type.getText();

  175.                 if ("DefaultConfiguration".equals(typeText) || "Configuration".equals(typeText)) {
  176.                     checkConfigurationVariable(ast);
  177.                 }
  178.                 else if ("File".equals(typeText)
  179.                         && ast.findFirstToken(TokenTypes.MODIFIERS)
  180.                                 .findFirstToken(TokenTypes.FINAL) != null) {
  181.                     fileVariableNames.add(ast.findFirstToken(TokenTypes.IDENT).getText());
  182.                 }
  183.             }
  184.         }
  185.     }

  186.     /**
  187.      * Examines the configuration variable to see if it is defined as described in
  188.      * {@link #checkVariable(DetailAST)}.
  189.      * @param ast The variable to examine.
  190.      */
  191.     private void checkConfigurationVariable(DetailAST ast) {
  192.         checkConfigNames.add(ast.findFirstToken(TokenTypes.IDENT).getText());
  193.         final DetailAST assignment = ast.findFirstToken(TokenTypes.ASSIGN);

  194.         if (assignment == null) {
  195.             violations.put(ast, MSG_KEY_CONFIG_NOT_ASSIGNED);
  196.         }
  197.         else if (assignment.getFirstChild().getFirstChild().getType() == TokenTypes.METHOD_CALL) {
  198.             final DetailAST assignmentMethod = assignment.getFirstChild()
  199.                     .getFirstChild().findFirstToken(TokenTypes.IDENT);

  200.             if (assignmentMethod != null
  201.                     && !createMethodRegexp.matcher(assignmentMethod.getText()).matches()) {
  202.                 violations.put(assignment, MSG_KEY_CONFIG_NOT_ASSIGNED_WITH);
  203.             }
  204.         }
  205.         else if (assignment.getFirstChild().getFirstChild().getType() != TokenTypes.LITERAL_NULL) {
  206.             violations.put(ast, MSG_KEY_CONFIG_NOT_ASSIGNED_PROPERLY);
  207.         }
  208.     }

  209.     /**
  210.      * Examines the method call and verify it is defined correctly.
  211.      * addAttribute method which is called by one of the configurations found earlier, must have
  212.      * all it's parameters be acceptable to {@link #isValidMethodCallExpression(DetailAST)}.
  213.      * Any method that matches {@link #verifyMethodRegexp} are tracked for future purposes.
  214.      * @param ast The method call to examine.
  215.      */
  216.     private void checkMethodCall(DetailAST ast) {
  217.         if (methodAst != null) {
  218.             final DetailAST firstChild = ast.getFirstChild();
  219.             final String methodCallName = getMethodCallName(firstChild);
  220.             final String methodCallerName = getMethodCallerName(firstChild);

  221.             if ("addAttribute".equals(methodCallName)
  222.                     && checkConfigNames.contains(methodCallerName)) {
  223.                 final DetailAST elist = ast.findFirstToken(TokenTypes.ELIST);

  224.                 for (DetailAST expression = elist.getFirstChild(); expression != null;
  225.                         expression = expression.getNextSibling()) {
  226.                     if (expression.getType() == TokenTypes.EXPR
  227.                             && !isValidMethodCallExpression(expression.getFirstChild())) {
  228.                         violations.put(expression, MSG_KEY_UNKNOWN_PROPERTY);
  229.                     }
  230.                 }
  231.             }
  232.             else if (methodCallerName.equals(methodCallName)
  233.                     && ast.getParent().getParent().getType() != TokenTypes.METHOD_CALL
  234.                     && verifyMethodRegexp.matcher(methodCallName).matches()) {
  235.                 foundVerify = true;
  236.             }
  237.         }
  238.     }

  239.     /**
  240.      * Retrieves the name of the method being called.
  241.      * @param ast The method call token to examine.
  242.      * @return The name of the method.
  243.      */
  244.     private String getMethodCallName(DetailAST ast) {
  245.         final String result;
  246.         if (ast.getType() == TokenTypes.DOT) {
  247.             result = getMethodCallName(ast.getFirstChild().getNextSibling());
  248.         }
  249.         else {
  250.             result = ast.getText();
  251.         }
  252.         return result;
  253.     }

  254.     /**
  255.      * Retrieves the name of the variable calling the method.
  256.      * @param ast The method call token to examine.
  257.      * @return The name of who is calling the method.
  258.      */
  259.     private String getMethodCallerName(DetailAST ast) {
  260.         final String result;
  261.         if (ast.getType() == TokenTypes.DOT) {
  262.             result = getMethodCallName(ast.getFirstChild());
  263.         }
  264.         else {
  265.             result = ast.getText();
  266.         }
  267.         return result;
  268.     }

  269.     /**
  270.      * Identifies if the parameter of the method call is valid.
  271.      * Plain string literal is allowed.
  272.      * Adding multiple string literals is allowed because of line length limits.
  273.      * Plain {@code null} is allowed due to backward compatibility.
  274.      * Method calls are allowed only if they are any form of getPath, converting an enum to a
  275.      * string, or retrieving the path of a final {@link File} variable.
  276.      * @param expression The expression to examine.
  277.      * @return {@code true} if the method call is defined correctly.
  278.      */
  279.     private boolean isValidMethodCallExpression(DetailAST expression) {
  280.         boolean result = false;
  281.         final DetailAST firstChild = expression.getFirstChild();

  282.         switch (expression.getType()) {
  283.             case TokenTypes.STRING_LITERAL:
  284.                 result = true;
  285.                 break;
  286.             case TokenTypes.METHOD_CALL:
  287.                 result = isValidMethodCallExpressionMethodCall(firstChild);
  288.                 break;
  289.             case TokenTypes.PLUS:
  290.                 result = isValidMethodCallExpression(firstChild)
  291.                         && isValidMethodCallExpression(firstChild.getNextSibling());
  292.                 break;
  293.             case TokenTypes.LITERAL_NULL:
  294.                 result = true;
  295.                 break;
  296.             default:
  297.                 break;
  298.         }

  299.         return result;
  300.     }

  301.     /**
  302.      * Identifies if the inner method call of a method call is valid as defined in
  303.      * {@link #isValidMethodCallExpression(DetailAST)}.
  304.      * @param firstChild The first child of the method call.
  305.      * @return {@code true} if the method call is defined correctly.
  306.      */
  307.     private boolean isValidMethodCallExpressionMethodCall(DetailAST firstChild) {
  308.         boolean result = false;

  309.         if (firstChild.getType() == TokenTypes.DOT) {
  310.             if (firstChild.getFirstChild().getType() == TokenTypes.DOT) {
  311.                 result = isEnumerationCall(firstChild);
  312.             }
  313.             else if (isFileVariable(firstChild.getFirstChild())) {
  314.                 result = true;
  315.             }
  316.         }
  317.         else {
  318.             final String methodName = firstChild.getText();

  319.             if (isMethodGetPath(methodName)) {
  320.                 result = true;
  321.             }
  322.         }

  323.         return result;
  324.     }

  325.     /**
  326.      * Checks if the method call is calling toString, getName, or name on an enumeration.
  327.      * @param ast The AST to examine.
  328.      * @return {@code true} if the method call is on a enumeration.
  329.      */
  330.     private static boolean isEnumerationCall(DetailAST ast) {
  331.         boolean result = false;
  332.         final DetailAST firstChild = ast.getFirstChild();
  333.         final DetailAST methodCalled = firstChild.getNextSibling();
  334.         final DetailAST parameters = ast.getNextSibling();

  335.         if (firstChild.getFirstChild().getType() == TokenTypes.IDENT
  336.                 && ("toString".equals(methodCalled.getText())
  337.                         || "getName".equals(methodCalled.getText())
  338.                         || "name".equals(methodCalled.getText()))
  339.                 && parameters.getChildCount() == 0) {
  340.             result = true;
  341.         }

  342.         return result;
  343.     }

  344.     /**
  345.      * Checks if the method call is 'getPath' on a {@link File} variable.
  346.      * @param firstChild The AST to examine.
  347.      * @return {@code true} if the method call is on a file variable.
  348.      */
  349.     private boolean isFileVariable(DetailAST firstChild) {
  350.         return METHOD_GET_PATH.equals(firstChild.getNextSibling().getText())
  351.                 && fileVariableNames.contains(firstChild.getText());
  352.     }

  353.     /**
  354.      * Checks if the method name is a form of 'getPath'.
  355.      * @param methodName The name to examine.
  356.      * @return {@code true} if the method is of the form.
  357.      */
  358.     private static boolean isMethodGetPath(String methodName) {
  359.         return METHOD_GET_PATH.equals(methodName)
  360.                 || "getNonCompilablePath".equals(methodName)
  361.                 || "getUriString".equals(methodName)
  362.                 || "getResourcePath".equals(methodName);
  363.     }

  364.     @Override
  365.     public void leaveToken(DetailAST ast) {
  366.         if (ast == methodAst) {
  367.             if (foundVerify) {
  368.                 if (checkConfigNames.isEmpty()) {
  369.                     violations.put(ast, MSG_KEY_CONFIG_NOT_FOUND);
  370.                 }

  371.                 for (Map.Entry<DetailAST, String> entry : violations.entrySet()) {
  372.                     log(entry.getKey(), entry.getValue());
  373.                 }
  374.             }

  375.             resetInternalFields();
  376.         }
  377.     }

  378.     /** Resets the internal fields when a new file/method is to be processed. */
  379.     private void resetInternalFields() {
  380.         methodAst = null;
  381.         fileVariableNames.clear();
  382.         checkConfigNames.clear();
  383.         foundVerify = false;
  384.         violations.clear();
  385.     }

  386. }