All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.codenarc.rule.junit.SpockMissingAssertRule.groovy Maven / Gradle / Ivy

There is a newer version: 3.5.0-groovy-4.0
Show newest version
/*
 * Copyright 2023 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.codenarc.rule.junit

import org.codehaus.groovy.ast.MethodNode
import org.codehaus.groovy.ast.expr.BinaryExpression
import org.codehaus.groovy.ast.expr.ConstantExpression
import org.codehaus.groovy.ast.expr.DeclarationExpression
import org.codehaus.groovy.ast.expr.MethodCallExpression
import org.codehaus.groovy.ast.expr.VariableExpression
import org.codehaus.groovy.ast.stmt.AssertStatement
import org.codehaus.groovy.ast.stmt.BlockStatement
import org.codehaus.groovy.ast.stmt.DoWhileStatement
import org.codehaus.groovy.ast.stmt.ExpressionStatement
import org.codehaus.groovy.ast.stmt.ForStatement
import org.codehaus.groovy.ast.stmt.IfStatement
import org.codehaus.groovy.ast.stmt.Statement
import org.codehaus.groovy.ast.stmt.SwitchStatement
import org.codehaus.groovy.ast.stmt.TryCatchStatement
import org.codehaus.groovy.ast.stmt.WhileStatement

import org.codenarc.rule.AbstractAstVisitor
import org.codenarc.rule.AbstractAstVisitorRule
import org.codenarc.util.WildcardPattern

import java.util.regex.Pattern

/**
 * Spock treats all expressions on the first level of a then or expect block as an implicit assertion. However,
 * everything inside if/for/switch/... blocks is not an implicit assert, just a useless comparison (unless wrapped by a `with` or `verifyAll`).
 *Ø
 * This rule finds such expressions, where an explicit call to assert would be required. Please note that the rule might
 * produce false positives, as it relies on method names to determine whether an expression has a boolean type or not.
 *
 * @author Jean André Gauthier
 * @author Daniel Clausen
  */
class SpockMissingAssertRule extends AbstractAstVisitorRule {

    String name = 'SpockMissingAssert'
    int priority = 3
    String specificationSuperclassNames = '*Specification'
    String specificationClassNames = null
    Class astVisitorClass = SpockMissingAssertAstVisitor
}

class SpockMissingAssertAstVisitor extends AbstractAstVisitor {

    // Intentionally omitting and, as it doesn't have any semantic impact
    private static final List SPOCK_LABELS = ['given', 'when', 'then', 'expect', 'where', 'cleanup', 'setup']

    private static final List LABELS_WITH_IMPLICIT_ASSERTIONS = ['then', 'expect']

    private static final List METHODS_WITH_IMPLICIT_ASSERTIONS = ['with', 'verifyAll']

    private static final List METHODS_FOR_COLLECTION_ITERATION = ['each', 'eachWithIndex', 'times']

    private static final List BOOLEAN_METHOD_PATTERNS = [~/^is(\p{Lu}.*)?/, ~/^has(\p{Lu}.*)?/, ~/^asBoolean$/, ~/^any(\p{Lu}.*)?/, ~/^contains(\p{Lu}.*)?/, ~/^every(\p{Lu}.*)?/, ~/^equals(\p{Lu}.*)?/]

    private static final List RELATIONAL_OPERATORS = ['==', '!=', '<', '<=', '>', '>=', '===', '!==']

    private static final List LOGICAL_OPERATORS = ['&&', '||']

    private static final List REGEX_OPERATORS = ['==~']

    private static final List INSTANCEOF_OPERATORS = ['instanceof']

    private static final List MEMBERSHIP_OPERATORS = ['in']

    private String currentLabel = null

    private int nNestedStatements = 0

    @Override
    void visitDoWhileLoop(DoWhileStatement statement) {
        visitIfInSpockClass {
            updateCurrentLabel(statement)
            handleNestedStatement {
                super.visitDoWhileLoop(statement)
            }
        }
    }

    @Override
    void visitForLoop(ForStatement statement) {
        visitIfInSpockClass {
            updateCurrentLabel(statement)
            handleNestedStatement {
                super.visitForLoop(statement)
            }
        }
    }

    @Override
    void visitIfElse(IfStatement statement) {
        visitIfInSpockClass {
            updateCurrentLabel(statement)
            handleNestedStatement {
                super.visitIfElse(statement)
            }
        }
    }

    @Override
    void visitSwitch(SwitchStatement statement) {
        visitIfInSpockClass {
            updateCurrentLabel(statement)
            handleNestedStatement {
                super.visitSwitch(statement)
            }
        }
    }

    @Override
    void visitTryCatchFinally(TryCatchStatement statement) {
        visitIfInSpockClass {
            updateCurrentLabel(statement)
            handleNestedStatement {
                super.visitTryCatchFinally(statement)
            }
        }
    }

    @Override
    void visitWhileLoop(WhileStatement statement) {
        visitIfInSpockClass {
            updateCurrentLabel(statement)
            handleNestedStatement {
                super.visitWhileLoop(statement)
            }
        }
    }

    @Override
    void visitConstructorOrMethod(MethodNode node, boolean isConstructor) {
        visitIfInSpockClass {
            resetCurrentLabel()
            // Do not inspect fixture / helper methods
            if (isSpockFeatureMethod(node)) {
                super.visitConstructorOrMethod(node, isConstructor)
            }
        }
    }

    private void resetCurrentLabel() {
        currentLabel = null
    }

