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

org.sonar.python.checks.tests.ImplicitlySkippedTestCheck Maven / Gradle / Ivy

There is a newer version: 4.23.0.17664
Show newest version
/*
 * SonarQube Python Plugin
 * Copyright (C) 2011-2024 SonarSource SA
 * mailto:info AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
 *
 * This program 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 Sonar Source-Available License for more details.
 *
 * You should have received a copy of the Sonar Source-Available License
 * along with this program; if not, see https://sonarsource.com/license/ssal/
 */
package org.sonar.python.checks.tests;

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import org.sonar.check.Rule;
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
import org.sonar.plugins.python.api.tree.AssertStatement;
import org.sonar.plugins.python.api.tree.AssignmentStatement;
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
import org.sonar.plugins.python.api.tree.CallExpression;
import org.sonar.plugins.python.api.tree.Expression;
import org.sonar.plugins.python.api.tree.FunctionDef;
import org.sonar.plugins.python.api.tree.IfStatement;
import org.sonar.plugins.python.api.tree.Name;
import org.sonar.plugins.python.api.tree.QualifiedExpression;
import org.sonar.plugins.python.api.tree.ReturnStatement;
import org.sonar.plugins.python.api.tree.Statement;
import org.sonar.plugins.python.api.tree.Tree;
import org.sonar.python.tests.UnittestUtils;

@Rule(key = "S5918")
public class ImplicitlySkippedTestCheck extends PythonSubscriptionCheck {

  private static final String MESSAGE = "Skip this test explicitly.";
  private static final String MESSAGE_RETURN_SKIP = "Remove this return, it is not needed to skip the test.";

  private static final Tree.Kind[] literalsKind = {Tree.Kind.STRING_LITERAL, Tree.Kind.NUMERIC_LITERAL, Tree.Kind.LIST_LITERAL,
    Tree.Kind.BOOLEAN_LITERAL_PATTERN, Tree.Kind.NUMERIC_LITERAL_PATTERN, Tree.Kind.NONE_LITERAL_PATTERN, Tree.Kind.STRING_LITERAL_PATTERN};

  @Override
  public void initialize(Context context) {
    context.registerSyntaxNodeConsumer(Tree.Kind.FUNCDEF, ctx -> {
      FunctionDef functionDef = (FunctionDef) ctx.syntaxNode();

      if (!functionDef.name().name().startsWith("test")) {
        return;
      }

      ReturnStatement returnStatement = getReturnStatementFromFirstIfStatement(functionDef);
      if (returnStatement == null) {
        return;
      }

      if (hasAnySkipStatement(returnStatement.expressions())) {
        ctx.addIssue(returnStatement.returnKeyword(), MESSAGE_RETURN_SKIP);
        return;
      }

      // We add an issue only if there is an assert statement somewhere in the function definition
      if (containsAssertion(functionDef)) {
        ctx.addIssue(returnStatement, MESSAGE);
      }
    });
  }

  @Override
  public CheckScope scope() {
    return CheckScope.ALL;
  }

  private static ReturnStatement getReturnStatementFromFirstIfStatement(FunctionDef functionDef) {
    // check first non assignment statement is an if with a return
    for (Statement statement : functionDef.body().statements()) {
      // skip literal assignment expression
      if (statement.is(Tree.Kind.ASSIGNMENT_STMT) && ((AssignmentStatement) statement).assignedValue().is(literalsKind)) {
        continue;
      }

      if (statement.is(Tree.Kind.IF_STMT)) {
        IfStatement ifStatement = (IfStatement) statement;
        List statements = ifStatement.body().statements();
        if (statements.get(0).is(Tree.Kind.RETURN_STMT)) {
          return (ReturnStatement) statements.get(0);
        }
      }

      return null;
    }
    return null;
  }

  private static boolean hasAnySkipStatement(List expressions) {
    return expressions.stream()
      .filter(expression -> expression.is(Tree.Kind.CALL_EXPR))
      .map(CallExpression.class::cast)
      .filter(callExpr -> callExpr.callee().is(Tree.Kind.QUALIFIED_EXPR))
      .map(callExpr -> (QualifiedExpression) callExpr.callee())
      .anyMatch(callExpr -> isPytestSkip(callExpr) || isUnittestSkip(callExpr));
  }

  private static boolean isPytestSkip(QualifiedExpression qualifiedExpression) {
    return Optional.of(qualifiedExpression).stream()
      .map(qualifExpr -> qualifExpr.name().symbol())
      .filter(Objects::nonNull)
      .anyMatch(symbol -> "pytest.skip".equals(symbol.fullyQualifiedName()));
  }

  private static boolean isUnittestSkip(QualifiedExpression qualifiedExpression) {
    return Optional.of(qualifiedExpression).stream()
      .anyMatch(qualifExpr -> qualifExpr.qualifier().is(Tree.Kind.NAME) && "self".equals(((Name) qualifExpr.qualifier()).name())
        && "skipTest".equals(qualifExpr.name().name()));
  }

  private static boolean containsAssertion(FunctionDef functionDef) {
    Set supportedAssertMethods = UnittestUtils.isWithinUnittestTestCase(functionDef) ?
      UnittestUtils.allAssertMethods() : Collections.emptySet();
    AssertionVisitor assertVisitor = new AssertionVisitor(supportedAssertMethods);
    functionDef.accept(assertVisitor);
    return assertVisitor.hasAnAssert;
  }

  static class AssertionVisitor extends BaseTreeVisitor {
    boolean hasAnAssert = false;
    Set supportedMethods;

    public AssertionVisitor(Set supportedMethods) {
      this.supportedMethods = supportedMethods;
    }

    @Override
    public void visitAssertStatement(AssertStatement assertStatement) {
      hasAnAssert = true;
      super.visitAssertStatement(assertStatement);
    }

    @Override
    public void visitCallExpression(CallExpression callExpression) {
      if (callExpression.callee().is(Tree.Kind.QUALIFIED_EXPR)) {
        QualifiedExpression qualifiedExpression = (QualifiedExpression) callExpression.callee();
        if (qualifiedExpression.qualifier().is(Tree.Kind.NAME) && "self".equals(((Name) qualifiedExpression.qualifier()).name())
          && supportedMethods.contains(qualifiedExpression.name().name())) {
          hasAnAssert = true;
        }
      }
      super.visitCallExpression(callExpression);
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy