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

org.sonar.php.checks.DuplicatedMethodCheck Maven / Gradle / Ivy

The newest version!
/*
 * SonarQube PHP Plugin
 * Copyright (C) 2010-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 GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * 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 GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package org.sonar.php.checks;

import java.util.ArrayList;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import org.sonar.check.Rule;
import org.sonar.plugins.php.api.tree.CompilationUnitTree;
import org.sonar.plugins.php.api.tree.Tree;
import org.sonar.plugins.php.api.tree.declaration.ClassDeclarationTree;
import org.sonar.plugins.php.api.tree.declaration.DeclaredTypeTree;
import org.sonar.plugins.php.api.tree.declaration.FunctionDeclarationTree;
import org.sonar.plugins.php.api.tree.declaration.FunctionTree;
import org.sonar.plugins.php.api.tree.declaration.MethodDeclarationTree;
import org.sonar.plugins.php.api.tree.declaration.ReturnTypeClauseTree;
import org.sonar.plugins.php.api.tree.expression.AnonymousClassTree;
import org.sonar.plugins.php.api.tree.expression.ExpressionTree;
import org.sonar.plugins.php.api.tree.expression.NameIdentifierTree;
import org.sonar.plugins.php.api.tree.lexical.SyntaxToken;
import org.sonar.plugins.php.api.tree.statement.BlockTree;
import org.sonar.plugins.php.api.tree.statement.ReturnStatementTree;
import org.sonar.plugins.php.api.tree.statement.StatementTree;
import org.sonar.plugins.php.api.visitors.PHPVisitorCheck;

import static org.sonar.php.checks.utils.CheckUtils.isEmptyArrayConstructor;
import static org.sonar.php.checks.utils.SyntacticEquivalence.areSyntacticallyEquivalent;

@Rule(key = "S4144")
public class DuplicatedMethodCheck extends PHPVisitorCheck {

  private static final String ISSUE_MSG = "Update this method so that its implementation is not identical to \"%s\" on line %d.";
  private static final Function METHOD_TO_NAME = f -> ((MethodDeclarationTree) f).name();
  private static final Function FUNCTION_TO_NAME = f -> ((FunctionDeclarationTree) f).name();
  private static final int MINIMUM_NUMBER_OF_STATEMENTS = 2;

  private final Deque> methods = new LinkedList<>();
  private final List functions = new ArrayList<>();

  @Override
  public void visitCompilationUnit(CompilationUnitTree tree) {
    functions.clear();
    methods.clear();
    super.visitCompilationUnit(tree);
    checkDuplications(functions, FUNCTION_TO_NAME);
  }

  @Override
  public void visitFunctionDeclaration(FunctionDeclarationTree tree) {
    // Ignore functions with fewer than 2 statements
    if (tree.body().statements().size() >= MINIMUM_NUMBER_OF_STATEMENTS) {
      functions.add(tree);
    }
    super.visitFunctionDeclaration(tree);
  }

  @Override
  public void visitMethodDeclaration(MethodDeclarationTree tree) {
    // Ignore abstract and empty methods
    if (isDuplicateCandidate(tree)) {
      methods.peek().add(tree);
    }
    super.visitMethodDeclaration(tree);
  }

  private static boolean isDuplicateCandidate(MethodDeclarationTree tree) {
    return tree.body().is(Tree.Kind.BLOCK) && (((BlockTree) tree.body()).statements().size() >= MINIMUM_NUMBER_OF_STATEMENTS || isNonTrivialAccessor(tree));
  }

  private static boolean isNonTrivialAccessor(MethodDeclarationTree tree) {
    String methodName = tree.name().text();
    List statements = ((BlockTree) tree.body()).statements();
    boolean isAccessor = statements.size() == 1 && (methodName.startsWith("set") || methodName.startsWith("get") || methodName.startsWith("is"));
    return isAccessor && !isTrivialStatement(statements.get(0));
  }

  private static boolean isTrivialStatement(StatementTree statement) {
    if (statement.is(Tree.Kind.RETURN_STATEMENT)) {
      ExpressionTree expression = ((ReturnStatementTree) statement).expression();
      return expression != null &&
        (isEmptyArrayConstructor(expression) || expression.is(Tree.Kind.NEW_EXPRESSION, Tree.Kind.NULL_LITERAL));
    } else if (statement.is(Tree.Kind.THROW_STATEMENT)) {
      return true;
    }
    return false;
  }

  @Override
  public void visitClassDeclaration(ClassDeclarationTree tree) {
    methods.push(new ArrayList<>());
    super.visitClassDeclaration(tree);
    checkDuplications(methods.pop(), METHOD_TO_NAME);
  }

  @Override
  public void visitAnonymousClass(AnonymousClassTree tree) {
    methods.push(new ArrayList<>());
    super.visitAnonymousClass(tree);
    checkDuplications(methods.pop(), METHOD_TO_NAME);
  }

  private void checkDuplications(List functionDeclarations, Function toName) {
    Set reported = new HashSet<>();
    for (int i = 0; i < functionDeclarations.size(); i++) {
      FunctionTree func = functionDeclarations.get(i);
      SyntaxToken methodIdentifier = toName.apply(func).token();
      List methodBody = ((BlockTree) func.body()).statements();
      functionDeclarations.stream()
        .skip(i + 1L)
        // avoid reporting multiple times
        .filter(m -> !reported.contains(m))
        // avoid methods with different parameters and/or return type
        .filter(m -> haveSameParametersAndReturnType(func, m))
        // only consider method syntactically equivalent
        .filter(m -> areSyntacticallyEquivalent(methodBody.iterator(), ((BlockTree) m.body()).statements().iterator()))
        .forEach(m -> {
          context()
            .newIssue(this,
              toName.apply(m),
              String.format(ISSUE_MSG, methodIdentifier.text(), methodIdentifier.line()))
            .secondary(methodIdentifier, "original implementation");
          reported.add(m);
        });
    }
  }

  private static boolean haveSameParametersAndReturnType(FunctionTree f1, FunctionTree f2) {
    return f1.parameters().parameters().size() == f2.parameters().parameters().size() &&
      areSyntacticallyEquivalent(declaredTypeTreeOrNull(f1), declaredTypeTreeOrNull(f2));
  }

  private static DeclaredTypeTree declaredTypeTreeOrNull(FunctionTree functionTree) {
    return Optional.ofNullable(functionTree.returnTypeClause()).map(ReturnTypeClauseTree::declaredType).orElse(null);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy