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

org.sonar.python.checks.FunctionUsingLoopVariableCheck 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.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.sonar.check.Rule;
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
import org.sonar.plugins.python.api.SubscriptionContext;
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.CallExpression;
import org.sonar.plugins.python.api.tree.ClassDef;
import org.sonar.plugins.python.api.tree.ComprehensionExpression;
import org.sonar.plugins.python.api.tree.Decorator;
import org.sonar.plugins.python.api.tree.DictCompExpression;
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.FunctionLike;
import org.sonar.plugins.python.api.tree.LambdaExpression;
import org.sonar.plugins.python.api.tree.Name;
import org.sonar.plugins.python.api.tree.ParameterList;
import org.sonar.plugins.python.api.tree.ReturnStatement;
import org.sonar.plugins.python.api.tree.Tree;
import org.sonar.plugins.python.api.tree.YieldStatement;
import org.sonar.python.checks.utils.Expressions;
import org.sonar.python.semantic.SymbolUtils;
import org.sonar.python.tree.TreeUtils;

@Rule(key = "S1515")
public class FunctionUsingLoopVariableCheck extends PythonSubscriptionCheck {
  @Override
  public void initialize(Context context) {
    context.registerSyntaxNodeConsumer(Tree.Kind.FUNCDEF, FunctionUsingLoopVariableCheck::checkFunctionLike);
    context.registerSyntaxNodeConsumer(Tree.Kind.LAMBDA, FunctionUsingLoopVariableCheck::checkFunctionLike);
  }

  private static void checkFunctionLike(SubscriptionContext ctx) {
    FunctionLike functionLike = (FunctionLike) ctx.syntaxNode();
    Tree enclosingLoop = enclosingLoop(functionLike);
    if (enclosingLoop == null || !enclosingLoop.is(Tree.Kind.WHILE_STMT, Tree.Kind.FOR_STMT, Tree.Kind.GENERATOR_EXPR, Tree.Kind.LIST_COMPREHENSION,
      Tree.Kind.SET_COMPREHENSION, Tree.Kind.DICT_COMPREHENSION)) {
      return;
    }
    if (isReturnedOrCalledWithinLoop(functionLike, enclosingLoop)) {
      return;
    }
    Set enclosingScopeSymbols = getEnclosingScopeSymbols(enclosingLoop);
    for (Symbol symbol : enclosingScopeSymbols) {
      List problematicUsages = new ArrayList<>();
      List bindingUsages = new ArrayList<>();
      for (Usage usage : symbol.usages()) {
        Tree usageTree = usage.tree();
        if (isUsedInFunctionLike(usageTree, functionLike) && !usage.isBindingUsage()) {
          if (TreeUtils.firstAncestor(usageTree, t -> t.is(Tree.Kind.NONLOCAL_STMT, Tree.Kind.GLOBAL_STMT)) != null) {
            // We don't raise any issue on the variable if it's part of a nonlocal or global statement
            problematicUsages.clear();
            break;
          }
          problematicUsages.add(usageTree);
        }
        if (usage.isBindingUsage() && isWithinEnclosingLoop(usageTree, enclosingLoop)) {
          bindingUsages.add(usageTree);
        }
      }
      reportIssue(ctx, functionLike, problematicUsages, bindingUsages, symbol.name());
    }
  }

  private static void reportIssue(SubscriptionContext ctx, FunctionLike functionLike, List problematicUsages, List bindingUsages, String symbolName) {
    if (!problematicUsages.isEmpty() && !bindingUsages.isEmpty()) {
      PreciseIssue issue;
      if (functionLike.is(Tree.Kind.FUNCDEF)) {
        issue = ctx.addIssue(problematicUsages.get(0), String.format("Add a parameter to function \"%s\" and use variable \"%s\" as its default value;" +
          "The value of \"%s\" might change at the next loop iteration.", ((FunctionDef) functionLike).name().name(), symbolName, symbolName))
          .secondary(((FunctionDef) functionLike).name(), "Function capturing the variable");
      } else {
        issue = ctx.addIssue(problematicUsages.get(0),
          String.format("Add a parameter to the parent lambda function and use variable \"%s\" as its default value; " +
            "The value of \"%s\" might change at the next loop iteration.", symbolName, symbolName))
          .secondary(((LambdaExpression) functionLike).lambdaKeyword(), "Lambda capturing the variable");
      }
      for (Tree bindingUsage : bindingUsages) {
        issue.secondary(bindingUsage, "Assignment in the loop");
      }
    }
  }

  private static boolean isUsedInFunctionLike(Tree usageTree, FunctionLike functionLike) {
    ParameterList parameters = functionLike.parameters();
    if (parameters != null && isUsedAsDefaultValue(usageTree, parameters)) {
      return false;
    }
    if (functionLike.is(Tree.Kind.FUNCDEF)) {
      FunctionDef functionDef = (FunctionDef) functionLike;
      for (Decorator decorator : functionDef.decorators()) {
        if (TreeUtils.hasDescendant(decorator, t -> t.equals(usageTree))) {
          return false;
        }
      }
    }
    return TreeUtils.hasDescendant(functionLike, tree -> tree.equals(usageTree));
  }

  private static boolean isUsedAsDefaultValue(Tree usageTree, ParameterList parameters) {
    return parameters.nonTuple().stream().anyMatch(p ->
      p.defaultValue() != null && (usageTree.equals(p.defaultValue()) || TreeUtils.hasDescendant(p.defaultValue(), t -> t.equals(usageTree))));
  }

  private static boolean isWithinEnclosingLoop(Tree usageTree, Tree enclosingLoop) {
    return TreeUtils.hasDescendant(enclosingLoop, tree -> tree.equals(usageTree));
  }

  private static Set getEnclosingScopeSymbols(Tree enclosingLoop) {
    if (enclosingLoop.is(Tree.Kind.LIST_COMPREHENSION) || enclosingLoop.is(Tree.Kind.SET_COMPREHENSION) || enclosingLoop.is(Tree.Kind.GENERATOR_EXPR)) {
      return ((ComprehensionExpression) enclosingLoop).localVariables();
    }
    if (enclosingLoop.is(Tree.Kind.DICT_COMPREHENSION)) {
      return ((DictCompExpression) enclosingLoop).localVariables();
    }
    Tree enclosingScope = TreeUtils.firstAncestor(enclosingLoop, tree -> tree.is(Tree.Kind.FUNCDEF, Tree.Kind.CLASSDEF, Tree.Kind.FILE_INPUT));
    if (enclosingScope == null) {
      return Collections.emptySet();
    }
    if (enclosingScope.is(Tree.Kind.FUNCDEF)) {
      return ((FunctionLike) enclosingScope).localVariables();
    }
    if (enclosingScope.is(Tree.Kind.CLASSDEF)) {
      return ((ClassDef) enclosingScope).classFields();
    }
    return ((FileInput) enclosingScope).globalVariables();
  }

  private static Tree enclosingLoop(FunctionLike functionLike) {
    return TreeUtils.firstAncestor(functionLike, t -> t.is(Tree.Kind.FUNCDEF, Tree.Kind.CLASSDEF, Tree.Kind.WHILE_STMT, Tree.Kind.FOR_STMT,
      Tree.Kind.RETURN_STMT, Tree.Kind.YIELD_STMT, Tree.Kind.GENERATOR_EXPR, Tree.Kind.COMP_FOR,
      Tree.Kind.LIST_COMPREHENSION, Tree.Kind.SET_COMPREHENSION, Tree.Kind.DICT_COMPREHENSION));
  }

  private static boolean isReturnedOrCalledWithinLoop(FunctionLike functionLike, Tree enclosingLoop) {
    Tree parentCallExpr = TreeUtils.firstAncestor(functionLike, t -> !t.is(Tree.Kind.PARENTHESIZED));
    if (parentCallExpr != null && parentCallExpr.is(Tree.Kind.CALL_EXPR)) {
      return true;
    }
    CallOrReturnVisitor callOrReturnVisitor = new CallOrReturnVisitor(functionLike, enclosingLoop);
    enclosingLoop.accept(callOrReturnVisitor);
    return callOrReturnVisitor.isReturned || callOrReturnVisitor.isCalled;
  }

  static class CallOrReturnVisitor extends BaseTreeVisitor {
    Tree enclosingLoop;
    FunctionLike functionLike;
    boolean isReturned = false;
    boolean isCalled = false;

    public CallOrReturnVisitor(FunctionLike functionLike, Tree enclosingLoop) {
      this.functionLike = functionLike;
      this.enclosingLoop = enclosingLoop;
    }

    @Override
    public void visitCallExpression(CallExpression callExpression) {
      Symbol calleeSymbol = callExpression.calleeSymbol();
      if (calleeSymbol != null) {
        if (functionLike.is(Tree.Kind.FUNCDEF)) {
          isCalled |= calleeSymbol.equals(((FunctionDef) functionLike).name().symbol());
        } else {
          // lambda expression
          Name name = variableAssigned((LambdaExpression) functionLike);
          if (name != null) {
            isCalled |= calleeSymbol.equals(name.symbol());
          }
        }
      }
      super.visitCallExpression(callExpression);
    }

    @Override
    public void visitReturnStatement(ReturnStatement returnStatement) {
      isReturned |= isFunctionLikeReturned(returnStatement);
    }

    @Override
    public void visitYieldStatement(YieldStatement yieldStatement) {
      isReturned |= isFunctionLikeReturned(yieldStatement);
    }

    private static Name variableAssigned(LambdaExpression lambdaExpression) {
      Tree parentAssignment = TreeUtils.firstAncestorOfKind(lambdaExpression, Tree.Kind.ASSIGNMENT_STMT);
      if (parentAssignment != null) {
        AssignmentStatement assignmentStatement = (AssignmentStatement) parentAssignment;
        if (assignmentStatement.lhsExpressions().get(0).expressions().get(0).is(Tree.Kind.NAME)) {
          Name name = (Name) assignmentStatement.lhsExpressions().get(0).expressions().get(0);
          Expression expression = Expressions.singleAssignedValue(name);
          if (expression != null && expression.equals(lambdaExpression)) {
            return name;
          }
        }
      }
      return null;
    }

    private boolean isFunctionLikeReturned(Tree yieldOrReturnTree) {
      if (functionLike.is(Tree.Kind.FUNCDEF)) {
        Symbol functionSymbol = ((FunctionDef) functionLike).name().symbol();
        return TreeUtils.hasDescendant(yieldOrReturnTree, d -> TreeUtils.getSymbolFromTree(d).filter(symbol -> symbol.equals(functionSymbol)).isPresent());
      } else {
        // lambda expression
        return isLambdaReturned((LambdaExpression) functionLike, yieldOrReturnTree);
      }
    }

    private static boolean isLambdaReturned(LambdaExpression lambdaExpression, Tree yieldOrReturnTree) {
      Tree parentAssignment = TreeUtils.firstAncestorOfKind(lambdaExpression, Tree.Kind.ASSIGNMENT_STMT);
      if (parentAssignment != null) {
        AssignmentStatement assignmentStatement = (AssignmentStatement) parentAssignment;
        // If the lambda expression is used to construct a returned variable, we don't raise issues to avoid FPs, even if the lambda is not returned explicitly
        return SymbolUtils.assignmentsLhs(assignmentStatement).stream()
          .map(TreeUtils::getSymbolFromTree)
          .anyMatch(symbol -> TreeUtils.hasDescendant(yieldOrReturnTree, d -> TreeUtils.getSymbolFromTree(d).equals(symbol)));
      }
      return false;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy