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

org.sonar.python.checks.InfiniteRecursionCheck 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.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.check.Rule;
import org.sonar.plugins.python.api.PythonFile;
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
import org.sonar.plugins.python.api.cfg.CfgBlock;
import org.sonar.plugins.python.api.cfg.ControlFlowGraph;
import org.sonar.plugins.python.api.symbols.Symbol;
import org.sonar.plugins.python.api.symbols.Usage;
import org.sonar.plugins.python.api.tree.AssignmentStatement;
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
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.ComprehensionExpression;
import org.sonar.plugins.python.api.tree.ComprehensionIf;
import org.sonar.plugins.python.api.tree.ConditionalExpression;
import org.sonar.plugins.python.api.tree.DictCompExpression;
import org.sonar.plugins.python.api.tree.Expression;
import org.sonar.plugins.python.api.tree.ExpressionList;
import org.sonar.plugins.python.api.tree.FunctionDef;
import org.sonar.plugins.python.api.tree.LambdaExpression;
import org.sonar.plugins.python.api.tree.Name;
import org.sonar.plugins.python.api.tree.QualifiedExpression;
import org.sonar.plugins.python.api.tree.Tree;
import org.sonar.python.api.PythonKeyword;
import org.sonar.python.checks.utils.CheckUtils;
import org.sonar.python.tree.TreeUtils;

@Rule(key = "S2190")
public class InfiniteRecursionCheck extends PythonSubscriptionCheck {

  private static final String MESSAGE = "Add a way to break out of this %s's recursion.";

  @Override
  public void initialize(Context context) {
    context.registerSyntaxNodeConsumer(Tree.Kind.FUNCDEF, ctx -> {
      FunctionDef functionDef = (FunctionDef) ctx.syntaxNode();
      List allRecursiveCalls = new ArrayList<>();
      boolean endBlockIsReachable = collectRecursiveCallsAndCheckIfEndBlockIsReachable(functionDef, ctx.pythonFile(), allRecursiveCalls);
      if (!allRecursiveCalls.isEmpty() && !endBlockIsReachable) {
        String message = String.format(MESSAGE, functionDef.isMethodDefinition() ? "method" : "function");
        PreciseIssue issue = ctx.addIssue(functionDef.name(), message);
        allRecursiveCalls.forEach(call -> issue.secondary(call, "recursive call"));
      }
    });
  }

  private static boolean collectRecursiveCallsAndCheckIfEndBlockIsReachable(FunctionDef functionDef, PythonFile pythonFile, List allRecursiveCalls) {
    Symbol functionSymbol = functionDef.name().symbol();
    if (functionSymbol == null) {
      return true;
    }
    ControlFlowGraph cfg = ControlFlowGraph.build(functionDef, pythonFile);
    if (cfg == null) {
      return true;
    }
    RecursiveCallCollector recursiveCallCollector = new RecursiveCallCollector(functionDef, functionSymbol);
    Set pushedBlocks = new HashSet<>();
    Deque blockToVisit = new ArrayDeque<>();
    blockToVisit.addLast(cfg.start());
    pushedBlocks.add(cfg.start());
    while (!blockToVisit.isEmpty()) {
      CfgBlock block = blockToVisit.removeFirst();
      if (block == cfg.end()) {
        return true;
      }
      List blockRecursiveCalls = recursiveCallCollector.findRecursiveCalls(block.elements());
      if (!blockRecursiveCalls.isEmpty()) {
        allRecursiveCalls.addAll(blockRecursiveCalls.stream()
          .filter(tree -> !isInsideTryBlock(tree))
          .toList());
      } else {
        block.successors().stream().filter(pushedBlocks::add).forEach(blockToVisit::addLast);
      }
    }
    return recursiveCallCollector.functionSymbolHasBeenReassigned;
  }

  private static boolean isInsideTryBlock(Tree tree) {
    Tree ancestor = TreeUtils.firstAncestorOfKind(tree, Tree.Kind.FINALLY_CLAUSE, Tree.Kind.TRY_STMT);
    return ancestor != null && ancestor.is(Tree.Kind.TRY_STMT);
  }

  private static class RecursiveCallCollector extends BaseTreeVisitor {

    private final boolean isMethod;
    private final Symbol functionSymbol;
    @Nullable
    private final Symbol selfSymbol;
    // Classes can not only be compared by their string names with the current semantic
    @Nullable
    private final String className;
    private boolean functionSymbolHasBeenReassigned = false;
    private boolean isAsync = false;
    private final List recursiveCalls = new ArrayList<>();

    private RecursiveCallCollector(FunctionDef currentFunction, Symbol functionSymbol) {
      isMethod = currentFunction.isMethodDefinition();
      this.functionSymbol = functionSymbol;
      if (currentFunction.asyncKeyword() != null) {
        isAsync = true;
      }
      if (isMethod) {
        boolean isStatic = currentFunction.decorators().stream()
          .map(d -> TreeUtils.decoratorNameFromExpression(d.expression()))
          .anyMatch(decorator -> "staticmethod".equals(decorator) || "classmethod".equals(decorator));
        if (isStatic) {
          selfSymbol = null;
          className = findParentClassName(currentFunction);
        } else {
          selfSymbol = CheckUtils.findFirstParameterSymbol(currentFunction);
          className = null;
        }
      } else {
        selfSymbol = null;
        className = null;
      }
    }

    private List findRecursiveCalls(List elements) {
      recursiveCalls.clear();
      elements.forEach(element -> element.accept(this));
      return recursiveCalls;
    }

    @Override
    public void visitCallExpression(CallExpression callExpression) {
      Expression callee = callExpression.callee();
      if (!isAsyncWithoutAwait(callExpression) && (matchesLookupFunction(callee) || matchesLookupMethod(callee))) {
        recursiveCalls.add(callee);
      }
      super.visitCallExpression(callExpression);
    }

    private boolean isAsyncWithoutAwait(CallExpression callExpression) {
      Tree parent = TreeUtils.firstAncestorOfKind(callExpression, Tree.Kind.AWAIT, Tree.Kind.CALL_EXPR);
      return isAsync && (parent == null || !parent.is(Tree.Kind.AWAIT));
    }

    @Override
    public void visitFunctionDef(FunctionDef pyFunctionDefTree) {
      // ignore
    }

    @Override
    public void visitLambda(LambdaExpression pyLambdaExpressionTree) {
      // ignore
    }

    @Override
    public void visitConditionalExpression(ConditionalExpression pyConditionalExpressionTree) {
      scan(pyConditionalExpressionTree.condition());
      // ignore trueExpression and falseExpression, not broken down in the cfg
    }

    @Override
    public void visitPyListOrSetCompExpression(ComprehensionExpression tree) {
      scan(tree.comprehensionFor());
      // ignore resultExpression, not broken down in the cfg
    }

    @Override
    public void visitComprehensionIf(ComprehensionIf tree) {
      // ignore, not broken down in the cfg
    }

    @Override
    public void visitDictCompExpression(DictCompExpression tree) {
      // ignore, not broken down in the cfg
    }

    @Override
    public void visitAssignmentStatement(AssignmentStatement assignment) {
      if (isMethod && assignment.lhsExpressions().stream()
        .map(ExpressionList::expressions)
        .flatMap(Collection::stream)
        .anyMatch(expression -> matchesLookupSelf(expression) || matchesLookupMethod(expression))) {
        this.functionSymbolHasBeenReassigned = true;
      }
      super.visitAssignmentStatement(assignment);
    }

    @Override
    public void visitBinaryExpression(BinaryExpression pyBinaryExpressionTree) {
      scan(pyBinaryExpressionTree.leftOperand());
      // ignore conditional rightOperand, not broken down in the cfg
      String operator = pyBinaryExpressionTree.operator().value();
      if (!(PythonKeyword.OR.getValue().equals(operator) || PythonKeyword.AND.getValue().equals(operator))) {
        scan(pyBinaryExpressionTree.rightOperand());
      }
    }

    private boolean matchesLookupFunction(Expression expression) {
      if (!expression.is(Tree.Kind.NAME)) {
        return false;
      }
      Name name = (Name) expression;
      return !isMethod && functionSymbol.equals(name.symbol()) &&
        functionSymbol.usages().stream().filter(Usage::isBindingUsage).count() < 2;
    }

    private boolean matchesLookupMethod(Expression expression) {
      if (!expression.is(Tree.Kind.QUALIFIED_EXPR)) {
        return false;
      }
      QualifiedExpression qualifiedExpression = (QualifiedExpression) expression;
      // qualifiedExpression.name() symbols can not only be compared by their string names with the current semantic
      if (!isMethod || !functionSymbol.name().equals(qualifiedExpression.name().name())) {
        return false;
      }
      Expression qualifier = qualifiedExpression.qualifier();
      return matchesLookupSelf(qualifier) || matchesLookupClassName(qualifier);
    }

    private boolean matchesLookupSelf(Expression expression) {
      if (selfSymbol == null || !expression.is(Tree.Kind.NAME)) {
        return false;
      }
      return selfSymbol.equals(((Name) expression).symbol());
    }

    private boolean matchesLookupClassName(Expression expression) {
      if (className == null || !expression.is(Tree.Kind.NAME)) {
        return false;
      }
      return className.equals(((Name) expression).name());
    }

    @CheckForNull
    private static String findParentClassName(FunctionDef functionDef) {
      ClassDef parentClass = CheckUtils.getParentClassDef(functionDef);
      if (parentClass != null) {
        // classes symbols can not only be compared by their string names with the current semantic
        // and to prevent false-position, we ignore then a local variable with the same name exists
        String className = parentClass.name().name();
        boolean conflictsWithLocalVariable = functionDef.localVariables().stream().map(Symbol::name).anyMatch(className::equals);
        if (!conflictsWithLocalVariable) {
          return className;
        }
      }
      return null;
    }

  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy