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

org.sonar.python.checks.InstanceMethodSelfAsFirstCheck 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.Arrays;
import java.util.List;
import java.util.Optional;
import javax.annotation.Nullable;
import org.sonar.check.Rule;
import org.sonar.check.RuleProperty;
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
import org.sonar.plugins.python.api.symbols.ClassSymbol;
import org.sonar.plugins.python.api.symbols.FunctionSymbol;
import org.sonar.plugins.python.api.symbols.Usage;
import org.sonar.plugins.python.api.tree.CallExpression;
import org.sonar.plugins.python.api.tree.ClassDef;
import org.sonar.plugins.python.api.tree.Decorator;
import org.sonar.plugins.python.api.tree.FunctionDef;
import org.sonar.plugins.python.api.tree.Name;
import org.sonar.plugins.python.api.tree.Parameter;
import org.sonar.plugins.python.api.tree.Tree;
import org.sonar.python.tree.TreeUtils;

@Rule(key = "S5720")
public class InstanceMethodSelfAsFirstCheck extends PythonSubscriptionCheck {

  // We allow "_" as sometimes it is conventionally used to signal that self won't be used.
  private static final List ALLOWED_NAMES = Arrays.asList("self", "_");
  private static final List ALLOWED_NAMES_IN_METACLASSES = Arrays.asList("cls", "mcs");
  private static final List EXCEPTIONS = Arrays.asList("__init_subclass__", "__class_getitem__", "__new__");

  private static final String DEFAULT_IGNORED_DECORATORS = "abstractmethod";
  private List decoratorsToExclude;

  @RuleProperty(
    key = "ignoredDecorators",
    description = "Comma-separated list of decorators which will disable this rule.",
    defaultValue = DEFAULT_IGNORED_DECORATORS)
  public String ignoredDecorators = DEFAULT_IGNORED_DECORATORS;

  private List getExcludedDecorators() {
    if (decoratorsToExclude == null) {
      decoratorsToExclude = new ArrayList<>();
      decoratorsToExclude.add("staticmethod");
      decoratorsToExclude.add("classmethod");
      decoratorsToExclude.addAll(Arrays.asList(this.ignoredDecorators.split(",")));
    }

    return decoratorsToExclude;
  }

  private boolean isNonInstanceMethodDecorator(Decorator decorator) {
    String fqn = TreeUtils.decoratorNameFromExpression(decorator.expression());
    return fqn != null && this.getExcludedDecorators().stream().anyMatch(fqn::contains);
  }

  private static boolean isExceptionalUsageInClassBody(Usage usage, ClassDef parentClass) {
    if (usage.kind() != Usage.Kind.FUNC_DECLARATION) {
      Tree ancestor = TreeUtils.firstAncestorOfKind(usage.tree(), Tree.Kind.CLASSDEF, Tree.Kind.FUNCDEF);
      return isUsedAsDecorator(ancestor, usage.tree()) || parentClass.equals(ancestor);
    }
    return false;
  }

  private static boolean isUsedAsDecorator(@Nullable Tree tree, Tree usageTree) {
    if (tree instanceof FunctionDef functionDef) {
      return functionDef.decorators().stream()
        .map(Decorator::expression)
        .map(expression -> expression.is(Tree.Kind.CALL_EXPR) ? ((CallExpression) expression).callee() : expression)
        .anyMatch(expression -> expression.equals(usageTree));
    }
    return false;
  }

  private boolean isRelevantMethod(ClassDef classDef, ClassSymbol classSymbol, FunctionDef functionDef) {
    // Skip some known special methods
    if (EXCEPTIONS.contains(functionDef.name().name())) {
      return false;
    }

    // Skip if the class has a ignored decorator
    if (functionDef.decorators().stream().anyMatch(this::isNonInstanceMethodDecorator)) {
      return false;
    }

    FunctionSymbol functionSymbol = TreeUtils.getFunctionSymbolFromDef(functionDef);
    if (functionSymbol == null || functionSymbol.usages().stream().anyMatch(usage -> isExceptionalUsageInClassBody(usage, classDef))) {
      return false;
    }

    return !classSymbol.isOrExtends("zope.interface.Interface");
  }

  private static boolean isValidExceptionForCls(FunctionDef functionDef, String name, boolean mightBeMetaclass) {
    return ALLOWED_NAMES_IN_METACLASSES.contains(name) && (mightBeMetaclass || !functionDef.decorators().isEmpty());
  }

  @Override
  public void initialize(Context context) {
    context.registerSyntaxNodeConsumer(Tree.Kind.CLASSDEF, ctx -> {
      ClassDef classDef = (ClassDef) ctx.syntaxNode();
      ClassSymbol classSymbol = TreeUtils.getClassSymbolFromDef(classDef);

      // Do not raise on nested classes - they might use a different name for "self" in order to avoid confusion.
      if (classSymbol == null || TreeUtils.firstAncestorOfKind(classDef, Tree.Kind.CLASSDEF) != null) {
        return;
      }

      // We consider that a class MIGHT be a metaclass, either if the class is decorated, inherits from "type",
      // "typing.Protocol" or has an unresolved type hiearchy. In this case we shall also accept "cls" or "mcs"
      // as the name of the first parameter.
      boolean mightBeMetaclass = !classDef.decorators().isEmpty()
        || classSymbol.isOrExtends("type")
        || classSymbol.isOrExtends("typing.Protocol")
        || classSymbol.hasUnresolvedTypeHierarchy();

      TreeUtils.topLevelFunctionDefs(classDef).forEach(functionDef -> {
        List parameters = TreeUtils.positionalParameters(functionDef);
        if (parameters.isEmpty()) {
          return;
        }

        Parameter first = parameters.get(0);
        if (first.starToken() != null) {
          return;
        }

        Optional.ofNullable(first.name())
          .map(Name::name)
          .ifPresent(name -> {
            if (!ALLOWED_NAMES.contains(name)
              && isRelevantMethod(classDef, classSymbol, functionDef)
              && !isValidExceptionForCls(functionDef, name, mightBeMetaclass)) {
              ctx.addIssue(first, String.format("Rename \"%s\" to \"self\" or add the missing \"self\" parameter.", name));
            }
          });
      });
    });
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy