/**
* Copyright (C) 2006-2017 INRIA and contributors
* Spoon - http://spoon.gforge.inria.fr/
*
* This software is governed by the CeCILL-C License under French law and
* abiding by the rules of distribution of free software. You can use, modify
* and/or redistribute the software under the terms of the CeCILL-C license as
* circulated by CEA, CNRS and INRIA at http://www.cecill.info.
*
* 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 CeCILL-C License for more details.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL-C license and that you accept its terms.
*/
package spoon.reflect.visitor;
import spoon.reflect.code.CtBlock;
import spoon.reflect.code.CtCatchVariable;
import spoon.reflect.code.CtFieldAccess;
import spoon.reflect.code.CtFieldRead;
import spoon.reflect.code.CtFieldWrite;
import spoon.reflect.code.CtInvocation;
import spoon.reflect.declaration.CtAnnotationType;
import spoon.reflect.declaration.CtClass;
import spoon.reflect.declaration.CtElement;
import spoon.reflect.declaration.CtEnum;
import spoon.reflect.declaration.CtExecutable;
import spoon.reflect.declaration.CtField;
import spoon.reflect.declaration.CtInterface;
import spoon.reflect.declaration.CtMethod;
import spoon.reflect.declaration.CtPackage;
import spoon.reflect.declaration.CtType;
import spoon.reflect.declaration.CtTypeMember;
import spoon.reflect.declaration.CtVariable;
import spoon.reflect.declaration.ParentNotInitializedException;
import spoon.reflect.reference.CtArrayTypeReference;
import spoon.reflect.reference.CtExecutableReference;
import spoon.reflect.reference.CtFieldReference;
import spoon.reflect.reference.CtPackageReference;
import spoon.reflect.reference.CtReference;
import spoon.reflect.reference.CtTypeReference;
import spoon.support.SpoonClassNotFoundException;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* A scanner that calculates the imports for a given model.
*/
public class ImportScannerImpl extends CtScanner implements ImportScanner {
private static final Collection<String> namesPresentInJavaLang8 =
Collections.singletonList("FunctionalInterface");
private static final Collection<String> namesPresentInJavaLang9 = Arrays.asList(
"ProcessHandle", "StackWalker", "StackFramePermission");
protected Map<String, CtTypeReference<?>> classImports = new TreeMap<>();
protected Map<String, CtFieldReference<?>> fieldImports = new TreeMap<>();
protected Map<String, CtExecutableReference<?>> methodImports = new TreeMap<>();
//top declaring type of that import
protected CtTypeReference<?> targetType;
private Map<String, Boolean> namesPresentInJavaLang = new HashMap<>();
private Set<String> fieldAndMethodsNames = new HashSet<String>();
@Override
public <T> void visitCtFieldRead(CtFieldRead<T> fieldRead) {
enter(fieldRead);
scan(fieldRead.getVariable());
scan(fieldRead.getAnnotations());
scan(fieldRead.getTypeCasts());
scan(fieldRead.getVariable());
scan(fieldRead.getTarget());
exit(fieldRead);
}
@Override
public <T> void visitCtFieldWrite(CtFieldWrite<T> fieldWrite) {
enter(fieldWrite);
scan(fieldWrite.getVariable());
scan(fieldWrite.getAnnotations());
scan(fieldWrite.getTypeCasts());
scan(fieldWrite.getVariable());
scan(fieldWrite.getTarget());
exit(fieldWrite);
}
@Override
public <T> void visitCtFieldReference(CtFieldReference<T> reference) {
enter(reference);
if (reference.isStatic()) {
if (!addFieldImport(reference)) {
scan(reference.getDeclaringType());
}
} else {
scan(reference.getDeclaringType());
}
exit(reference);
}
@Override
public <T> void visitCtExecutableReference(
CtExecutableReference<T> reference) {
enter(reference);
if (reference.isStatic()) {
addMethodImport(reference);
} else if (reference.isConstructor()) {
scan(reference.getDeclaringType());
}
scan(reference.getActualTypeArguments());
exit(reference);
}
@Override
public <T> void visitCtInvocation(CtInvocation<T> invocation) {
enter(invocation);
scan(invocation.getAnnotations());
scan(invocation.getTypeCasts());
scan(invocation.getTarget());
scan(invocation.getExecutable());
scan(invocation.getArguments());
exit(invocation);
}
@Override
public <T> void visitCtTypeReference(CtTypeReference<T> reference) {
if (!(reference instanceof CtArrayTypeReference)) {
if (reference.getDeclaringType() == null) {
addClassImport(reference);
} else {
addClassImport(reference.getAccessType());
}
}
super.visitCtTypeReference(reference);
}
@Override
public void scan(CtElement element) {
if (element != null && !element.isImplicit()) {
element.accept(this);
}
}
@Override
public <A extends Annotation> void visitCtAnnotationType(
CtAnnotationType<A> annotationType) {
addClassImport(annotationType.getReference());
super.visitCtAnnotationType(annotationType);
}
@Override
public <T extends Enum<?>> void visitCtEnum(CtEnum<T> ctEnum) {
addClassImport(ctEnum.getReference());
super.visitCtEnum(ctEnum);
}
@Override
public <T> void visitCtInterface(CtInterface<T> intrface) {
addClassImport(intrface.getReference());
for (CtTypeMember t : intrface.getTypeMembers()) {
if (!(t instanceof CtType)) {
continue;
}
addClassImport(((CtType) t).getReference());
}
super.visitCtInterface(intrface);
}
@Override
public <T> void visitCtClass(CtClass<T> ctClass) {
addClassImport(ctClass.getReference());
for (CtTypeMember t : ctClass.getTypeMembers()) {
if (!(t instanceof CtType)) {
continue;
}
addClassImport(((CtType) t).getReference());
}
super.visitCtClass(ctClass);
}
@Override
public <T> void visitCtCatchVariable(CtCatchVariable<T> catchVariable) {
for (CtTypeReference<?> type : catchVariable.getMultiTypes()) {
addClassImport(type);
}
super.visitCtCatchVariable(catchVariable);
}
@Override
public Collection<CtReference> computeAllImports(CtType<?> simpleType) {
classImports.clear();
fieldImports.clear();
methodImports.clear();
//look for top declaring type of that simpleType
targetType = simpleType.getReference().getTopLevelType();
addClassImport(simpleType.getReference());
scan(simpleType);
Collection<CtReference> listallImports = new ArrayList<>();
listallImports.addAll(this.classImports.values());
listallImports.addAll(this.fieldImports.values());
listallImports.addAll(this.methodImports.values());
return listallImports;
}
@Override
public Collection<CtTypeReference<?>> computeImports(CtType<?> simpleType) {
classImports.clear();
fieldImports.clear();
methodImports.clear();
//look for top declaring type of that simpleType
targetType = simpleType.getReference().getTopLevelType();
addClassImport(simpleType.getReference());
scan(simpleType);
return this.classImports.values();
}
@Override
public void computeImports(CtElement element) {
classImports.clear();
fieldImports.clear();
methodImports.clear();
//look for top declaring type of that element
CtType<?> type = element.getParent(CtType.class);
targetType = type == null ? null : type.getReference().getTopLevelType();
scan(element);
}
@Override
public boolean isImported(CtReference ref) {
if (ref instanceof CtFieldReference) {
return isImportedInFieldImports((CtFieldReference) ref);
} else if (ref instanceof CtExecutableReference) {
return isImportedInMethodImports((CtExecutableReference) ref);
} else if (ref instanceof CtTypeReference) {
return isImportedInClassImports((CtTypeReference) ref);
} else {
return false;
}
}
/**
* Adds a type to the classImports.
*/
protected boolean addClassImport(CtTypeReference<?> ref) {
if (ref == null) {
return false;
}
if (targetType != null && targetType.getSimpleName().equals(ref.getSimpleName()) && !targetType.equals(ref)) {
return false;
}
if (classImports.containsKey(ref.getSimpleName())) {
return isImportedInClassImports(ref);
}
// don't import unnamed package elements
if (ref.getPackage() == null || ref.getPackage().isUnnamedPackage()) {
return false;
}
if (ref.getPackage().getSimpleName().equals("java.lang")) {
if (classNamePresentInJavaLang(ref)) {
// Don't import class with names clashing with some classes present in java.lang,
// because it leads to undecidability and compilation errors. I. e. always leave
// com.mycompany.String fully-qualified.
return false;
}
}
if (targetType != null && targetType.canAccess(ref) == false) {
//ref type is not visible in targetType we must not add import for it, java compiler would fail on that.
return false;
}
// we want to be sure that we are not importing a class because a static field or method we already imported
// moreover we make exception for same package classes to avoid problems in FQN mode
if (targetType != null) {
try {
CtElement parent = ref.getParent();
if (parent != null) {
parent = parent.getParent();
if (parent != null) {
if ((parent instanceof CtFieldAccess) || (parent instanceof CtExecutable) || (parent instanceof CtInvocation)) {
CtTypeReference declaringType;
CtReference reference;
CtPackageReference pack = targetType.getPackage();
if (parent instanceof CtFieldAccess) {
CtFieldAccess field = (CtFieldAccess) parent;
CtFieldReference localReference = field.getVariable();
declaringType = localReference.getDeclaringType();
reference = localReference;
} else if (parent instanceof CtExecutable) {
CtExecutable exec = (CtExecutable) parent;
CtExecutableReference localReference = exec.getReference();
declaringType = localReference.getDeclaringType();
reference = localReference;
} else if (parent instanceof CtInvocation) {
CtInvocation invo = (CtInvocation) parent;
CtExecutableReference localReference = invo.getExecutable();
declaringType = localReference.getDeclaringType();
reference = localReference;
} else {
declaringType = null;
reference = null;
}
if (reference != null && isImported(reference)) {
// if we are in the **same** package we do the import for test with method isImported
if (declaringType != null) {
if (declaringType.getPackage() != null && !declaringType.getPackage().isUnnamedPackage()) {
// ignore java.lang package
if (!declaringType.getPackage().getSimpleName().equals("java.lang")) {
// ignore type in same package
if (declaringType.getPackage().getSimpleName()
.equals(pack.getSimpleName())) {
classImports.put(ref.getSimpleName(), ref);
return true;
}
}
}
}
}
}
}
}
} catch (ParentNotInitializedException e) {
}
CtPackageReference pack = targetType.getPackage();
if (ref.getPackage() != null && !ref.getPackage().isUnnamedPackage()) {
// ignore java.lang package
if (!ref.getPackage().getSimpleName().equals("java.lang")) {
// ignore type in same package
if (ref.getPackage().getSimpleName()
.equals(pack.getSimpleName())) {
return false;
}
}
}
}
//note: we must add the type refs from the same package too, to assure that isImported(typeRef) returns true for them
//these type refs are removed in #getClassImports()
classImports.put(ref.getSimpleName(), ref);
return true;
}
protected boolean isImportedInClassImports(CtTypeReference<?> ref) {
if (targetType != null) {
CtPackageReference pack = targetType.getPackage();
// we consider that if a class belongs to java.lang or the same package than the actual class
// then it is imported by default
if (ref.getPackage() != null && !ref.getPackage().isUnnamedPackage()) {
// ignore java.lang package
if (!ref.getPackage().getSimpleName().equals("java.lang")) {
// ignore type in same package
if (ref.getPackage().getSimpleName()
.equals(pack.getSimpleName())) {
return true;
}
}
}
}
if (ref.equals(targetType)) {
return true;
}
if (!(ref.isImplicit()) && classImports.containsKey(ref.getSimpleName())) {
CtTypeReference<?> exist = classImports.get(ref.getSimpleName());
if (exist.getQualifiedName().equals(ref.getQualifiedName())) {
return true;
}
}
return false;
}
/**
* This method is used to check if the declaring type has been already imported, or if it is local
* In both case we do not want to import it, even in FQN mode.
* @param declaringType
* @return true if it is local or imported
*/
private boolean declaringTypeIsLocalOrImported(CtTypeReference declaringType) {
if (declaringType != null) {
if (isImportedInClassImports(declaringType) || classNamePresentInJavaLang(declaringType)) {
return true;
}
if (!isTypeInCollision(declaringType, false) && addClassImport(declaringType)) {
return true;
}
while (declaringType != null) {
if (declaringType.equals(targetType)) {
return true;
}
declaringType = declaringType.getDeclaringType();
}
}
return false;
}
protected boolean addMethodImport(CtExecutableReference ref) {
if (this.methodImports.containsKey(ref.getSimpleName())) {
return isImportedInMethodImports(ref);
}
// if the whole class is imported: no need to import the method.
if (declaringTypeIsLocalOrImported(ref.getDeclaringType())) {
return false;
}
methodImports.put(ref.getSimpleName(), ref);
// if we are in the same package than target type, we also import class to avoid FQN in FQN mode.
if (ref.getDeclaringType() != null) {
if (ref.getDeclaringType().getPackage() != null) {
if (ref.getDeclaringType().getPackage().equals(this.targetType.getPackage())) {
addClassImport(ref.getDeclaringType());
}
}
}
return true;
}
protected boolean isImportedInMethodImports(CtExecutableReference<?> ref) {
if (!(ref.isImplicit()) && methodImports.containsKey(ref.getSimpleName())) {
CtExecutableReference<?> exist = methodImports.get(ref.getSimpleName());
if (exist.getSignature().equals(ref.getSignature())) {
return true;
}
}
return false;
}
protected boolean addFieldImport(CtFieldReference ref) {
if (this.fieldImports.containsKey(ref.getSimpleName())) {
return isImportedInFieldImports(ref);
}
if (declaringTypeIsLocalOrImported(ref.getDeclaringType())) {
return false;
}
fieldImports.put(ref.getSimpleName(), ref);
return true;
}
protected boolean isImportedInFieldImports(CtFieldReference<?> ref) {
if (!(ref.isImplicit()) && fieldImports.containsKey(ref.getSimpleName())) {
CtFieldReference<?> exist = fieldImports.get(ref.getSimpleName());
try {
if (exist.getFieldDeclaration() != null && exist.getFieldDeclaration().equals(ref.getFieldDeclaration())) {
return true;
}
// in some rare cases we could not access to the field, then we do not import it.
} catch (SpoonClassNotFoundException notfound) {
return false;
}
}
return false;
}
protected boolean classNamePresentInJavaLang(CtTypeReference<?> ref) {
Boolean presentInJavaLang = namesPresentInJavaLang.get(ref.getSimpleName());
if (presentInJavaLang == null) {
// The following procedure of determining if the handle is present in Java Lang or
// not produces "false positives" if the analyzed source complianceLevel is > 6.
// For example, it reports that FunctionalInterface is present in java.lang even
// for compliance levels 6, 7. But this is not considered a bad thing, in opposite,
// it makes generated code a little more compatible with future versions of Java.
if (namesPresentInJavaLang8.contains(ref.getSimpleName())
|| namesPresentInJavaLang9.contains(ref.getSimpleName())) {
presentInJavaLang = true;
} else {
// Assuming Spoon's own runtime environment is Java 7+
try {
Class.forName("java.lang." + ref.getSimpleName());
presentInJavaLang = true;
} catch (ClassNotFoundException e) {
presentInJavaLang = false;
}
}
namesPresentInJavaLang.put(ref.getSimpleName(), presentInJavaLang);
}
return presentInJavaLang;
}
protected Set<String> lookForLocalVariables(CtElement parent) {
Set<String> result = new HashSet<>();
// try to get the block container
// if the first container is the class, then we are not in a block and we can quit now.
while (parent != null && !(parent instanceof CtBlock)) {
if (parent instanceof CtClass) {
return result;
}
parent = parent.getParent();
}
if (parent != null) {
CtBlock block = (CtBlock) parent;
boolean innerClass = false;
// now we have the first container block, we want to check if we're not in an inner class
while (parent != null && !(parent instanceof CtClass)) {
parent = parent.getParent();
}
if (parent != null) {
// uhoh it's not a package as a parent, we must in an inner block:
// let's find the last block BEFORE the class call: some collision could occur because of variables defined in that block
if (!(parent.getParent() instanceof CtPackage)) {
while (parent != null && !(parent instanceof CtBlock)) {
parent = parent.getParent();
}
if (parent != null) {
block = (CtBlock) parent;
}
}
}
AccessibleVariablesFinder avf = new AccessibleVariablesFinder(block);
List<CtVariable> variables = avf.find();
for (CtVariable variable : variables) {
result.add(variable.getSimpleName());
}
}
return result;
}
/**
* Test if the reference can be imported, i.e. test if the importation could lead to a collision.
* In FQN mode, it only tests the first package name: if a collision occurs with this first one, it should be imported.
* @param ref
* @return true if the ref should be imported.
*/
protected boolean isTypeInCollision(CtReference ref, boolean fqnMode) {
if (targetType.getSimpleName().equals(ref.getSimpleName()) && !targetType.equals(ref)) {
return true;
}
try {
CtElement parent;
if (ref instanceof CtTypeReference) {
parent = ref.getParent();
} else {
parent = ref;
}
Set<String> localVariablesOfBlock = new HashSet<>();
if (parent instanceof CtField) {
this.fieldAndMethodsNames.add(((CtField) parent).getSimpleName());
} else if (parent instanceof CtMethod) {
this.fieldAndMethodsNames.add(((CtMethod) parent).getSimpleName());
} else {
localVariablesOfBlock = this.lookForLocalVariables(parent);
}
while (!(parent instanceof CtPackage)) {
if ((parent instanceof CtFieldReference) || (parent instanceof CtExecutableReference)) {
CtReference parentType = (CtReference) parent;
LinkedList<String> qualifiedNameTokens = new LinkedList<>();
// we don't want to test the current ref name, as we risk to create field import and make autoreference
if (parentType != parent) {
qualifiedNameTokens.add(parentType.getSimpleName());
}
CtTypeReference typeReference;
if (parent instanceof CtFieldReference) {
typeReference = ((CtFieldReference) parent).getDeclaringType();
} else {
typeReference = ((CtExecutableReference) parent).getDeclaringType();
}
if (typeReference != null) {
qualifiedNameTokens.add(typeReference.getSimpleName());
if (typeReference.getPackage() != null) {
CtPackage ctPackage = typeReference.getPackage().getDeclaration();
while (ctPackage != null) {
qualifiedNameTokens.add(ctPackage.getSimpleName());
CtElement packParent = ctPackage.getParent();
if (packParent.getParent() != null && !((CtPackage) packParent).getSimpleName().equals(CtPackage.TOP_LEVEL_PACKAGE_NAME)) {
ctPackage = (CtPackage) packParent;
} else {
ctPackage = null;
}
}
}
}
if (!qualifiedNameTokens.isEmpty()) {
// qualified name token are ordered in the reverse order
// if the first package name is a variable name somewhere, it could lead to a collision
if (fieldAndMethodsNames.contains(qualifiedNameTokens.getLast()) || localVariablesOfBlock.contains(qualifiedNameTokens.getLast())) {
qualifiedNameTokens.removeLast();
if (fqnMode) {
return true;
} else {
// but if the other package names are not a variable name, it's ok to import
for (int i = qualifiedNameTokens.size() - 1; i > 0; i--) {
String testedToken = qualifiedNameTokens.get(i);
if (!fieldAndMethodsNames.contains(testedToken) && !localVariablesOfBlock.contains(testedToken)) {
return false;
}
}
return true;
}
}
}
}
parent = parent.getParent();
}
} catch (ParentNotInitializedException e) {
return false;
}
return false;
}
}