/*
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.errorprone.bugpatterns.argumentselectiondefects;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.VisitorState;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import java.util.List;
import java.util.Objects;
/**
* Detect whether our suggestion would create a method call which duplicates another one in this
* block.
*
* @author andrewrice@google.com (Andrew Rice)
*/
class CreatesDuplicateCallHeuristic implements Heuristic {
/**
* Returns true if there are no other calls to this method which already have an actual parameter
* in the position we are moving this one too.
*/
@Override
public boolean isAcceptableChange(
Changes changes, Tree node, MethodSymbol symbol, VisitorState state) {
return findArgumentsForOtherInstances(symbol, node, state)
.stream()
.allMatch(arguments -> !anyArgumentsMatch(changes.changedPairs(), arguments));
}
/**
* Return true if the replacement name is equal to the argument name for any replacement position.
*/
private static boolean anyArgumentsMatch(
List<ParameterPair> changedPairs, List<Parameter> arguments) {
return changedPairs
.stream()
.anyMatch(
change ->
Objects.equals(
change.actual().text(), arguments.get(change.formal().index()).text()));
}
/**
* Find all the other calls to {@code calledMethod} within the method (or class) which enclosed
* the original call.
*
* <p>We are interested in two different cases: 1) where there are other calls to the method we
* are calling; 2) declarations of the method we are calling (this catches the case when there is
* a recursive call with the arguments correctly swapped).
*
* @param calledMethod is the method call we are analysing for swaps
* @param currentNode is the tree node the method call occurred at
* @param state is the current visitor state
* @return a list containing argument lists for each call found
*/
private static List<List<Parameter>> findArgumentsForOtherInstances(
MethodSymbol calledMethod, Tree currentNode, VisitorState state) {
Tree enclosingNode = ASTHelpers.findEnclosingNode(state.getPath(), MethodTree.class);
if (enclosingNode == null) {
enclosingNode = ASTHelpers.findEnclosingNode(state.getPath(), ClassTree.class);
}
if (enclosingNode == null) {
return ImmutableList.of();
}
ImmutableList.Builder<List<Parameter>> resultBuilder = ImmutableList.builder();
new TreeScanner<Void, Void>() {
@Override
public Void visitMethodInvocation(MethodInvocationTree methodInvocationTree, Void aVoid) {
addToResult(ASTHelpers.getSymbol(methodInvocationTree), methodInvocationTree);
return super.visitMethodInvocation(methodInvocationTree, aVoid);
}
@Override
public Void visitNewClass(NewClassTree newClassTree, Void aVoid) {
addToResult(ASTHelpers.getSymbol(newClassTree), newClassTree);
return super.visitNewClass(newClassTree, aVoid);
}
@Override
public Void visitMethod(MethodTree methodTree, Void aVoid) {
MethodSymbol methodSymbol = ASTHelpers.getSymbol(methodTree);
if (methodSymbol != null) {
// if the method declared here is the one we are calling then add it
addToResult(methodSymbol, methodTree);
// if any supermethod of the one declared here is the one we are calling then add it
for (MethodSymbol superSymbol :
ASTHelpers.findSuperMethods(methodSymbol, state.getTypes())) {
addToResult(superSymbol, methodTree);
}
}
return super.visitMethod(methodTree, aVoid);
}
private void addToResult(MethodSymbol foundSymbol, Tree tree) {
if (foundSymbol != null
&& Objects.equals(calledMethod, foundSymbol)
&& !currentNode.equals(tree)) {
resultBuilder.add(createParameterList(tree));
}
}
private ImmutableList<Parameter> createParameterList(Tree tree) {
if (tree instanceof MethodInvocationTree) {
return Parameter.createListFromExpressionTrees(
((MethodInvocationTree) tree).getArguments());
}
if (tree instanceof NewClassTree) {
return Parameter.createListFromExpressionTrees(((NewClassTree) tree).getArguments());
}
if (tree instanceof MethodTree) {
return Parameter.createListFromVariableTrees(((MethodTree) tree).getParameters());
}
return ImmutableList.of();
}
}.scan(enclosingNode, null);
return resultBuilder.build();
}
}