    private static boolean isSpockFeatureMethod(MethodNode node) {
        if (node.code instanceof BlockStatement) {
            BlockStatement block = (BlockStatement) node.code
            // To be considered as a feature method by Spock, the method must have at least one statement label.
            // More details can be found in org.spockframework.compiler.SpecParser.isFeatureMethod() at
            // https://github.com/spockframework/spock/blob/52e7688b3f89533857006539e5905c9b4121f32b/spock-core/src/main/java/org/spockframework/compiler/SpecParser.java#LL153C5-L153C5
            return block.statements.any(s -> s.statementLabels != null && !s.statementLabels.intersect(SPOCK_LABELS).isEmpty())
        }
        return false
    }

    @Override
    void visitDeclarationExpression(DeclarationExpression expression) {
        // Do not inspect declaration expressions
    }

    @Override
    void visitAssertStatement(AssertStatement statement) {
        // Do not inspect assert expressions
    }

    @Override
    void visitExpressionStatement(ExpressionStatement statement) {
        visitIfInSpockClass {
            updateCurrentLabel(statement)
            // Do not inspect content in with/verifyAll methods
            if (isMethodsWithImplicitAssertionsExpression(statement)) {
                return
            }
            boolean isInLabelWithImplicitAssertions = currentLabel in LABELS_WITH_IMPLICIT_ASSERTIONS
            boolean isInTopLevel = nNestedStatements == 0
            boolean isBoolean = isBooleanExpression(statement)
            if (isInLabelWithImplicitAssertions && !isInTopLevel && isBoolean) {
                addViolation(statement, "'${currentLabel}:' might contain a boolean expression in a nested statement, which is not implicitly asserted")
            }
            visitCollectionIterationMethods(statement)
        }
    }

    private static boolean isBooleanExpression(ExpressionStatement statement) {
        // Handles literals & casts / coercion operators
        if (statement.expression.type.name == 'boolean' || statement.expression.type.name == 'Boolean') {
            return true
        }
        // Handles binary expressions
        if (statement.expression instanceof BinaryExpression) {
            BinaryExpression binaryExpression = statement.expression as BinaryExpression
            if (binaryExpression.operation.text in RELATIONAL_OPERATORS
                || binaryExpression.operation.text in LOGICAL_OPERATORS
                || binaryExpression.operation.text in REGEX_OPERATORS
                || binaryExpression.operation.text in INSTANCEOF_OPERATORS
                || binaryExpression.operation.text in MEMBERSHIP_OPERATORS
            ) {
                return true
            }
        }
        var variableAndMethod = getVariableAndMethod(statement)
        var method = variableAndMethod.v2
        // Heuristic: assume that methods whose name matches BOOLEAN_METHOD_PATTERNS return a boolean
        return method != null && BOOLEAN_METHOD_PATTERNS.any { it -> method.value.toString().matches(it) }
    }

    private static boolean isMethodsWithImplicitAssertionsExpression(ExpressionStatement statement) {
        var variableAndMethod = getVariableAndMethod(statement)
        var variable = variableAndMethod.v1
        var method = variableAndMethod.v2
        // To keep things simple, we only consider methods called on this
        return variable != null && variable.name == 'this' && method != null && METHODS_WITH_IMPLICIT_ASSERTIONS.contains(method.value)
    }

    private void visitCollectionIterationMethods(ExpressionStatement statement) {
        // Inspect the arguments from collection iteration methods (i.e loop equivalents)
        if (isCollectionIterationMethods(statement) && statement.expression instanceof MethodCallExpression) {
            MethodCallExpression methodCallExpression = statement.expression as MethodCallExpression
            methodCallExpression.arguments.visit(this)
        }
    }

    private static boolean isCollectionIterationMethods(ExpressionStatement statement) {
        var variableAndMethod = getVariableAndMethod(statement)
        var method = variableAndMethod.v2
        // Heuristic: assume that methods whose name matches METHODS_FOR_COLLECTION_ITERATION are equivalent to loops
        return method != null && METHODS_FOR_COLLECTION_ITERATION.contains(method.value)
    }

    private static Tuple2 getVariableAndMethod(ExpressionStatement statement) {
        var variable = null
        var method = null
        if (statement.expression instanceof MethodCallExpression) {
            MethodCallExpression methodCall = statement.expression as MethodCallExpression
            if (methodCall.objectExpression instanceof VariableExpression) {
                variable = methodCall.objectExpression as VariableExpression
            }
            if (methodCall.method instanceof ConstantExpression) {
                method = methodCall.method as ConstantExpression
            }
        }
        return new Tuple2<>(variable, method)
    }

    private void updateCurrentLabel(Statement statement) {
        // Spock only treats top-level labels as blocks
        if (nNestedStatements == 0) {
            List labels = statement.statementLabels
            if (labels != null) {
                Collection spockLabels = labels.intersect(SPOCK_LABELS)
                if (spockLabels.size() > 0) {
                    currentLabel = spockLabels.last()
                }
            }
        }
        super.visitStatement(statement)
    }

    private void handleNestedStatement(Closure callVisitorMethod) {
        nNestedStatements++
        callVisitorMethod()
        nNestedStatements--
    }

    private void visitIfInSpockClass(Closure callVisitorMethod) {
        def superClassPattern = new WildcardPattern(rule.specificationSuperclassNames)
        def classNamePattern = new WildcardPattern(rule.specificationClassNames, false)
        if (superClassPattern.matches(currentClassNode.superClass.name) || classNamePattern.matches(currentClassNode.name)) {
            callVisitorMethod()
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy