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

org.sonar.python.checks.MembershipTestSupportCheck 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.List;
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.AmbiguousSymbol;
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.Expression;
import org.sonar.plugins.python.api.tree.InExpression;
import org.sonar.plugins.python.api.tree.Tree;
import org.sonar.plugins.python.api.types.InferredType;
import org.sonar.python.tree.TreeUtils;
import org.sonar.python.types.InferredTypes;

@Rule(key = "S5642")
public class MembershipTestSupportCheck extends PythonSubscriptionCheck {
  private static final String PRIMARY_MESSAGE = "Change the type of %s";
  private static final String PRIMARY_MESSAGE_MULTILINE = "Change the type for the target expression of `in`";
  private static final String KNOWN_TYPE_MESSAGE = "; type %s does not support membership protocol.";
  private static final String UNKNOWN_TYPE_MESSAGE = "; the type does not support the membership protocol.";
  private static final String SECONDARY_MESSAGE = "The result value of this expression does not support the membership protocol.";

  // The ordering of membership protocol methods matters here (for extreme edge cases).
  //
  // For instance, a class that has __contains__ set to None but which defines __iter__ does not fulfill the membership protocol
  // A class that defines __contains__ and has __iter__ set to None does fulfill the membership protocol
  //
  // Hence, we check for these special methods in the same order as the python interpreter does at runtime.
  private static final List MEMBERSHIP_PROTOCOL_ENABLING_METHODS = List.of("__contains__", "__iter__", "__getitem__");

  @Override
  public void initialize(Context context) {
    context.registerSyntaxNodeConsumer(Tree.Kind.IN, ctx -> checkInExpression(ctx, (InExpression) ctx.syntaxNode()));
  }

  private static void checkInExpression(SubscriptionContext ctx, InExpression inExpression) {
    Expression rhs = inExpression.rightOperand();
    InferredType rhsType = rhs.type();

    if (canSupportMembershipProtocol(rhsType)) {
      return;
    }

    addIssueOnInAndNotIn(ctx, inExpression, genPrimaryMessage(rhs, rhsType))
      .secondary(inExpression.rightOperand(), SECONDARY_MESSAGE);
  }

  private static String genPrimaryMessage(Expression rhs, InferredType rhsType) {
    // Try to render rhs into the message, but only if it is not multiline to avoid generating messages that are too long.
    String inTarget = TreeUtils.treeToString(rhs, false);
    String message;
    if (inTarget == null) {
      message = PRIMARY_MESSAGE_MULTILINE;
    } else {
      message = String.format(PRIMARY_MESSAGE, inTarget);
    }

    var typeName = InferredTypes.typeName(rhsType);
    if (typeName != null) {
      message += String.format(KNOWN_TYPE_MESSAGE, typeName);
    } else {
      message += UNKNOWN_TYPE_MESSAGE;
    }

    return message;
  }

  private static PreciseIssue addIssueOnInAndNotIn(SubscriptionContext ctx, InExpression inExpression, String message) {
    var notToken = inExpression.notToken();
    if (notToken == null) {
      return ctx.addIssue(inExpression.operator(), message);
    }

    return ctx.addIssue(notToken, inExpression.operator(), message);
  }

  private static boolean canSupportMembershipProtocol(InferredType type) {
    for (var methodName : MEMBERSHIP_PROTOCOL_ENABLING_METHODS) {
      switch (canMemberBeMethod(type, methodName)) {
        case NOT_A_METHOD:
          return false;
        case METHOD, UNKNOWN:
          return true;
        default:
      }
    }

    // all membership protocol methods are guaranteed not to be present
    return false;
  }

  private enum MemberType {
    METHOD,
    NOT_A_METHOD,
    NOT_PRESENT,
    UNKNOWN
  }

  private static boolean canBeMethodSymbol(Symbol symbol) {
    if (symbol.is(Symbol.Kind.FUNCTION)) {
      return true;
    }

    // To avoid FPs, we accept OTHER unless we can show that it is a non-callable.
    //
    // This handles cases like __contains__ = other() or __contains__ = None.
    // Although it is unclear, whether such edge cases really appear in the wild
    if (symbol.is(Symbol.Kind.OTHER)) {
      var bindingUsages = symbol.usages().stream().filter(Usage::isBindingUsage).limit(2).toList();
      if (bindingUsages.size() == 1) {
        var bindingUsage = bindingUsages.get(0);
        var assignment = TreeUtils.firstAncestorOfKind(bindingUsage.tree(), Tree.Kind.ASSIGNMENT_STMT);

        return assignment == null || ((AssignmentStatement) assignment).assignedValue().type().canHaveMember("__call__");
      }

      return true;
    }

    if (symbol.is(Symbol.Kind.AMBIGUOUS)) {
      return ((AmbiguousSymbol) symbol).alternatives().stream().anyMatch(MembershipTestSupportCheck::canBeMethodSymbol);
    }

    return false;
  }

  private static MemberType canMemberBeMethod(InferredType type, String methodName) {
    var maybeMember = type.resolveMember(methodName);
    if (maybeMember.isPresent()) {
      var symbol = maybeMember.get();

      if (canBeMethodSymbol(symbol)) {
        return MemberType.METHOD;
      }

      return MemberType.NOT_A_METHOD;
    }

    if (!type.canHaveMember(methodName)) {
      return MemberType.NOT_PRESENT;
    }

    return MemberType.UNKNOWN;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy