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

org.sonar.python.checks.UnusedFunctionParameterCheck 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.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
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.FunctionSymbol;
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.ExpressionStatement;
import org.sonar.plugins.python.api.tree.FunctionDef;
import org.sonar.plugins.python.api.tree.Parameter;
import org.sonar.plugins.python.api.tree.ReturnStatement;
import org.sonar.plugins.python.api.tree.Statement;
import org.sonar.plugins.python.api.tree.Token;
import org.sonar.plugins.python.api.tree.Tree;
import org.sonar.plugins.python.api.tree.Tree.Kind;
import org.sonar.plugins.python.api.tree.Trivia;
import org.sonar.python.checks.utils.CheckUtils;
import org.sonar.python.checks.utils.StringLiteralValuesCollector;
import org.sonar.python.semantic.ClassSymbolImpl;
import org.sonar.python.semantic.FunctionSymbolImpl;
import org.sonar.python.semantic.SymbolUtils;
import org.sonar.python.tree.FunctionDefImpl;
import org.sonar.python.tree.TreeUtils;

@Rule(key = "S1172")
public class UnusedFunctionParameterCheck extends PythonSubscriptionCheck {

  private static final String MESSAGE = "Remove the unused function parameter \"%s\".";

  private static final Set AWS_LAMBDA_PARAMETERS = Set.of("event", "context");

  @Override
  public void initialize(Context context) {
    context.registerSyntaxNodeConsumer(Kind.FUNCDEF, ctx -> checkFunctionParameter(ctx, ((FunctionDef) ctx.syntaxNode())));
  }

  private static void checkFunctionParameter(SubscriptionContext ctx, FunctionDef functionDef) {
    if (isException(ctx, functionDef)) return;
    functionDef.localVariables().stream()
      .filter(symbol -> !isIgnoredSymbolName(symbol.name()))
      .filter(UnusedFunctionParameterCheck::isUnused)
      .filter(symbol -> !isUsedInStringLiteralOrComment(symbol.name(), functionDef))
      .map(symbol -> (Parameter) symbol.usages().get(0).tree().parent())
      .forEach(param -> ctx.addIssue(param, String.format(MESSAGE, param.name().name())));
  }

  private static boolean isUnused(Symbol s) {
    return s.usages().size() == 1 && s.usages().get(0).tree().parent().is(Kind.PARAMETER);
  }

  /* If the parameter is used within a string literal or comment, this might indicate either:
   * A docstring or a comment explains why this parameter is unused (e.g the method is method to be overridden)
   * The parameter is used through a DSL (e.g pandas.DataFrame.query)
   */
  private static boolean isUsedInStringLiteralOrComment(String symbolName, FunctionDef functionDef) {
    StringLiteralValuesCollector stringLiteralValuesCollector = new StringLiteralValuesCollector();
    stringLiteralValuesCollector.collect(functionDef);
    List comments = collectComments(functionDef);
    Pattern p = Pattern.compile("(^|\\s+|\"|'|@)" + symbolName + "($|\\s+|\"|')");
    return stringLiteralValuesCollector.anyMatches(str -> p.matcher(str).find()) ||
      comments.stream().anyMatch(str -> p.matcher(str).find());
  }

  private static boolean isIgnoredSymbolName(String symbolName) {
    return "self".equals(symbolName) || symbolName.startsWith("_") || AWS_LAMBDA_PARAMETERS.contains(symbolName);
  }

  private static boolean isException(SubscriptionContext ctx, FunctionDef functionDef) {
    FunctionSymbol functionSymbol = ((FunctionDefImpl) functionDef).functionSymbol();
    return CheckUtils.containsCallToLocalsFunction(functionDef) ||
      SymbolUtils.canBeAnOverridingMethod(functionSymbol) ||
      isInterfaceMethod(functionDef) ||
      isNotImplemented(functionDef) ||
      !functionDef.decorators().isEmpty() ||
      isSpecialMethod(functionDef) ||
      hasNonCallUsages(functionSymbol) ||
      isTestFunction(ctx, functionDef) ||
      isAbstractClass(functionDef);
  }

  private static boolean isAbstractClass(FunctionDef functionDef) {
    FunctionSymbol functionSymbol = ((FunctionDefImpl) functionDef).functionSymbol();
    if (functionSymbol == null) {
      return false;
    }
    Symbol owner = ((FunctionSymbolImpl) functionSymbol).owner();
    return owner != null && ((((ClassSymbolImpl) owner).superClasses().stream().anyMatch(symbol -> "abc.ABC".equals(symbol.fullyQualifiedName())))
      || (((ClassSymbolImpl) owner).hasMetaClass()));
  }

  private static boolean isInterfaceMethod(FunctionDef functionDef) {
    return functionDef.body().statements().stream()
      .allMatch(statement -> statement.is(Kind.PASS_STMT, Kind.RAISE_STMT)
        || (statement.is(Kind.EXPRESSION_STMT) && isStringExpressionOrEllipsis((ExpressionStatement) statement)));
  }

  // Note that this will also exclude method containing only a return statement that returns nothing
  private static boolean isNotImplemented(FunctionDef functionDef) {
    List statements = functionDef.body().statements();
    if (statements.size() != 1) return false;
    if (!statements.get(0).is(Kind.RETURN_STMT)) return false;
    ReturnStatement returnStatement = (ReturnStatement) statements.get(0);
    return returnStatement.expressions().stream().allMatch(retValue ->
      TreeUtils.getSymbolFromTree(retValue).filter(s -> "NotImplemented".equals(s.fullyQualifiedName())).isPresent());
  }

  private static boolean isStringExpressionOrEllipsis(ExpressionStatement stmt) {
    return stmt.expressions().stream().allMatch(expr -> expr.is(Kind.STRING_LITERAL, Kind.ELLIPSIS));
  }

  private static boolean isSpecialMethod(FunctionDef functionDef) {
    String name = functionDef.name().name();
    return name.startsWith("__") && name.endsWith("__");
  }

  private static boolean hasNonCallUsages(@Nullable FunctionSymbol functionSymbol) {
    return Optional.ofNullable(functionSymbol)
      .filter(fs -> fs.usages().stream().anyMatch(usage -> usage.kind() != Usage.Kind.FUNC_DECLARATION && !isFunctionCall(usage)))
      .isPresent();
  }

  private static boolean isTestFunction(SubscriptionContext ctx, FunctionDef functionDef) {
    String fileName = ctx.pythonFile().fileName();
    if (fileName.startsWith("conftest") || fileName.startsWith("test")) {
      return true;
    }
    return functionDef.name().name().startsWith("test");
  }

  private static boolean isFunctionCall(Usage usage) {
    if (usage.kind() != Usage.Kind.OTHER) return false;
    Tree tree = usage.tree();
    CallExpression callExpression = ((CallExpression) TreeUtils.firstAncestorOfKind(tree, Kind.CALL_EXPR));
    if (callExpression == null) return false;
    Expression callee = callExpression.callee();
    return callee == tree || TreeUtils.hasDescendant(callee, t -> t == tree);
  }

  private static List collectComments(Tree element) {
    List comments = new ArrayList<>();
    Deque stack = new ArrayDeque<>();
    stack.push(element);
    while (!stack.isEmpty()) {
      Tree currentElement = stack.pop();
      if (currentElement.is(Kind.TOKEN)) {
        ((Token) currentElement).trivia().stream().map(Trivia::value).forEach(comments::add);
      }
      for (int i = currentElement.children().size() - 1; i >= 0; i--) {
        Optional.ofNullable(currentElement.children().get(i)).ifPresent(stack::push);
      }
    }
    return comments;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy