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

org.sonar.python.checks.UselessStatementCheck 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;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import org.sonar.check.Rule;
import org.sonar.check.RuleProperty;
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
import org.sonar.plugins.python.api.SubscriptionContext;
import org.sonar.plugins.python.api.symbols.ClassSymbol;
import org.sonar.plugins.python.api.symbols.FunctionSymbol;
import org.sonar.plugins.python.api.symbols.Symbol;
import org.sonar.plugins.python.api.symbols.Usage;
import org.sonar.plugins.python.api.tree.BinaryExpression;
import org.sonar.plugins.python.api.tree.CallExpression;
import org.sonar.plugins.python.api.tree.ClassDef;
import org.sonar.plugins.python.api.tree.ConditionalExpression;
import org.sonar.plugins.python.api.tree.Expression;
import org.sonar.plugins.python.api.tree.FileInput;
import org.sonar.plugins.python.api.tree.FunctionDef;
import org.sonar.plugins.python.api.tree.Name;
import org.sonar.plugins.python.api.tree.QualifiedExpression;
import org.sonar.plugins.python.api.tree.StatementList;
import org.sonar.plugins.python.api.tree.StringLiteral;
import org.sonar.plugins.python.api.tree.Token;
import org.sonar.plugins.python.api.tree.Tree;
import org.sonar.plugins.python.api.tree.Tree.Kind;
import org.sonar.plugins.python.api.tree.UnaryExpression;
import org.sonar.plugins.python.api.tree.WithItem;
import org.sonar.plugins.python.api.tree.WithStatement;
import org.sonar.python.tree.TreeUtils;

@Rule(key = "S905")
public class UselessStatementCheck extends PythonSubscriptionCheck {

  private static final boolean DEFAULT_REPORT_ON_STRINGS = false;
  private static final String DEFAULT_IGNORED_OPERATORS = "<<,>>,|";

  @RuleProperty(
    key = "reportOnStrings",
    description = "Enable issues on string literals which are not assigned. Set this parameter to \"false\" if you use strings as comments.",
    defaultValue = "" + DEFAULT_REPORT_ON_STRINGS)
  public boolean reportOnStrings = DEFAULT_REPORT_ON_STRINGS;

  @RuleProperty(
    key = "ignoredOperators",
    description = "Comma separated list of ignored operators",
    defaultValue = DEFAULT_IGNORED_OPERATORS)
  public String ignoredOperators = DEFAULT_IGNORED_OPERATORS;

  List ignoredOperatorsList;

  private List ignoredOperators() {
    if (ignoredOperatorsList == null) {
      ignoredOperatorsList = Stream.of(ignoredOperators.split(","))
        .map(String::trim).toList();
    }
    return ignoredOperatorsList;
  }

  private static final List regularKinds = Arrays.asList(Kind.NUMERIC_LITERAL, Kind.LIST_LITERAL, Kind.SET_LITERAL, Kind.DICTIONARY_LITERAL,
    Kind.NONE, Kind.LAMBDA);

  private static final List binaryExpressionKinds = Arrays.asList(Kind.AND, Kind.OR, Kind.PLUS, Kind.MINUS,
    Kind.MULTIPLICATION, Kind.DIVISION, Kind.FLOOR_DIVISION, Kind.MODULO, Kind.MATRIX_MULTIPLICATION, Kind.SHIFT_EXPR,
    Kind.BITWISE_AND, Kind.BITWISE_OR, Kind.BITWISE_XOR, Kind.COMPARISON, Kind.POWER);

  private static final List unaryExpressionKinds = Arrays.asList(Kind.UNARY_PLUS, Kind.UNARY_MINUS, Kind.BITWISE_COMPLEMENT, Kind.NOT);

  private static final Set ignoredContexts = new HashSet<>(List.of("contextlib.suppress"));

  private static final String MESSAGE = "Remove or refactor this statement; it has no side effects.";

  @Override
  public void initialize(Context context) {
    context.registerSyntaxNodeConsumer(Kind.STRING_LITERAL, this::checkStringLiteral);
    context.registerSyntaxNodeConsumer(Kind.NAME, UselessStatementCheck::checkName);
    context.registerSyntaxNodeConsumer(Kind.QUALIFIED_EXPR, UselessStatementCheck::checkQualifiedExpression);
    context.registerSyntaxNodeConsumer(Kind.CONDITIONAL_EXPR, UselessStatementCheck::checkConditionalExpression);
    binaryExpressionKinds.forEach(b -> context.registerSyntaxNodeConsumer(b, this::checkBinaryExpression));
    unaryExpressionKinds.forEach(u -> context.registerSyntaxNodeConsumer(u, this::checkUnaryExpression));
    regularKinds.forEach(r -> context.registerSyntaxNodeConsumer(r, UselessStatementCheck::checkNode));
  }

  private static void checkNode(SubscriptionContext ctx) {
    if ("__manifest__.py".equals(ctx.pythonFile().fileName())) {
      return;
    }
    Tree tree = ctx.syntaxNode();
    Tree tryParent = TreeUtils.firstAncestorOfKind(tree, Kind.TRY_STMT);
    if (tryParent != null) {
      return;
    }
    if (isBooleanExpressionWithCalls(tree)) {
      return;
    }
    Tree parent = tree.parent();
    if (parent == null || !parent.is(Kind.EXPRESSION_STMT)) {
      return;
    }
    if (isWithinIgnoredContext(tree)) {
      return;
    }
    // Safe cast because the rule only subscribes to expressions
    if (isAnAirflowException((Expression) tree)) {
      return;
    }
    ctx.addIssue(tree, MESSAGE);
  }

  private static boolean isAnAirflowException(Expression expression) {
    if (isWithinAirflowContext(expression)) {
      StatementList statementList = (StatementList) TreeUtils.firstAncestorOfKind(expression, Kind.STATEMENT_LIST);
      return Optional.ofNullable(statementList).map(StatementList::statements).map(statements -> statements.get(statements.size() - 1))
        .filter(lastStatement -> lastStatement.equals(TreeUtils.firstAncestorOfKind(expression, Kind.EXPRESSION_STMT))).isPresent();
    }
    return false;
  }

  private static boolean isWithinIgnoredContext(Tree tree) {
    Tree withParent = TreeUtils.firstAncestorOfKind(tree, Kind.WITH_STMT);
    if (withParent != null) {
      WithStatement withStatement = (WithStatement) withParent;
      return withStatement.withItems().stream()
        .map(WithItem::test)
        .filter(item -> item.is(Kind.CALL_EXPR))
        .map(item -> ((CallExpression) item).calleeSymbol())
        .filter(Objects::nonNull)
        .anyMatch(s -> ignoredContexts.contains(s.fullyQualifiedName()));
    }
    return false;
  }

  private static boolean isWithinAirflowContext(Tree tree) {
    Tree withParent = TreeUtils.firstAncestorOfKind(tree, Kind.WITH_STMT);
    while (withParent != null) {
      WithStatement withStatement = (WithStatement) withParent;
      if (withStatement.withItems().stream()
        .map(WithItem::test)
        .filter(item -> item.is(Kind.CALL_EXPR))
        .map(item -> ((CallExpression) item).calleeSymbol())
        .filter(Objects::nonNull)
        .anyMatch(s -> "airflow.DAG".equals(s.fullyQualifiedName()))) {
        return true;
      }
      withParent = TreeUtils.firstAncestorOfKind(withParent, Kind.WITH_STMT);
    }
    FunctionDef funcParent = (FunctionDef) TreeUtils.firstAncestorOfKind(tree, Kind.FUNCDEF);
    return funcParent != null && funcParent.decorators().stream().map(deco -> TreeUtils.getSymbolFromTree(deco.expression())).filter(Optional::isPresent)
      .anyMatch(symbol -> "airflow.decorators.dag".equals(symbol.get().fullyQualifiedName()));
  }

  private static boolean isBooleanExpressionWithCalls(Tree tree) {
    return (tree.is(Kind.AND) || tree.is(Kind.OR) || tree.is(Kind.NOT)) && (TreeUtils.hasDescendant(tree, t -> t.is(Kind.CALL_EXPR)));
  }

  public static void checkConditionalExpression(SubscriptionContext ctx) {
    ConditionalExpression conditionalExpression = (ConditionalExpression) ctx.syntaxNode();
    if (TreeUtils.hasDescendant(conditionalExpression, t -> t.is(Kind.CALL_EXPR))) {
      return;
    }
    checkNode(ctx);
  }

  private void checkStringLiteral(SubscriptionContext ctx) {
    StringLiteral stringLiteral = (StringLiteral) ctx.syntaxNode();
    if (!reportOnStrings || isDocString(stringLiteral)) {
      return;
    }
    checkNode(ctx);
  }

  private static void checkName(SubscriptionContext ctx) {
    Name name = (Name) ctx.syntaxNode();
    Symbol symbol = name.symbol();
    if (symbol != null && symbol.is(Symbol.Kind.CLASS)) {
      ClassSymbol classSymbol = (ClassSymbol) symbol;
      // Creating an exception without raising it is covered by S3984
      if (classSymbol.canBeOrExtend("BaseException")) {
        return;
      }
    }
    if (symbol != null && symbol.usages().stream().anyMatch(u -> u.kind().equals(Usage.Kind.IMPORT)) && symbol.usages().size() == 2) {
      // Avoid raising on useless statements made to suppress issues due to "unused" import
      return;
    }
    checkNode(ctx);
  }

  private static void checkQualifiedExpression(SubscriptionContext ctx) {
    QualifiedExpression qualifiedExpression = (QualifiedExpression) ctx.syntaxNode();
    Symbol symbol = qualifiedExpression.symbol();
    if (symbol != null && symbol.is(Symbol.Kind.FUNCTION) && ((FunctionSymbol) symbol).decorators().stream().noneMatch(d -> d.matches("property"))) {
      checkNode(ctx);
    }
  }

  private void checkBinaryExpression(SubscriptionContext ctx) {
    BinaryExpression binaryExpression = (BinaryExpression) ctx.syntaxNode();
    Token operator = binaryExpression.operator();
    if (ignoredOperators().contains(operator.value())) {
      return;
    }
    if (couldBePython2PrintStatement(binaryExpression)) {
      return;
    }
    checkNode(ctx);
  }

  private static boolean couldBePython2PrintStatement(BinaryExpression binaryExpression) {
    return TreeUtils.hasDescendant(binaryExpression, t -> t.is(Kind.CALL_EXPR)
      && ((CallExpression) t).callee().is(Kind.NAME)
      && ((Name) ((CallExpression) t).callee()).name().equals("print"));
  }

  private void checkUnaryExpression(SubscriptionContext ctx) {
    UnaryExpression unaryExpression = (UnaryExpression) ctx.syntaxNode();
    Token operator = unaryExpression.operator();
    if (ignoredOperators().contains(operator.value())) {
      return;
    }
    checkNode(ctx);
  }

  private static boolean isDocString(StringLiteral stringLiteral) {
    Tree parent = TreeUtils.firstAncestorOfKind(stringLiteral, Kind.FILE_INPUT, Kind.CLASSDEF, Kind.FUNCDEF);
    return Optional.ofNullable(parent)
      .map(p -> ((p.is(Kind.FILE_INPUT) && stringLiteral.equals(((FileInput) p).docstring()))
        || (p.is(Kind.CLASSDEF) && stringLiteral.equals(((ClassDef) p).docstring()))
        || (p.is(Kind.FUNCDEF) && stringLiteral.equals(((FunctionDef) p).docstring()))))
      .orElse(false);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy