/*
* 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.HashSet;
import java.util.Set;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ConstructorNode;
import org.codehaus.groovy.ast.ImportNode;
import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.jdt.groovy.model.GroovyClassFileWorkingCopy;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.compiler.CharOperation;
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.core.search.TypeReferenceMatch;
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.core.CompilationUnit;
import org.eclipse.jdt.internal.core.search.matching.DeclarationOfReferencedTypesPattern;
import org.eclipse.jdt.internal.core.search.matching.JavaSearchPattern;
import org.eclipse.jdt.internal.core.search.matching.TypeReferencePattern;
import org.eclipse.jdt.internal.core.util.Util;
import org.eclipse.jface.text.Position;
/**
* @author Andrew Eisenberg
* @created Aug 29, 2009
*/
public class TypeReferenceSearchRequestor implements ITypeRequestor {
private static final String DOT = ".";
private final SearchRequestor requestor;
private final SearchParticipant participant;
private final char[] qualificationPattern;
private final char[] namePattern;
private final boolean isCaseSensitive;
private final boolean isCamelCase;
private final boolean findDeclaration;
private final Set<Position> acceptedPositions = new HashSet<Position>();
private char[] cachedContents;
public TypeReferenceSearchRequestor(TypeReferencePattern pattern, SearchRequestor requestor, SearchParticipant participant) {
this.requestor = requestor;
this.participant = participant;
this.isCaseSensitive = ((Boolean) ReflectionUtils.getPrivateField(JavaSearchPattern.class, "isCaseSensitive", pattern))
.booleanValue();
this.namePattern = extractArray(pattern, "simpleName");
this.qualificationPattern = extractArray(pattern, "qualification");
this.isCamelCase = ((Boolean) ReflectionUtils.getPrivateField(JavaSearchPattern.class, "isCamelCase", pattern))
.booleanValue();
this.findDeclaration = pattern instanceof DeclarationOfReferencedTypesPattern;
}
protected char[] extractArray(TypeReferencePattern pattern, String fieldName) {
char[] arr;
arr = (char[]) ReflectionUtils.getPrivateField(TypeReferencePattern.class, fieldName, pattern);
if (!isCaseSensitive) {
arr = CharOperation.toLowerCase(arr);
}
return arr;
}
public VisitStatus acceptASTNode(ASTNode node, TypeLookupResult result, IJavaElement enclosingElement) {
// don't do constructor calls. They are found through the class node inside of it
if (node instanceof ClassExpression || node instanceof ClassNode || node instanceof ImportNode
|| node instanceof AnnotationNode /* || node instanceof ConstructorNode */) {
// the type variable may not have correct source location
ClassNode type;
if (node instanceof ConstructorNode) {
type = ((ConstructorNode) node).getDeclaringClass();
} else if (node instanceof AnnotationNode) {
type = ((AnnotationNode) node).getClassNode();
} else {
type = result.type;
}
if (node instanceof ClassExpression && type.equals(VariableScope.CLASS_CLASS_NODE)) {
// special case...there is a Foo.class expression.
// the difference between Foo.class and Foo does not appear in the AST.
// The type of the expression is considered to be Class, but we still need to
// look for a reference for Foo
type = ((ClassExpression) node).getType();
}
if (type != null) {
type = GroovyUtils.getBaseType(type);
if (qualifiedNameMatches(type) && hasValidSourceLocation(node)) {
int start = -1;
int end = -1;
boolean startEndFound = false;
if (node instanceof ImportNode) {
if (((ImportNode) node).getType() == null) {
// if the import node's type is not null, then the type will be visited later anyway.
// so don't visit it here.
end = node.getEnd();
start = node.getStart();
}
} else if (node instanceof ClassExpression) {
end = node.getEnd();
start = node.getStart();
} else if (node instanceof ClassNode) {
ClassNode classNode = (ClassNode) node;
if (classNode.getNameEnd() > 0) {
// we are actually dealing with a declaration
// start = classNode.getNameStart();
// end = classNode.getNameEnd() + 1;
// startEndFound = true;
} else if (classNode.redirect() == classNode) {
// this is a script declaration... ignore
start = end = -1;
startEndFound = true;
} else {
// ensure classNode has proper source location
classNode = maybeGetComponentType(classNode);
end = classNode.getEnd();
start = classNode.getStart();
}
} else if (node instanceof ConstructorNode) {
start = ((ConstructorNode) node).getNameStart();
end = ((ConstructorNode) node).getNameEnd() + 1;
if (start == 0 && end == 1) {
// synthetic constructor from script
start = end = -1;
startEndFound = true;
}
} else if (node instanceof AnnotationNode) {
type = ((AnnotationNode) node).getClassNode();
end = type.getEnd();
start = type.getStart();
}
if (!startEndFound) {
// we have a little more work to do before finding the real
// offset in the text
StartEnd startEnd = getMatchLocation(type, enclosingElement, start, end);
if (startEnd != null) {
start = startEnd.start;
end = startEnd.end;
} else {
// match really wasn't found
start = end = -1;
}
}
if (start >= 0 && 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)) {
IJavaElement realElement = enclosingElement.getOpenable() instanceof GroovyClassFileWorkingCopy ? ((GroovyClassFileWorkingCopy) enclosingElement
.getOpenable()).convertToBinary(enclosingElement) : enclosingElement;
try {
requestor.acceptSearchMatch(createMatch(result, realElement, start, end));
acceptedPositions.add(position);
} catch (CoreException e) {
Util.log(e, "Error accepting search match for " + realElement);
}
}
}
}
}
}
return VisitStatus.CONTINUE;
}
protected TypeReferenceMatch createMatch(TypeLookupResult result, IJavaElement enclosingElement, int start, int end) {
IJavaElement element;
if (findDeclaration) {
// don't use the enclosing element, but rather use the declaration of the type
try {
ClassNode type = result.type;
while (type.getComponentType() != null) {
type = type.getComponentType();
}
element = enclosingElement.getJavaProject().findType(type.getName().replace('$', '.'), new NullProgressMonitor());
if (element == null) {
element = enclosingElement;
}
} catch (JavaModelException e) {
Util.log(e);
element = enclosingElement;
}
} else {
element = enclosingElement;
}
return new TypeReferenceMatch(element, getAccuracy(result.confidence), start, end - start, false, participant,
element.getResource());
}
/**
* @param node
* @return
*/
private boolean hasValidSourceLocation(ASTNode node) {
// find the correct ast node that has source locations on it
// sometimes array nodes do not have source locations, so get around that here.
ASTNode astNodeWithSourceLocation;
if (node instanceof ClassNode) {
astNodeWithSourceLocation = maybeGetComponentType((ClassNode) node);
} else {
astNodeWithSourceLocation = node;
}
return astNodeWithSourceLocation.getEnd() > 0;
}
/**
* sometimes the underlying component type contains the source location, not the array type
*
* @param orig the original class node
* @return the component type if that type contains source information, otherwise return the original
*/
private ClassNode maybeGetComponentType(ClassNode orig) {
if (orig.getComponentType() != null) {
ClassNode componentType = orig.getComponentType();
if (componentType.getColumnNumber() != -1) {
return componentType;
}
}
return orig;
}
private String[] extractNameAndQualification(ClassNode type) {
// qualification includes the full package name, plus all enclosing types with '.' separators
String qualification = type.getPackageName();
if (qualification == null) {
qualification = "";
}
String semiQualified = type.getNameWithoutPackage();
String simple;
int lastDollar = semiQualified.lastIndexOf('$');
if (lastDollar > 0) {
simple = semiQualified.substring(lastDollar + 1);
semiQualified = semiQualified.replace('$', '.').substring(0, lastDollar);
if (qualification.length() == 0) {
qualification = semiQualified;
} else {
qualification += DOT + semiQualified;
}
} else {
simple = semiQualified;
}
return new String[] { qualification, simple };
}
private boolean qualifiedNameMatches(ClassNode type) {
String[] nameAndQualification = extractNameAndQualification(type);
String name, qualification;
qualification = nameAndQualification[0];
name = nameAndQualification[1];
if (!isCaseSensitive) {
name = name.toLowerCase();
qualification = qualification.toLowerCase();
}
boolean match = true;
if (namePattern != null) { // if pattern is null, then this means '*'
if (isCamelCase) {
match = CharOperation.camelCaseMatch(namePattern, name.toCharArray());
} else {
match = CharOperation.equals(namePattern, name.toCharArray());
}
}
if (match && qualificationPattern != null) {
if (isCamelCase) { // if pattern is null, then this means '*'
match = CharOperation.camelCaseMatch(qualificationPattern, qualification.toCharArray());
} else {
match = CharOperation.equals(qualificationPattern, qualification.toCharArray());
}
}
return match;
}
private class StartEnd {
StartEnd(int start, int end) {
this.start = start;
this.end = end;
}
final int start;
final int end;
}
/**
* THe problem that this method gets around is that we can't tell exactly what the text is and exactly where or if there is a
* match in the source location. For example, in the text, the type can be fully qualified, or array, or coming from an alias,
* or a bound type parameter. On top of that, some source locations are off by one. All these will have location relative to the
* offset provided in the start and end fields of the {@link ClassNode}.
*
* @param node the class node to find in the source
* @param elt the element used to find the text
* @return the start and end offsets of the actual match, or null if no match exists.
*/
private StartEnd getMatchLocation(ClassNode node, IJavaElement elt, int maybeStart, int maybeEnd) {
CompilationUnit unit = (CompilationUnit) elt.getAncestor(IJavaElement.COMPILATION_UNIT);
if (unit != null && cachedContents == null) {
cachedContents = unit.getContents();
}
if (cachedContents != null) {
int nameLength = maybeEnd - maybeStart;
int start = -1;
int end = -1;
String name = node.getName();
// handle inner types here
int dollarIndex = name.lastIndexOf('$');
name = name.substring(dollarIndex + 1);
if (name.length() <= nameLength) {
// might be a qualified name
start = CharOperation.indexOf(name.toCharArray(), cachedContents, true, maybeStart, maybeEnd + 1);
end = start + name.length();
}
if (start == -1) {
// check for simple name
String nameWithoutPackage = node.getNameWithoutPackage();
start = CharOperation.indexOf(nameWithoutPackage.toCharArray(), cachedContents, true, maybeStart, maybeEnd + 1);
end = start + nameWithoutPackage.length();
}
if (start == -1) {
// can be an aliased type name
return null;
} else {
return new StartEnd(start, end);
}
}
// shuoldn't happen, but may be a binary match
return new StartEnd(node.getStart(), node.getEnd());
}
/**
* 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;
}
private int getAccuracy(TypeConfidence confidence) {
if (shouldAlwaysBeAccurate()) {
return SearchMatch.A_ACCURATE;
}
switch (confidence) {
case EXACT:
return SearchMatch.A_ACCURATE;
default:
return SearchMatch.A_INACCURATE;
}
}
}