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

org.sonar.python.checks.IdentityComparisonWithNewObjectCheck 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.Collections;
import java.util.HashSet;
import java.util.List;
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.SubscriptionContext;
import org.sonar.plugins.python.api.symbols.Symbol;
import org.sonar.plugins.python.api.symbols.Usage;
import org.sonar.plugins.python.api.tree.CallExpression;
import org.sonar.plugins.python.api.tree.Expression;
import org.sonar.plugins.python.api.tree.IsExpression;
import org.sonar.plugins.python.api.tree.Name;
import org.sonar.plugins.python.api.tree.Tree;
import org.sonar.plugins.python.api.types.InferredType;
import org.sonar.plugins.python.api.quickfix.PythonQuickFix;
import org.sonar.python.checks.utils.Expressions;
import org.sonar.python.quickfix.TextEditUtils;
import org.sonar.python.tree.TreeUtils;

import static java.util.Arrays.asList;
import static org.sonar.plugins.python.api.types.BuiltinTypes.NONE_TYPE;

// https://jira.sonarsource.com/browse/RSPEC-5796
@Rule(key = "S5796")
public class IdentityComparisonWithNewObjectCheck extends PythonSubscriptionCheck {
  private static final String MESSAGE_IS = "Replace this \"is\" operator with \"==\".";
  public static final String IS_QUICK_FIX_MESSAGE = "Replace with \"==\"";
  public static final String IS_NOT_QUICK_FIX_MESSAGE = "Replace with \"!=\"";
  private static final String MESSAGE_IS_NOT = "Replace this \"is not\" operator with \"!=\".";
  private static final String MESSAGE_SECONDARY = "This expression creates a new object every time.";

  @Override
  public void initialize(Context context) {
    context.registerSyntaxNodeConsumer(Tree.Kind.IS, IdentityComparisonWithNewObjectCheck::checkIsComparison);
  }

  private static void checkIsComparison(SubscriptionContext subscriptionContext) {
    final IsExpression isExpr = (IsExpression) subscriptionContext.syntaxNode();

    // Exit early if we can infer that the types don't match to avoid overlap with RSPEC-3403, RSPEC-5727
    InferredType t1 = isExpr.leftOperand().type();
    InferredType t2 = isExpr.rightOperand().type();
    if (!t1.isIdentityComparableWith(t2) || t1.canOnlyBe(NONE_TYPE) || t2.canOnlyBe(NONE_TYPE)) {
      return;
    }

    // The `if` merely ensures that an issue is reported at most once per operator.
    if (!checkOperand(isExpr.leftOperand(), isExpr, subscriptionContext)) {
      checkOperand(isExpr.rightOperand(), isExpr, subscriptionContext);
    }
  }

  /**
   * Checks a single operand of an is/is not-comparison.
   * Returns true if it finds an issue.
   */
  private static boolean checkOperand(Expression operand, IsExpression isExpr, SubscriptionContext ctx) {
    Optional> secondariesOpt = findIssueForOperand(operand);
    secondariesOpt.ifPresent(secondaryLocations -> {
      PreciseIssue issue;
      var notToken = isExpr.notToken();
      if (notToken != null) {
        var quickFix = PythonQuickFix.newQuickFix(IS_NOT_QUICK_FIX_MESSAGE)
          .addTextEdit(TextEditUtils.replace(isExpr.operator(), "!="))
          .addTextEdit(TextEditUtils.removeUntil(notToken, isExpr.rightOperand()))
          .build();

        issue = ctx.addIssue(isExpr.operator(), notToken, MESSAGE_IS_NOT);
        issue.addQuickFix(quickFix);
      } else {
        var quickFix = PythonQuickFix.newQuickFix(IS_QUICK_FIX_MESSAGE)
          .addTextEdit(TextEditUtils.replace(isExpr.operator(), "=="))
          .build();
        issue = ctx.addIssue(isExpr.operator(), MESSAGE_IS);
        issue.addQuickFix(quickFix);
      }


      for (Tree secondary : secondaryLocations) {
        issue.secondary(secondary, MESSAGE_SECONDARY);
      }
    });
    return secondariesOpt.isPresent();
  }

  /**
   * Checks whether an operand of an is-comparison is suitable.
   *
   * @param expr the operand
   * @return An empty option if there is no issue, or an option containing the list of relevant secondary expressions.
   */
  private static Optional> findIssueForOperand(Expression expr) {
    if (instantiatesFreshObject(expr)) {
      // Issue exists, but the list of secondaries is empty.
      return Optional.of(Collections.emptyList());
    } else if (expr.is(Tree.Kind.NAME)) {
      Name name = (Name) expr;
      Symbol symb = name.symbol();
      if (symb != null) {
        Expression rhs = Expressions.singleAssignedValue((Name) expr);
        if (rhs != null && instantiatesFreshObject(rhs) && cannotEscape(symb)) {
          // Issue exists, right hand side of the assignment that defines the variable is the secondary position
          return Optional.of(Collections.singletonList(rhs));
        }
      }
    }
    return Optional.empty();
  }

  /**
   * Fully qualified names of constructors and functions that are guaranteed to return references that aren't shared
   * anywhere else, and are thus unsuitable for a comparison with an is, because the comparison would
   * always return False.
   *
   * Note that these are fully qualified names of (value-level) expressions, not types.
   */
  private static final Set FUNCTIONS_RETURNING_UNIQUE_REF =
    new HashSet<>(asList("dict", "list", "set", "complex"));

  /**
   * Checks whether an expression is guaranteed to instantiate a fresh object with a reference that has not been
   * shared anywhere.
   */
  private static boolean instantiatesFreshObject(Expression expr) {
    switch (expr.getKind()) {
      case DICTIONARY_LITERAL, DICT_COMPREHENSION, LIST_LITERAL, LIST_COMPREHENSION, SET_LITERAL, SET_COMPREHENSION -> {
        return true;
      }
      case CALL_EXPR -> {
        Symbol calleeSymbol = ((CallExpression) expr).calleeSymbol();
        if (calleeSymbol != null) {
          return FUNCTIONS_RETURNING_UNIQUE_REF.contains(calleeSymbol.fullyQualifiedName());
        }
        return false;
      }
      default -> {
        return false;
      }
    }
  }

  /**
   * Checks that the reference stored in a variable cannot escape.
   */
  private static boolean cannotEscape(Symbol symb) {
    List usages = symb.usages();
    if (usages.size() > 2) {
      // Check that all usages are either assignments or `is`-comparisons.
      return usages.stream().allMatch(u -> u.isBindingUsage() ||
        TreeUtils.firstAncestorOfKind(u.tree(), Tree.Kind.IS) != null);
    }
    return true;
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy