/*
* Copyright 2009-2017 the original author or authors.
*
* 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 org.eclipse.jdt.groovy.search;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.FieldExpression;
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.jdt.groovy.internal.compiler.ast.GroovyTypeDeclaration;
import org.codehaus.jdt.groovy.internal.compiler.ast.JDTClassNode;
import org.codehaus.jdt.groovy.model.GroovyClassFileWorkingCopy;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.core.search.MethodDeclarationMatch;
import org.eclipse.jdt.core.search.MethodReferenceMatch;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchParticipant;
import org.eclipse.jdt.core.search.SearchRequestor;
import org.eclipse.jdt.groovy.core.util.GroovyUtils;
import org.eclipse.jdt.groovy.core.util.ReflectionUtils;
import org.eclipse.jdt.groovy.search.TypeLookupResult.TypeConfidence;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.SourceTypeBinding;
import org.eclipse.jdt.internal.core.search.matching.MethodPattern;
import org.eclipse.jdt.internal.core.util.Util;
import org.eclipse.jface.text.Position;
public class MethodReferenceSearchRequestor implements ITypeRequestor {
protected static final int MAX_PARAMS = 10;
protected final SearchRequestor requestor;
protected final SearchParticipant participant;
protected final char[] name;
protected final String declaringQualifiedName;
protected final boolean findDeclarations;
protected final boolean findReferences;
protected char[][] parameterQualifications;
protected char[][] parameterSimpleNames;
protected final int declaredParameterCount;
protected final Set<Position> acceptedPositions = new HashSet<Position>();
public MethodReferenceSearchRequestor(MethodPattern pattern, SearchRequestor requestor, SearchParticipant participant) {
this.requestor = requestor;
this.participant = participant;
name = (char[]) ReflectionUtils.getPrivateField(MethodPattern.class, "selector", pattern);
char[] arr = (char[]) ReflectionUtils.getPrivateField(MethodPattern.class, "declaringSimpleName", pattern);
String declaringSimpleName = arr == null ? "" : new String(arr);
arr = (char[]) ReflectionUtils.getPrivateField(MethodPattern.class, "declaringQualification", pattern);
String declaringQualification = ((arr == null || arr.length == 0) ? "" : (new String(arr) + "."));
declaringQualifiedName = declaringQualification + declaringSimpleName;
findDeclarations = ((Boolean) ReflectionUtils.getPrivateField(MethodPattern.class, "findDeclarations", pattern)).booleanValue();
findReferences = ((Boolean) ReflectionUtils.getPrivateField(MethodPattern.class, "findReferences", pattern)).booleanValue();
parameterQualifications = pattern.parameterQualifications;
parameterSimpleNames = pattern.parameterSimpleNames;
declaredParameterCount = pattern.parameterSimpleNames == null ? 0 : pattern.parameterSimpleNames.length;
}
public VisitStatus acceptASTNode(ASTNode node, TypeLookupResult result, IJavaElement enclosingElement) {
boolean doCheck = false;
boolean isDeclaration = false;
boolean isConstructorCall = false; // FIXADE hmmm...not capturing constructor calls here.
int start = 0;
int end = 0;
if (result.declaringType == null) {
// GRECLIPSE-1180 probably a literal of some kind
return VisitStatus.CONTINUE;
}
if (node instanceof ConstantExpression) {
String cName = ((ConstantExpression) node).getText();
if (cName != null && CharOperation.equals(name, cName.toCharArray())) {
start = node.getStart();
end = node.getEnd();
doCheck = end > 0; // avoid synthetic references
}
} else if (node instanceof FieldExpression) {
if (CharOperation.equals(name, ((FieldExpression) node).getFieldName().toCharArray())) {
start = node.getStart();
end = node.getEnd();
doCheck = end > 0; // avoid synthetic references
}
} else if (node instanceof MethodNode) {
MethodNode mnode = (MethodNode) node;
if (CharOperation.equals(name, mnode.getName().toCharArray())) {
isDeclaration = true;
start = mnode.getNameStart();
end = mnode.getNameEnd() + 1; // arrrgh...why +1?
doCheck = true;
}
} else if (node instanceof VariableExpression) {
VariableExpression vnode = (VariableExpression) node;
if (CharOperation.equals(name, vnode.getName().toCharArray())) {
start = vnode.getStart();
end = start + vnode.getName().length();
doCheck = true;
}
} else if (node instanceof StaticMethodCallExpression) {
StaticMethodCallExpression smnode = (StaticMethodCallExpression) node;
if (CharOperation.equals(name, smnode.getMethod().toCharArray())) {
start = smnode.getStart();
end = start + name.length;
doCheck = true;
}
}
// at this point, if doCheck is true, then we know that the method name matches
if (doCheck && end > 0) {
// don't want to double accept nodes. This could happen with field and object initializers can get pushed into multiple
// constructors
Position position = new Position(start, end - start);
if (!acceptedPositions.contains(position)) {
int numberOfParameters = findNumberOfParameters(node, result);
boolean isCompleteMatch = nameAndArgsMatch(GroovyUtils.getBaseType(result.declaringType), numberOfParameters);
if (isCompleteMatch) {
IJavaElement realElement = enclosingElement.getOpenable() instanceof GroovyClassFileWorkingCopy ? ((GroovyClassFileWorkingCopy) enclosingElement
.getOpenable()).convertToBinary(enclosingElement) : enclosingElement;
SearchMatch match = null;
if (isDeclaration && findDeclarations) {
match = new MethodDeclarationMatch(realElement, getAccuracy(result.confidence, isCompleteMatch), start, end
- start, participant, realElement.getResource());
} else if (!isDeclaration && findReferences) {
match = new MethodReferenceMatch(realElement, getAccuracy(result.confidence, isCompleteMatch), start, end
- start, isConstructorCall, false, false, false, participant, realElement.getResource());
}
if (match != null) {
try {
requestor.acceptSearchMatch(match);
acceptedPositions.add(position);
} catch (CoreException e) {
Util.log(
e,
"Error reporting search match inside of " + realElement + " in resource "
+ realElement.getResource());
}
}
}
}
}
return VisitStatus.CONTINUE;
}
/**
* @return finds the number of parameters in the method reference/declaration currently being analyzed.
*/
private int findNumberOfParameters(ASTNode node, TypeLookupResult result) {
return node instanceof MethodNode && ((MethodNode) node).getParameters() != null ? ((MethodNode) node).getParameters().length
: Math.max(0, result.scope.getMethodCallNumberOfArguments());
}
private Map<ClassNode, Boolean> cachedDeclaringNameMatches = new HashMap<ClassNode, Boolean>();
private Map<ClassNode, boolean[]> cachedParameterCounts = new HashMap<ClassNode, boolean[]>();
/**
* Recursively checks the hierarchy for matching names
*/
private boolean nameAndArgsMatch(ClassNode declaringType, int currentCallCount) {
return matchOnName(declaringType) && matchOnNumberOfParameters(declaringType, currentCallCount);
}
private boolean matchOnName(ClassNode declaringType) {
if (declaringType == null) {
return false;
}
String declaringTypeName = declaringType.getName();
if (// since local variables have a declaring type of object, we don't accidentally want to return them as a match
(declaringTypeName.equals("java.lang.Object") && declaringType.getDeclaredMethods(String.valueOf(name)).size() == 0)) {
return false;
}
if (declaringQualifiedName == null || declaringQualifiedName.equals("")) {
// no type specified, accept all
return true;
}
declaringTypeName = declaringTypeName.replace('$', '.');
Boolean maybeMatch = cachedDeclaringNameMatches.get(declaringType);
if (maybeMatch != null) {
return maybeMatch;
}
if (declaringTypeName.equals(declaringQualifiedName)) {
cachedDeclaringNameMatches.put(declaringType, true);
// the name matches, now what about number of arguments?
return true;
} else {
// check the supers
maybeMatch = matchOnName(declaringType.getSuperClass());
if (!maybeMatch) {
for (ClassNode iface : declaringType.getInterfaces()) {
maybeMatch = matchOnName(iface);
if (maybeMatch) {
break;
}
}
}
cachedDeclaringNameMatches.put(declaringType, maybeMatch);
return maybeMatch;
}
}
/**
* When matching method references and declarations, we can't actually match on parameter types. Instead, we match on the number
* of parameterrs and assume that it is slightly more preceise than just matching on name.
*
* The heuristic that is used in this method is this:
* <ol>
* <li>The search pattern expects 'n' parameters
* <li>the current node has 'm' arguments.
* <li>if the m == n, then there is a precise match.
* <li>if not, look at all methods in current type with same name.
* <li>if there is a method in the current type with the same number of arguments, then assume the current node matches that
* other method, and there is no match.
* <li>If there are no existing methods with same number of parameters, then assume that current method call is an alternative
* way of calling the method and return a match
* </ol>
*
* @param declaringType
* @param currentCallCount
* @return true if there is a precise match between number of arguments and numner of parameters. false if there exists a
* different method with same number of arguments in current type, or true otherwise
*/
private boolean matchOnNumberOfParameters(ClassNode declaringType, int currentCallCount) {
boolean methodParamNumberMatch;
if (currentCallCount == declaredParameterCount) {
// precise match
methodParamNumberMatch = true;
} else {
boolean[] foundParameterNumbers = cachedParameterCounts.get(declaringType);
if (foundParameterNumbers == null) {
foundParameterNumbers = new boolean[MAX_PARAMS + 1];
gatherParameters(declaringType, foundParameterNumbers);
cachedParameterCounts.put(declaringType, foundParameterNumbers);
}
// now, if we find a method that has the same number of parameters in the call,
// then assume the call is for this target method (and therefore there is no match)
methodParamNumberMatch = !foundParameterNumbers[Math.min(MAX_PARAMS, currentCallCount)];
}
return methodParamNumberMatch;
}
private void gatherParameters(ClassNode declaringType, boolean[] foundParameterNumbers) {
if (declaringType == null) {
return;
}
declaringType = findWrappedNode(declaringType.redirect());
List<MethodNode> methods = declaringType.getMethods(String.valueOf(name));
for (MethodNode method : methods) {
// GRECLIPSE-1233
// ensure default parameters are ignored
method = method.getOriginal();
foundParameterNumbers[Math.min(method.getParameters().length, MAX_PARAMS)] = true;
}
gatherParameters(declaringType.getSuperClass(), foundParameterNumbers);
for (ClassNode iface : declaringType.getInterfaces()) {
gatherParameters(iface, foundParameterNumbers);
}
}
/**
* Attempt to convert from a {@link JDTClassNode} to a {@link ClassNode} in order to check default parameters
*/
private ClassNode findWrappedNode(ClassNode declaringType) {
ClassNode wrappedNode = null;
if (declaringType instanceof JDTClassNode) {
ReferenceBinding binding = ((JDTClassNode) declaringType).getJdtBinding();
if (binding instanceof SourceTypeBinding) {
SourceTypeBinding sourceTypeBinding = (SourceTypeBinding) binding;
if (sourceTypeBinding.scope != null) {
TypeDeclaration typeDeclaration = sourceTypeBinding.scope.referenceContext;
if (typeDeclaration instanceof GroovyTypeDeclaration) {
GroovyTypeDeclaration groovyTypeDeclaration = (GroovyTypeDeclaration) typeDeclaration;
wrappedNode = groovyTypeDeclaration.getClassNode();
}
}
}
}
return wrappedNode == null ? declaringType : wrappedNode;
}
private int getAccuracy(TypeConfidence confidence, boolean isCompleteMatch) {
if (shouldAlwaysBeAccurate()) {
return SearchMatch.A_ACCURATE;
}
if (!isCompleteMatch) {
return SearchMatch.A_INACCURATE;
}
switch (confidence) {
case EXACT:
return SearchMatch.A_ACCURATE;
default:
return SearchMatch.A_INACCURATE;
}
}
/**
* check to see if this requestor has something to do with refactoring, if so, we always want an accurate match otherwise we get
* complaints in the refactoring wizard of "possible matches"
*/
private boolean shouldAlwaysBeAccurate() {
return requestor.getClass().getPackage().getName().indexOf("refactoring") != -1;
}
}