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

org.sonar.python.checks.ModifiedParameterValueCheck Maven / Gradle / Ivy

The 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.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.check.Rule;
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
import org.sonar.plugins.python.api.quickfix.PythonQuickFix;
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.CompoundAssignmentStatement;
import org.sonar.plugins.python.api.tree.Expression;
import org.sonar.plugins.python.api.tree.FunctionDef;
import org.sonar.plugins.python.api.tree.Parameter;
import org.sonar.plugins.python.api.tree.QualifiedExpression;
import org.sonar.plugins.python.api.tree.SubscriptionExpression;
import org.sonar.plugins.python.api.tree.Tree;
import org.sonar.plugins.python.api.types.BuiltinTypes;
import org.sonar.python.checks.utils.CheckUtils;
import org.sonar.python.tree.TreeUtils;

import static org.sonar.plugins.python.api.tree.Tree.Kind.ASSIGNMENT_STMT;
import static org.sonar.plugins.python.api.tree.Tree.Kind.CALL_EXPR;
import static org.sonar.plugins.python.api.tree.Tree.Kind.COMPOUND_ASSIGNMENT;
import static org.sonar.plugins.python.api.tree.Tree.Kind.DEL_STMT;
import static org.sonar.plugins.python.api.tree.Tree.Kind.DICTIONARY_LITERAL;
import static org.sonar.plugins.python.api.tree.Tree.Kind.FUNCDEF;
import static org.sonar.plugins.python.api.tree.Tree.Kind.LIST_LITERAL;
import static org.sonar.plugins.python.api.tree.Tree.Kind.QUALIFIED_EXPR;
import static org.sonar.plugins.python.api.tree.Tree.Kind.SET_LITERAL;
import static org.sonar.plugins.python.api.tree.Tree.Kind.SUBSCRIPTION;
import static org.sonar.python.quickfix.TextEditUtils.insertLineBefore;
import static org.sonar.python.quickfix.TextEditUtils.replace;
import static org.sonar.python.tree.TreeUtils.getSymbolFromTree;
import static org.sonar.python.tree.TreeUtils.nonTupleParameters;

@Rule(key = "S5717")
public class ModifiedParameterValueCheck extends PythonSubscriptionCheck {

  private static final String MESSAGE = "Change this default value to \"None\" and initialize this parameter inside the function/method.";
  private static final String MODIFIED_SECONDARY = "The parameter is modified.";
  private static final String ASSIGNED_SECONDARY = "The parameter is stored in another object.";

  private static final Set COMMON_MUTATING_METHODS = new HashSet<>(Arrays.asList("__delitem__", "__setitem__"));
  private static final String CLEAR = "clear";
  private static final Set LIST_MUTATING_METHODS = new HashSet<>(Arrays.asList("append", CLEAR, "extend", "insert", "pop", "remove", "reverse", "sort"));
  private static final Set SET_MUTATING_METHODS = new HashSet<>(
    Arrays.asList("update", "intersection_update", "difference_update", "symmetric_difference_update", "add", "remove", "discard", "pop", CLEAR));
  private static final Set DICT_MUTATING_METHODS = new HashSet<>(Arrays.asList("pop", CLEAR, "popitem", "setdefault", "update"));
  private static final Set DEQUE_MUTATING_METHODS = new HashSet<>(Arrays.asList("appendleft", "extendleft", "popleft", "rotate"));
  static {
    DEQUE_MUTATING_METHODS.addAll(LIST_MUTATING_METHODS);
  }
  private static final Set COUNTER_MUTATING_METHODS = new HashSet<>();
  static {
    COUNTER_MUTATING_METHODS.add("subtract");
    COUNTER_MUTATING_METHODS.addAll(DICT_MUTATING_METHODS);
  }

  private static final Set ORDERED_DICT_MUTATING_METHODS = new HashSet<>();
  static {
    ORDERED_DICT_MUTATING_METHODS.add("move_to_end");
    ORDERED_DICT_MUTATING_METHODS.addAll(DICT_MUTATING_METHODS);
  }

  private static final Set DEFAULT_DICT_MUTATING_METHODS = new HashSet<>();
  static {
    DEFAULT_DICT_MUTATING_METHODS.add("__getitem__");
    DEFAULT_DICT_MUTATING_METHODS.addAll(DICT_MUTATING_METHODS);
  }

  private static final Map> MUTATING_METHODS = new HashMap<>();
  static {
    MUTATING_METHODS.put("list", LIST_MUTATING_METHODS);
    MUTATING_METHODS.put("set", SET_MUTATING_METHODS);
    MUTATING_METHODS.put("dict", DICT_MUTATING_METHODS);
    MUTATING_METHODS.put("collections.deque", DEQUE_MUTATING_METHODS);
    MUTATING_METHODS.put("collections.UserList", LIST_MUTATING_METHODS);
    MUTATING_METHODS.put("collections.UserDict", DICT_MUTATING_METHODS);
    MUTATING_METHODS.put("collections.ChainMap", DICT_MUTATING_METHODS);
    MUTATING_METHODS.put("collections.Counter", COUNTER_MUTATING_METHODS);
    MUTATING_METHODS.put("collections.OrderedDict", ORDERED_DICT_MUTATING_METHODS);
    MUTATING_METHODS.put("collections.defaultdict", DEFAULT_DICT_MUTATING_METHODS);
  }

  @Override
  public void initialize(Context context) {
    context.registerSyntaxNodeConsumer(FUNCDEF, ctx -> {
      FunctionDef functionDef = (FunctionDef) ctx.syntaxNode();
      // avoid raising issues on nested function, it may have been done on purpose
      if (TreeUtils.firstAncestorOfKind(functionDef, FUNCDEF) != null) {
        return;
      }

      for (Parameter parameter : nonTupleParameters(functionDef)) {
        Expression defaultValue = parameter.defaultValue();
        if (defaultValue == null) {
          continue;
        }

        getSymbolFromTree(parameter.name())
          .filter(symbol -> !isUsingMemoization(symbol))
          .ifPresent(paramSymbol -> {
            Map mutations = getMutations(defaultValue, paramSymbol);
            if (!mutations.isEmpty()) {
              PreciseIssue issue = ctx.addIssue(parameter, MESSAGE);
              mutations.keySet().forEach(t -> issue.secondary(t, mutations.get(t)));

              getQuickFix(functionDef, defaultValue, paramSymbol)
                .ifPresent(issue::addQuickFix);
            }
          });
      }
    });
  }

  // We use "\n" systematically, the IDE will decide which one to use,
  // therefore suppressing java:S3457 (Printf-style format strings should be used correctly)
  @SuppressWarnings("java:S3457")
  private static Optional getQuickFix(FunctionDef functionDef, Expression defaultValue, Symbol paramSymbol) {
    Tree firstStatement = functionDef.body().statements().get(0);
    String paramName = paramSymbol.name();

    return Optional.ofNullable(parameterInitialization(defaultValue)).map(
      paramInit -> PythonQuickFix.newQuickFix("Initialize this parameter inside the function/method")
        .addTextEdit(replace(defaultValue, "None"))
        .addTextEdit(insertLineBefore(firstStatement, String.format("if %1$s is None:\n    %1$s = %2$s", paramName, paramInit)))
        .build());
  }

  @CheckForNull
  private static String parameterInitialization(Expression defaultValue) {
    if (defaultValue.is(CALL_EXPR, DICTIONARY_LITERAL, LIST_LITERAL, SET_LITERAL)) {
      return TreeUtils.treeToString(defaultValue, false);
    }
    return null;
  }

  @CheckForNull
  private static String defaultValueType(Expression expression) {
    for (String nonCompliantType : MUTATING_METHODS.keySet()) {
      if (expression.type().canOnlyBe(nonCompliantType)) {
        return nonCompliantType;
      }
    }
    return null;
  }

  private static boolean isUsingMemoization(Symbol symbol) {
    return symbol.name().contains("cache") || symbol.name().contains("memo");
  }

  private static Map getMutations(Expression defaultValue, Symbol paramSymbol) {
    if (!defaultValue.type().canOnlyBe(BuiltinTypes.NONE_TYPE)) {
      List attributeSet = getAttributeSet(paramSymbol);
      if (!attributeSet.isEmpty()) {
        return attributeSet.stream().collect(Collectors.toMap(tree -> tree, tree -> MODIFIED_SECONDARY));
      }
    }
    String defaultValueType = defaultValueType(defaultValue);
    Set typeMutatingMethods = MUTATING_METHODS.get(defaultValueType);
    if (typeMutatingMethods == null) {
      return Collections.emptyMap();
    }
    Map mutations = new HashMap<>();
    for (Usage usage : paramSymbol.usages()) {
      getKindOfWriteUsage(paramSymbol, defaultValueType, typeMutatingMethods, usage).ifPresent(s -> mutations.put(usage.tree().parent(), s));
    }
    return mutations;
  }

  private static Optional getKindOfWriteUsage(Symbol paramSymbol, @Nullable String defaultValueType, Set typeMutatingMethods, Usage usage) {
    Tree parent = usage.tree().parent();
    if (parent.is(QUALIFIED_EXPR)) {
      QualifiedExpression qualifiedExpression = (QualifiedExpression) parent;

      return getSymbolFromTree(qualifiedExpression.qualifier()).filter(paramSymbol::equals).isPresent()
        && isMutatingMethod(typeMutatingMethods, qualifiedExpression.name().name()) ? Optional.of(MODIFIED_SECONDARY) : Optional.empty();
    }
    if (isUsedInDelStatement(usage.tree()) ||
      isUsedInLhsOfAssignment(usage.tree(), exp -> isAccessingExpression(exp, usage.tree())) ||
      isUsedInLhsOfCompoundAssignment(usage.tree()) ||
      isGetItemOnDefaultDict(defaultValueType, usage.tree())) {
      return Optional.of(MODIFIED_SECONDARY);
    }
    return mightBeReferencedOutsideOfFunction(usage.tree()) ? Optional.of(ASSIGNED_SECONDARY) : Optional.empty();
  }

  /**
   * Detects if shape of tree is equal to
   * - expression[SOMETHING]
   * - expression.SOMETHING
   */
  private static boolean isAccessingExpression(Expression expression, Tree tree) {
    return isObjectOfSubscription(tree, expression) || isQualifier(tree, expression);
  }

  /**
   * Detects case where tree might be referenced outside of function
   * - self.attr = tree
   */
  private static boolean mightBeReferencedOutsideOfFunction(Tree tree) {
    AssignmentStatement assignment = ((AssignmentStatement) TreeUtils.firstAncestorOfKind(tree, ASSIGNMENT_STMT));
    if (assignment == null) {
      return false;
    }
    return assignment.assignedValue() == tree
      && assignment.lhsExpressions().stream()
        .flatMap(expressionList -> expressionList.expressions().stream())
        .anyMatch(ModifiedParameterValueCheck::isAccessingSelf);
  }

  private static boolean isAccessingSelf(Expression expression) {
    switch (expression.getKind()) {
      case QUALIFIED_EXPR:
        return CheckUtils.isSelf(((QualifiedExpression) expression).qualifier());
      case SUBSCRIPTION:
        return CheckUtils.isSelf(((SubscriptionExpression) expression).object());
      default:
        return false;
    }
  }

  private static List getAttributeSet(Symbol paramSymbol) {
    return paramSymbol.usages().stream()
      .map(Usage::tree)
      .filter(tree -> isUsedInLhsOfAssignment(tree, expression -> isQualifier(tree, expression)))
      .toList();
  }

  private static boolean isUsedInLhsOfCompoundAssignment(Tree tree) {
    CompoundAssignmentStatement compoundAssignmentStatement = ((CompoundAssignmentStatement) TreeUtils.firstAncestorOfKind(tree, COMPOUND_ASSIGNMENT));
    return compoundAssignmentStatement != null && isAccessingExpression(compoundAssignmentStatement.lhsExpression(), tree);
  }

  private static boolean isGetItemOnDefaultDict(@Nullable String defaultValueType, Tree tree) {
    return "collections.defaultdict".equals(defaultValueType) && isObjectOfSubscription(tree, tree.parent());
  }

  private static boolean isObjectOfSubscription(Tree usageTree, Tree tree) {
    return tree.is(SUBSCRIPTION) && ((SubscriptionExpression) tree).object() == usageTree;
  }

  private static boolean isQualifier(Tree usageTree, Tree tree) {
    return tree.is(QUALIFIED_EXPR) && ((QualifiedExpression) tree).qualifier() == usageTree;
  }

  private static boolean isUsedInLhsOfAssignment(Tree tree, Predicate lhsPredicate) {
    AssignmentStatement assignment = ((AssignmentStatement) TreeUtils.firstAncestorOfKind(tree, ASSIGNMENT_STMT));
    if (assignment == null) {
      return false;
    }
    return assignment.lhsExpressions().stream()
      .flatMap(expressionList -> expressionList.expressions().stream())
      .anyMatch(lhsPredicate);
  }

  private static boolean isUsedInDelStatement(Tree tree) {
    return TreeUtils.firstAncestorOfKind(tree, DEL_STMT) != null;
  }

  private static boolean isMutatingMethod(Set typeMutatingMethods, String method) {
    return typeMutatingMethods.contains(method) ||
      COMMON_MUTATING_METHODS.contains(method) ||
      (method.startsWith("__i") && method.endsWith("__"));
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy