/*
* 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.codehaus.jdt.groovy.internal.compiler.ast;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import groovy.lang.GroovyClassLoader;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.ResolveVisitor;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.jdt.groovy.internal.compiler.ast.GroovyParser.GrapeAwareGroovyClassLoader;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.groovy.core.util.ReflectionUtils;
import org.eclipse.jdt.internal.compiler.ast.SingleTypeReference;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.env.AccessRestriction;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeIds;
/**
* An extension to the standard groovy ResolveVisitor that can ask JDT for types when groovy cannot find them. A groovy project in
* Eclipse is typically configured with very limited knowledge of its dependencies so most lookups are through JDT.
*
* Resolver lifecycle:<br>
* The JDTResolver is created at the same time as the (Groovy) CompilationUnit. The CompilationUnit knows about all the code that is
* to be compiled together. The resolver maintains a cache from Binding to JDTClassNode and the cache contents have the same
* lifetime as the JDTResolver. The resolver does type lookups through the currently active scope - the active scope is set when the
* method 'commencingResolution()' is called. This is called by the superclass (ResolveVisitor) when it is about to start resolving
* every reference in a type.
*
* @author Andy Clement
*/
public class JDTResolver extends ResolveVisitor {
/** Any type name that is equal to or shorter than this is likely to be a primitive type. */
private static final int BOOLEAN_LENGTH = "boolean".length();
/** Arbitrary selection of common types. */
private static final Map<String, ClassNode> COMMON_TYPES;
static {
Map<String, ClassNode> commonTypes = new HashMap<String, ClassNode>();
commonTypes.put("java.lang.Class", ClassHelper.CLASS_Type);
commonTypes.put("java.lang.Object", ClassHelper.OBJECT_TYPE);
commonTypes.put("java.lang.String", ClassHelper.STRING_TYPE);
commonTypes.put("java.lang.Boolean", ClassHelper.Boolean_TYPE);
commonTypes.put("java.lang.Byte", ClassHelper.Byte_TYPE);
commonTypes.put("java.lang.Character", ClassHelper.Character_TYPE);
commonTypes.put("java.lang.Double", ClassHelper.Double_TYPE);
commonTypes.put("java.lang.Float", ClassHelper.Float_TYPE);
commonTypes.put("java.lang.Integer", ClassHelper.Integer_TYPE);
commonTypes.put("java.lang.Long", ClassHelper.Long_TYPE);
commonTypes.put("java.lang.Short", ClassHelper.Short_TYPE);
commonTypes.put("boolean", ClassHelper.boolean_TYPE);
commonTypes.put("byte", ClassHelper.byte_TYPE);
commonTypes.put("char", ClassHelper.char_TYPE);
commonTypes.put("double", ClassHelper.double_TYPE);
commonTypes.put("float", ClassHelper.float_TYPE);
commonTypes.put("int", ClassHelper.int_TYPE);
commonTypes.put("long", ClassHelper.long_TYPE);
commonTypes.put("short", ClassHelper.short_TYPE);
COMMON_TYPES = Collections.unmodifiableMap(commonTypes);
}
private static final boolean DEBUG = false;
private void log(String string) {
System.err.printf("JDTResolver@%x[%d]: %s%n", System.identityHashCode(this), Thread.currentThread().getId(), string);
}
private void log(String string, ClassNode type, boolean foundit) {
log(string + " " + type.getName() + "? " + foundit);
}
// allow test cases to quiz a resolver
public static boolean recordInstances = false;
public static List<JDTResolver> instances = null;
public static JDTClassNode getCachedNode(String name) {
for (JDTResolver instance : instances) {
JDTClassNode node = getCachedNode(instance, name);
if (node != null) return node;
}
return null;
}
public static JDTClassNode getCachedNode(JDTResolver instance, String name) {
for (JDTClassNode nodeFromCache : instance.nodeCache.values()) {
if (name.equals(String.valueOf(nodeFromCache.jdtBinding.readableName()))) {
return nodeFromCache;
}
}
return null;
}
// Type references are resolved through the 'activeScope'. This ensures visibility rules are obeyed - just because a
// type exists does not mean it is visible to some other type and scope lookups verify this.
protected GroovyCompilationUnitScope activeScope = null;
// map of scopes in which resolution can happen
private Map<ClassNode, GroovyTypeDeclaration> scopes = new HashMap<ClassNode, GroovyTypeDeclaration>();
// By recording what is currently in progress in terms of creation, we avoid recursive problems (like Enum<E extends Enum<E>>)
private Map<TypeBinding, JDTClassNode> inProgress = new IdentityHashMap<TypeBinding, JDTClassNode>();
// Cache from bindings to JDTClassNodes to avoid unnecessary JDTClassNode creation
private Map<TypeBinding, JDTClassNode> nodeCache = new IdentityHashMap<TypeBinding, JDTClassNode>();
private Set<ClassNode> resolvedClassNodes = new HashSet<ClassNode>();
public JDTResolver(CompilationUnit groovyCompilationUnit) {
super(groovyCompilationUnit);
if (recordInstances) {
if (instances == null) {
instances = new ArrayList<JDTResolver>();
}
instances.add(this);
}
}
public void cleanUp() {
inProgress.clear();
//nodeCache.clear();
}
@Override
protected boolean resolveFromModule(ClassNode type, boolean testModuleImports) {
boolean foundit = super.resolveFromModule(type, testModuleImports);
recordDependency(type.getName());
if (DEBUG) {
log("resolveFromModule", type, foundit);
}
if (foundit) {
if (type.redirect() instanceof JDTClassNode && ((JDTClassNode) type.redirect()).getJdtBinding().hasRestrictedAccess()) {
TypeBinding binding = ((JDTClassNode) type.redirect()).getJdtBinding();
AccessRestriction restriction = activeScope.environment().getAccessRestriction(binding.erasure());
if (restriction != null) {
SingleTypeReference ref = new SingleTypeReference(type.getNameWithoutPackage().toCharArray(), ((long) type.getStart() << 32 | (long) type.getEnd() - 1));
activeScope.problemReporter().forbiddenReference(binding, ref, restriction.classpathEntryType, restriction.classpathEntryName, restriction.getProblemId());
}
}
}
return foundit;
}
@Override
protected boolean resolveFromCompileUnit(ClassNode type) {
boolean foundit = super.resolveFromCompileUnit(type);
recordDependency(type.getName());
if (DEBUG) {
log("resolveFromCompileUnit", type, foundit);
}
if (foundit) {
return true;
}
if (activeScope != null) {
// Ask JDT for a source file, visible from this scope
ClassNode node = activeScope.lookupClassNodeForSource(type.getName(), this);
if (DEBUG) {
log("resolveFromCompileUnit (jdt) ", type, node != null);
}
if (node != null) {
type.setRedirect(node);
return true;
}
}
return false;
}
@Override
protected boolean resolveFromDefaultImports(ClassNode type, boolean testDefaultImports) {
boolean foundit = super.resolveFromDefaultImports(type, testDefaultImports);
if (activeScope != null) {
// TODO need to refactor (duplicated in GroovyCompilationUnitScope)
boolean b = testDefaultImports & !type.hasPackageName();
// we do not resolve a vanilla name starting with a lower case letter
// try to resolve against adefault import, because we know that the
// default packages do not contain classes like these
b &= !(type instanceof LowerCaseClass);
if (b) {
String extraImports = activeScope.compilerOptions().groovyExtraImports;
if (extraImports != null) {
try {
String filename = String.valueOf(activeScope.referenceContext.getFileName());
// may be something to do
StringTokenizer st = new StringTokenizer(extraImports, ";");
// Form would be 'com.foo.*,com.bar.MyType;.gradle=com.this.*,com.foo.Type"
// If there is no qualifying suffix it applies to all types
while (st.hasMoreTokens()) {
String onesuffix = st.nextToken();
int equals = onesuffix.indexOf('=');
@SuppressWarnings("unused")
boolean shouldApply = false;
String imports = null;
if (equals == -1) {
// definetly applies
shouldApply = true;
imports = onesuffix;
} else {
// need to check the suffix
String suffix = onesuffix.substring(0, equals);
shouldApply = filename.endsWith(suffix);
imports = onesuffix.substring(equals + 1);
}
StringTokenizer st2 = new StringTokenizer(imports, ",");
while (st2.hasMoreTokens()) {
String nextElement = st2.nextToken();
// One of two forms: a.b.c.* or a.b.c.Type
if (nextElement.endsWith(".*")) {
String withoutStar = nextElement.substring(0, nextElement.length() - 1);
ConstructedClassWithPackage tmp = new ConstructedClassWithPackage(withoutStar, type.getName());
if (resolve(tmp, false, false, false)) {
type.setRedirect(tmp.redirect());
return true;
}
} else {
String importedTypeName = nextElement;
int asIndex = importedTypeName.indexOf(" as ");
String asName = null;
if (asIndex != -1) {
asName = importedTypeName.substring(asIndex + 4).trim();
importedTypeName = importedTypeName.substring(0, asIndex).trim();
}
String typeName = type.getName();
if (importedTypeName.endsWith(typeName) || typeName.equals(asName)) {
int lastdot = importedTypeName.lastIndexOf('.');
String importTypeNameChopped = importedTypeName.substring(0, lastdot + 1);
if (typeName.equals(asName)) {
typeName = importedTypeName.substring(lastdot + 1);
}
ConstructedClassWithPackage tmp = new ConstructedClassWithPackage(importTypeNameChopped,
typeName);
if (resolve(tmp, false, false, false)) {
type.setRedirect(tmp.redirect());
return true;
}
}
}
}
}
} catch (Exception e) {
new RuntimeException("Problem processing extraImports: " + extraImports, e).printStackTrace();
}
}
}
}
recordDependency(type.getName());
if (DEBUG) {
log("resolveFromDefaultImports", type, foundit);
}
return foundit;
}
@Override
protected boolean resolveFromStaticInnerClasses(ClassNode type, boolean testStaticInnerClasses) {
boolean foundit = super.resolveFromStaticInnerClasses(type, testStaticInnerClasses);
recordDependency(type.getName());
if (DEBUG) {
log("resolveFromStaticInnerClasses", type, foundit);
}
return foundit;
// FIXASC (M3) anything special for inner types?
}
@Override
protected boolean resolveFromClassCache(ClassNode type) {
return false;
}
protected boolean resolveToOuter(ClassNode type) {
return resolveToClass(type);
}
protected boolean resolveToClass(ClassNode type) {
ClassNode node;
if (activeScope != null) {
node = activeScope.lookupClassNodeForBinary(type.getName(), this);
if (DEBUG) {
log("resolveToClass (jdt)", type, node != null);
}
if (node != null) {
type.setRedirect(node);
return true;
}
}
// Rudimentary grab support - if the compilation unit has our special classloader and a
// grab has occurred, try and find the class through it
GroovyClassLoader loader = compilationUnit.getClassLoader();
if (loader instanceof GrapeAwareGroovyClassLoader) {
GrapeAwareGroovyClassLoader gagc = (GrapeAwareGroovyClassLoader) loader;
if (gagc.grabbed) {
Class<?> cls;
try {
cls = loader.loadClass(type.getName(), false, true);
} catch (ClassNotFoundException cnfe) {
return false;
} catch (CompilationFailedException cfe) {
return false;
}
if (cls == null) {
return false;
}
node = ClassHelper.make(cls);
type.setRedirect(node);
return true;
}
}
return false;
}
@Override
protected boolean resolveToScript(ClassNode type) {
return false;
}
// Records a list of type names that aren't resolvable for the current resolution (unresolvables is cleared in
// finishedResolution()). This means we won't constantly attempt to lookup something that is not found through the same routes
// over and over (GRECLIPSE-870)
private Set<String> unresolvables = new HashSet<String>();
@Override
protected boolean resolve(ClassNode type, boolean testModuleImports, boolean testDefaultImports, boolean testStaticInnerClasses) {
String name = type.getName();
// save time by being selective about whether to consult the commonRedirectMap
if (name.charAt(0) == 'j' || name.length() <= BOOLEAN_LENGTH) {
ClassNode commonRedirect = COMMON_TYPES.get(type.getName());
if (commonRedirect != null) {
type.setRedirect(commonRedirect);
return true;
}
}
if (unresolvables.contains(name)) {
return false;
} else {
boolean b = super.resolve(type, testModuleImports, testDefaultImports, testStaticInnerClasses);
if (!b) {
unresolvables.add(name);
}
return b;
}
}
public ClassNode resolve(String qualifiedName) {
ClassNode type = ClassHelper.makeWithoutCaching(qualifiedName);
if (super.resolve(type)) {
return type.redirect();
} else {
return ClassHelper.DYNAMIC_TYPE;
}
}
// avoiding an inner resolve is dangerous.
// leave a back door here to turn it back on
// if no one complains, then safe to remove
private static boolean doInnerResolve = Boolean.getBoolean("greclipse.doInnerResolve");
@Override
protected boolean resolveToInnerEnum(ClassNode type) {
if (doInnerResolve) {
return super.resolveToInnerEnum(type);
}
// inner classes are resolved by JDT, so
// if we get here then the inner class does not exist
return false;
}
@Override
protected boolean resolveToInner(ClassNode type) {
if (doInnerResolve) {
return super.resolveToInner(type);
}
// inner classes are resolved by JDT, so
// if we get here then the inner class does not exist
return false;
}
// FIXASC callers could check if it is a 'funky' type before always recording a depedency
// by 'funky' I mean that the type was constructed just to try something (org.foo.bar.java$lang$Wibble doesn't want recording!)
private void recordDependency(String typename) {
if (activeScope != null) {
if (typename.indexOf('.') != -1) {
activeScope.recordQualifiedReference(CharOperation.splitOn('.', typename.toCharArray()));
} else {
activeScope.recordSimpleReference(typename.toCharArray());
}
}
}
/**
* Converts a JDT TypeBinding to a Groovy ClassNode.
*/
protected ClassNode convertToClassNode(TypeBinding jdtBinding) {
JDTClassNode existingNode = checkForExisting(jdtBinding);
if (existingNode != null) {
if (DEBUG) {
log("Using cached JDTClassNode for binding " + toString(jdtBinding));
}
return existingNode;
}
if (DEBUG) {
if (jdtBinding.id != TypeIds.T_void /*&& !jdtBinding.isPrimitiveOrBoxedPrimitiveType()*/) {
log("createJDTClassNode: Building new JDTClassNode for binding " + toString(jdtBinding));
}
}
return createJDTClassNode(jdtBinding);
}
private JDTClassNode checkForExisting(TypeBinding jdtBinding) {
JDTClassNode node = inProgress.get(jdtBinding);
if (node == null) {
node = nodeCache.get(jdtBinding);
}
if (node != null) {
assert Arrays.equals(jdtBinding.readableName(), node.jdtBinding.readableName());
}
return node;
}
/**
* Creates a Groovy ClassNode that represents the JDT TypeBinding. Steps
* include building the basic structure, marking node as 'in progress' and
* continuing with initialization. This allows self-referential generics.
*
* @param jdtBinding the JDT binding for which to create a ClassNode
* @return a {@link JDTClassNode}
*/
private ClassNode createJDTClassNode(TypeBinding jdtBinding) {
JDTClassNodeBuilder cnb = new JDTClassNodeBuilder(this);
ClassNode classNode = cnb.configureType(jdtBinding);
if (classNode instanceof JDTClassNode) {
final JDTClassNode jdtNode = (JDTClassNode) classNode;
assert !inProgress.containsKey(jdtBinding);
inProgress.put(jdtBinding, jdtNode);
// fix up generics for BinaryTypeBinding
jdtNode.setupGenerics();
assert nodeCache.get(jdtBinding) == null : "not unique";
nodeCache.put(jdtBinding, jdtNode);
inProgress.remove(jdtBinding);
}
return classNode;
}
/**
* Called when a resolvevisitor is commencing resolution for a type - allows us to setup the JDTResolver to point at the right
* scope for resolutionification. If not able to find a scope, that is a serious problem!
*/
@Override
protected boolean commencingResolution() {
GroovyTypeDeclaration gtDeclaration = scopes.get(currentClass);
if (gtDeclaration == null) {
if (resolvedClassNodes.contains(currentClass)) {
// already resolved!
return false;
}
GroovyEclipseBug geb = new GroovyEclipseBug("commencingResolution failed: no declaration found for class " + currentClass);
geb.printStackTrace();
throw geb;
}
activeScope = null;
if (gtDeclaration.scope == null) {
// The scope may be null if there were errors in the code - let's not freak out the user here
if (gtDeclaration.hasErrors()) {
return false;
}
GroovyEclipseBug geb = new GroovyEclipseBug("commencingResolution failed: declaration found, but unexpectedly found no scope for " + currentClass.getName());
geb.printStackTrace();
throw geb;
}
activeScope = (GroovyCompilationUnitScope) gtDeclaration.scope.compilationUnitScope();
if (DEBUG) {
log("commencing resolution for " + currentClass.getName());
}
return true;
}
@Override
protected void finishedResolution() {
resolvedClassNodes.add(currentClass);
scopes.remove(currentClass);
unresolvables.clear();
}
public GroovyCompilationUnitScope getScope() {
return activeScope;
}
/**
* When recorded, the jdt resolver will be able to (later on) navigate from the classnode back to the JDT scope that should be
* used.
*/
public void record(GroovyTypeDeclaration gtDeclaration) {
// FIXASC can the relationship here from classNode to scope be better preserved to remove the need for this map?
scopes.put(gtDeclaration.getClassNode(), gtDeclaration);
if (gtDeclaration.memberTypes != null) {
TypeDeclaration[] members = gtDeclaration.memberTypes;
for (int m = 0; m < members.length; m++) {
record((GroovyTypeDeclaration) members[m]);
}
}
GroovyTypeDeclaration[] anonymousTypes = gtDeclaration.getAnonymousTypes();
if (anonymousTypes != null) {
for (int m = 0; m < anonymousTypes.length; m++) {
record(anonymousTypes[m]);
}
}
}
public void startResolving(ClassNode node, SourceUnit source) {
try {
super.startResolving(node, source);
unresolvables.clear();
} catch (AbortResolutionException are) {
// Can occur if there are other problems with the node (syntax errors) - so don't try resolving it
}
}
private static String toString(TypeBinding jdtBinding) {
StringBuilder buffer = new StringBuilder();
buffer.append(jdtBinding.readableName());
buffer.append('(').append(jdtBinding.id).append(')');
try {
Object lookup = ReflectionUtils.throwableGetPrivateField(jdtBinding.getClass(), "environment", jdtBinding);
buffer.append("[from lookup ").append(Integer.toHexString(System.identityHashCode(lookup))).append(']');
} catch (Throwable t) {
// not available
}
return buffer.toString();
}
}