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

org.sonar.python.checks.ChangeMethodContractCheck 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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import javax.annotation.Nullable;
import org.sonar.check.Rule;
import org.sonar.plugins.python.api.LocationInFile;
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
import org.sonar.plugins.python.api.SubscriptionContext;
import org.sonar.plugins.python.api.symbols.FunctionSymbol;
import org.sonar.plugins.python.api.symbols.Symbol;
import org.sonar.plugins.python.api.tree.FunctionDef;
import org.sonar.python.semantic.SymbolUtils;

import static org.sonar.plugins.python.api.symbols.Symbol.Kind.FUNCTION;
import static org.sonar.plugins.python.api.tree.Tree.Kind.FUNCDEF;

@Rule(key = "S2638")
public class ChangeMethodContractCheck extends PythonSubscriptionCheck {

  private static final Set IGNORING_DECORATORS = Set.of(
    "abc.abstractmethod",
    "abstractmethod",
    "overload"
  );

  @Override
  public void initialize(Context context) {
    context.registerSyntaxNodeConsumer(FUNCDEF, ctx -> {
      FunctionDef functionDef = (FunctionDef) ctx.syntaxNode();
      String functionName = functionDef.name().name();
      if (functionName.startsWith("__") && functionName.endsWith("__")) {
        // ignore special methods
        return;
      }
      Symbol symbol = functionDef.name().symbol();
      if (symbol == null || symbol.kind() != FUNCTION) {
        return;
      }
      FunctionSymbol functionSymbol = (FunctionSymbol) symbol;
      if (functionSymbol.hasVariadicParameter() || functionSymbol.hasDecorators()) {
        // ignore function declarations with packed params
        return;
      }
      checkMethodContract(ctx, functionSymbol);
    });
  }

  private static void checkMethodContract(SubscriptionContext ctx, FunctionSymbol method) {
    SymbolUtils.getOverriddenMethod(method, SymbolUtils::getFirstAlternativeIfEqualArgumentNames)
      .ifPresent(overriddenMethod -> {
        if (overriddenMethod.hasVariadicParameter() || hasDecorators(overriddenMethod)) {
          // ignore function declarations with packed params
          return;
        }

        int paramsDiff = method.parameters().size() - overriddenMethod.parameters().size();

        if (paramsDiff != 0 && overriddenMethod.parameters().stream().anyMatch(FunctionSymbol.Parameter::isKeywordOnly)) {
          reportIssue(ctx, "Change this method signature to accept the same arguments as the method it overrides.", method.definitionLocation(), overriddenMethod);
          return;
        }

        if (paramsDiff > 0) {
          reportOnExtraParameters(ctx, method, overriddenMethod);
        } else if (paramsDiff < 0) {
          reportOnMissingParameters(ctx, method, overriddenMethod);
        } else {
          checkDefaultValuesAndParamNames(ctx, method, overriddenMethod);
        }
      });
  }

  private static boolean hasDecorators(FunctionSymbol symbol) {
    return symbol.hasDecorators() && symbol.decorators().stream().anyMatch(Predicate.not(IGNORING_DECORATORS::contains));
  }

  private static void reportOnMissingParameters(SubscriptionContext ctx, FunctionSymbol method, FunctionSymbol overriddenMethod) {
    int indexFirstMissingParam = method.parameters().size();
    List overriddenParams = overriddenMethod.parameters();
    var missingParameters = overriddenParams.subList(indexFirstMissingParam, overriddenParams.size())
      .stream()
      .map(FunctionSymbol.Parameter::name)
      .toList();
    if (!missingParameters.isEmpty()) {
//      If at least one parameter missing name - set message as "Add missing N parameters" where N is amount of missing parameters
      reportIssue(ctx, getMissingParametersMessage(missingParameters), method.definitionLocation(), overriddenMethod);
    }
  }

  private static String getMissingParametersMessage(List missingParameters) {
    if (missingParameters.contains(null)) {
      return missingParameters.size() == 1 ?
        "Add 1 missing parameter." : ("Add " + missingParameters.size() + " missing parameters.");
    }
    return "Add missing parameters " + String.join(" ", missingParameters).trim() + ".";
  }

  private static void reportIssue(SubscriptionContext ctx, String message, @Nullable LocationInFile location, FunctionSymbol overriddenMethod) {
    Optional.ofNullable(location).ifPresent(issueLocation -> {
      LocationInFile secondaryLocation = overriddenMethod.definitionLocation();
      if (secondaryLocation != null) {
        PreciseIssue preciseIssue = ctx.addIssue(issueLocation, message);
        preciseIssue.secondary(secondaryLocation, "Overridden method's definition");
      } else {
        ctx.addIssue(issueLocation, message + " This method overrides " + overriddenMethod.fullyQualifiedName() + ".");
      }
    });
  }

  private static void reportOnExtraParameters(SubscriptionContext ctx, FunctionSymbol method, FunctionSymbol overriddenMethod) {
    long paramsWithoutDefaultValue = method.parameters().stream().filter(parameter -> !parameter.hasDefaultValue()).count();
    if (paramsWithoutDefaultValue == overriddenMethod.parameters().size()) {
      return;
    }
    method.parameters().stream()
      .filter(parameter -> !parameter.hasDefaultValue() && parameter.name() != null)
      .filter(parameter -> overriddenMethod.parameters().stream().noneMatch(p -> Objects.equals(parameter.name(), p.name())))
      .forEach(parameter -> reportIssue(ctx,"Remove parameter " + parameter.name() + " or provide default value.", parameter.location(), overriddenMethod));
  }


  private static void checkDefaultValuesAndParamNames(SubscriptionContext ctx, FunctionSymbol method, FunctionSymbol overriddenMethod) {
    Map mismatchedOverriddenParamPosition = new HashMap<>();
    Map mismatchedParamPosition = new HashMap<>();

    List parameters = method.parameters();

    for (int i = 0; i < overriddenMethod.parameters().size(); i++) {
      FunctionSymbol.Parameter overriddenParam = overriddenMethod.parameters().get(i);
      FunctionSymbol.Parameter parameter = method.parameters().get(i);
      if (!Objects.equals(overriddenParam.name(), parameter.name())) {
        mismatchedOverriddenParamPosition.put(overriddenParam.name(), i);
        mismatchedParamPosition.put(parameter.name(), i);
      } else {
        checkDefaultValueAndKeywordOnly(ctx, overriddenMethod, overriddenParam, parameter);
      }
    }

    mismatchedParamPosition.forEach((name, index) -> {
      Integer overriddenParamIndex = mismatchedOverriddenParamPosition.get(name);
      FunctionSymbol.Parameter parameter = parameters.get(index);
      if (overriddenParamIndex != null && !parameter.isKeywordOnly()) {
        reportIssue(ctx, "Move parameter " + name + " to position " + overriddenParamIndex + ".", parameter.location(), overriddenMethod);
      }
    });

  }

  private static void checkDefaultValueAndKeywordOnly(SubscriptionContext ctx, FunctionSymbol overriddenMethod, FunctionSymbol.Parameter overriddenParam,
                                                      FunctionSymbol.Parameter parameter) {
    String prefix = "Make parameter " + parameter.name();
    if (overriddenParam.hasDefaultValue() && !parameter.hasDefaultValue()) {
      reportIssue(ctx, "Add a default value to parameter " + parameter.name() + ".", parameter.location(), overriddenMethod);
    }
    if ((!overriddenParam.isKeywordOnly() && !overriddenParam.isPositionalOnly()) && (parameter.isKeywordOnly() || parameter.isPositionalOnly())) {
      reportIssue(ctx, prefix + " keyword-or-positional.", parameter.location(), overriddenMethod);
    }
    if (overriddenParam.isPositionalOnly() && !parameter.isPositionalOnly()) {
      reportIssue(ctx, prefix + " positional only.", parameter.location(), overriddenMethod);
    }
    if (overriddenParam.isKeywordOnly() && !parameter.isKeywordOnly()) {
      reportIssue(ctx, prefix + " keyword only.", parameter.location(), overriddenMethod);
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy