package org.revapi.java.compilation;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;
import static org.revapi.java.model.JavaElementFactory.elementFor;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.URI;
import java.util.AbstractMap;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.IntersectionType;
import javax.lang.model.type.NoType;
import javax.lang.model.type.PrimitiveType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.type.TypeVisitor;
import javax.lang.model.type.WildcardType;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.SimpleTypeVisitor8;
import javax.lang.model.util.Types;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import org.revapi.Archive;
import org.revapi.java.AnalysisConfiguration;
import org.revapi.java.FlatFilter;
import org.revapi.java.model.AnnotationElement;
import org.revapi.java.model.JavaElementBase;
import org.revapi.java.model.JavaElementFactory;
import org.revapi.java.model.MethodElement;
import org.revapi.java.model.MissingClassElement;
import org.revapi.java.spi.IgnoreCompletionFailures;
import org.revapi.java.spi.UseSite;
import org.revapi.java.spi.Util;
import org.revapi.query.Filter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Lukas Krejci
* @since 0.11.0
*/
final class ClasspathScanner {
private static final Logger LOG = LoggerFactory.getLogger(ClasspathScanner.class);
private static final List<Modifier> ACCESSIBLE_MODIFIERS = Arrays.asList(Modifier.PUBLIC, Modifier.PROTECTED);
private static final String SYSTEM_CLASSPATH_NAME = "<system classpath>";
private final StandardJavaFileManager fileManager;
private final ProbingEnvironment environment;
private final Map<Archive, File> classPath;
private final Map<Archive, File> additionalClassPath;
private final AnalysisConfiguration.MissingClassReporting missingClassReporting;
private final boolean ignoreMissingAnnotations;
private final InclusionFilter inclusionFilter;
private final boolean defaultInclusionCase;
ClasspathScanner(StandardJavaFileManager fileManager, ProbingEnvironment environment,
Map<Archive, File> classPath, Map<Archive, File> additionalClassPath,
AnalysisConfiguration.MissingClassReporting missingClassReporting,
boolean ignoreMissingAnnotations, InclusionFilter inclusionFilter) {
this.fileManager = fileManager;
this.environment = environment;
this.classPath = classPath;
this.additionalClassPath = additionalClassPath;
this.missingClassReporting = missingClassReporting == null
? AnalysisConfiguration.MissingClassReporting.ERROR
: missingClassReporting;
this.ignoreMissingAnnotations = ignoreMissingAnnotations;
this.inclusionFilter = inclusionFilter;
this.defaultInclusionCase = inclusionFilter.defaultCase();
}
void initTree() throws IOException {
List<ArchiveLocation> classPathLocations = classPath.keySet().stream().map(ArchiveLocation::new)
.collect(toList());
Scanner scanner = new Scanner();
for (ArchiveLocation loc : classPathLocations) {
scanner.scan(loc, classPath.get(loc.getArchive()), true);
}
Set<TypeElement> lastUnknowns = Collections.emptySet();
Map<String, ArchiveLocation> cachedArchives = new HashMap<>(additionalClassPath.size());
while (!scanner.requiredTypes.isEmpty() && !lastUnknowns.equals(scanner.requiredTypes.keySet())) {
lastUnknowns = new HashSet<>(scanner.requiredTypes.keySet());
for (TypeElement t : lastUnknowns) {
try {
Field f = t.getClass().getField("classfile");
JavaFileObject jfo = (JavaFileObject) f.get(t);
if (jfo == null) {
t = environment.getElementUtils().getTypeElement(t.getQualifiedName());
if (t == null) {
//this type is really missing...
continue;
}
jfo = (JavaFileObject) f.get(t);
}
URI uri = jfo.toUri();
String path;
if ("jar".equals(uri.getScheme())) {
path = uri.getSchemeSpecificPart();
//jar:file:/path .. let's get rid of the "file:" part
int colonIdx = path.indexOf(':');
if (colonIdx >= 0) {
path = path.substring(colonIdx + 1);
}
//separate the file path from the in-jar path
path = path.substring(0, path.lastIndexOf('!'));
} else {
path = uri.getPath();
}
ArchiveLocation loc = cachedArchives.get(path);
if (loc == null) {
Archive ar = null;
for (Map.Entry<Archive, File> e : additionalClassPath.entrySet()) {
if (e.getValue().getAbsolutePath().equals(path)) {
ar = e.getKey();
break;
}
}
if (ar != null) {
loc = new ArchiveLocation(ar);
cachedArchives.put(path, loc);
}
}
if (loc != null) {
scanner.scanClass(loc, t, false);
}
} catch (NoSuchFieldException e) {
//TODO fallback to manually looping through archives
} catch (IllegalAccessException e) {
//should not happen
throw new AssertionError("Illegal access after setAccessible(true) on a field. Wha?", e);
}
}
}
//ok, so scanning the archives doesn't give us any new resolved classes that we need in the API...
//let's scan the system classpath. What will be left after this will be the truly missing classes.
//making a copy because the required types might be modified during scanning
Map<TypeElement, Boolean> rts = new HashMap<>(scanner.requiredTypes);
ArchiveLocation systemClassPath = new ArchiveLocation(new Archive() {
@Nonnull @Override public String getName() {
return SYSTEM_CLASSPATH_NAME;
}
@Nonnull @Override public InputStream openStream() throws IOException {
throw new UnsupportedOperationException();
}
});
for (Map.Entry<TypeElement, Boolean> e : rts.entrySet()) {
if (e.getKey().asType().getKind() != TypeKind.ERROR) {
scanner.scanClass(systemClassPath, e.getKey(), false);
}
}
scanner.initEnvironment();
}
private final class Scanner {
final Set<TypeElement> processed = new HashSet<>();
final Map<TypeElement, Boolean> requiredTypes = new IdentityHashMap<>();
final Map<TypeElement, TypeRecord> types = new IdentityHashMap<>();
final TypeVisitor<TypeElement, Void> getTypeElement = new SimpleTypeVisitor8<TypeElement, Void>() {
@Override
protected TypeElement defaultAction(TypeMirror e, Void ignored) {
throw new IllegalStateException("Could not determine the element of a type: " + e);
}
@Override
public TypeElement visitDeclared(DeclaredType t, Void ignored) {
return (TypeElement) t.asElement();
}
@Override
public TypeElement visitTypeVariable(TypeVariable t, Void ignored) {
return t.getUpperBound().accept(this, null);
}
@Override
public TypeElement visitArray(ArrayType t, Void ignored) {
return t.getComponentType().accept(this, null);
}
@Override
public TypeElement visitPrimitive(PrimitiveType t, Void ignored) {
return null;
}
@Override
public TypeElement visitIntersection(IntersectionType t, Void aVoid) {
return t.getBounds().get(0).accept(this, null);
}
@Override
public TypeElement visitWildcard(WildcardType t, Void aVoid) {
if (t.getExtendsBound() != null) {
return t.getExtendsBound().accept(this, null);
} else if (t.getSuperBound() != null) {
return t.getSuperBound().accept(this, null);
} else {
return environment.getElementUtils().getTypeElement("java.lang.Object");
}
}
@Override
public TypeElement visitNoType(NoType t, Void aVoid) {
return null;
}
};
void scan(ArchiveLocation location, File path, boolean primaryApi) throws IOException {
fileManager.setLocation(location, Collections.singleton(path));
Iterable<? extends JavaFileObject> jfos = fileManager.list(location, "",
EnumSet.of(JavaFileObject.Kind.CLASS), true);
for (JavaFileObject jfo : jfos) {
TypeElement type = Util.findTypeByBinaryName(environment.getElementUtils(),
fileManager.inferBinaryName(location, jfo));
//type can be null if it represents an anonymous or member class...
if (type != null) {
scanClass(location, type, primaryApi);
}
}
}
void scanClass(ArchiveLocation loc, TypeElement type, boolean primaryApi) {
try {
if (processed.contains(type)) {
return;
}
processed.add(type);
Boolean wasAnno = requiredTypes.remove(type);
String bn = environment.getElementUtils().getBinaryName(type).toString();
String cn = type.getQualifiedName().toString();
boolean includes = inclusionFilter.accepts(bn, cn);
boolean excludes = inclusionFilter.rejects(bn, cn);
//technically, we could find this out later on in the method, but doing this here ensures that the
//javac tries to fully load the class (and therefore throw any completion failures.
//Doing this then ensures that we get a correct TypeKind after this call.
TypeElement superType = getTypeElement.visit(IgnoreCompletionFailures.in(type::getSuperclass));
//type.asType() possibly not completely correct when dealing with inner class of a parameterized class
TypeMirror typeType = type.asType();
if (typeType.getKind() == TypeKind.ERROR) {
//just re-add the missing type and return. It will be dealt with accordingly
//in initEnvironment
requiredTypes.put(type, wasAnno);
return;
}
org.revapi.java.model.TypeElement t =
new org.revapi.java.model.TypeElement(environment, loc.getArchive(), type,
(DeclaredType) type.asType());
TypeRecord tr = getTypeRecord(type);
tr.modelElement = t;
//this will be revisited... in here we're just establishing the types that are in the API for sure...
tr.inApi = !excludes &&
(primaryApi && !shouldBeIgnored(type) && !(type.getEnclosingElement() instanceof TypeElement));
tr.primaryApi = primaryApi;
tr.explicitlyExcluded = excludes;
tr.explicitlyIncluded = includes;
if (tr.explicitlyExcluded) {
return;
}
if (superType != null) {
addUse(tr, type, superType, UseSite.Type.IS_INHERITED);
tr.superTypes.add(getTypeRecord(superType));
if (!processed.contains(superType)) {
requiredTypes.put(superType, false);
}
}
IgnoreCompletionFailures.in(type::getInterfaces).stream().map(getTypeElement::visit)
.forEach(e -> {
if (!processed.contains(e)) {
requiredTypes.put(e, false);
}
addUse(tr, type, e, UseSite.Type.IS_IMPLEMENTED);
tr.superTypes.add(getTypeRecord(e));
});
addTypeParamUses(tr, type, type.asType());
for (Element e : IgnoreCompletionFailures.in(type::getEnclosedElements)) {
switch (e.getKind()) {
case ANNOTATION_TYPE:
case CLASS:
case ENUM:
case INTERFACE:
addUse(tr, type, (TypeElement) e, UseSite.Type.CONTAINS);
//the contained classes by default inherit the API status of their containing class
scanClass(loc, (TypeElement) e, tr.inApi);
break;
case CONSTRUCTOR:
case METHOD:
scanMethod(tr, (ExecutableElement) e);
break;
case ENUM_CONSTANT:
case FIELD:
scanField(tr, (VariableElement) e);
break;
}
}
type.getAnnotationMirrors().forEach(a -> scanAnnotation(tr, type, a));
} catch (Exception e) {
LOG.error("Failed to scan class " + type.getQualifiedName().toString()
+ ". Analysis results may be skewed.", e);
}
}
void placeInTree(TypeRecord typeRecord) {
TypeElement type = typeRecord.modelElement.getDeclaringElement();
if (!(type.getEnclosingElement() instanceof TypeElement)) {
environment.getTree().getRootsUnsafe().add(typeRecord.modelElement);
} else {
ArrayDeque<String> nesting = new ArrayDeque<>();
type = (TypeElement) type.getEnclosingElement();
while (type != null) {
nesting.push(type.getQualifiedName().toString());
type = type.getEnclosingElement() instanceof TypeElement
? (TypeElement) type.getEnclosingElement()
: null;
}
Function<String, Filter<org.revapi.java.model.TypeElement>> findByCN =
cn -> FlatFilter.by(e -> cn.equals(e.getCanonicalName()));
List<org.revapi.java.model.TypeElement> parents = Collections.emptyList();
while (parents.isEmpty() && !nesting.isEmpty()) {
parents = environment.getTree().searchUnsafe(
org.revapi.java.model.TypeElement.class, false, findByCN.apply(nesting.pop()), null);
}
org.revapi.java.model.TypeElement parent = parents.isEmpty() ? null : parents.get(0);
while (!nesting.isEmpty()) {
String cn = nesting.pop();
parents = environment.getTree().searchUnsafe(
org.revapi.java.model.TypeElement.class, false, findByCN.apply(cn),
parent);
if (parents.isEmpty()) {
//we found a "gap" in the parents included in the model. let's start from the top
//again
do {
parents = environment.getTree().searchUnsafe(
org.revapi.java.model.TypeElement.class, false, findByCN.apply(cn), null);
if (parents.isEmpty() && !nesting.isEmpty()) {
cn = nesting.pop();
} else {
break;
}
} while (!nesting.isEmpty());
}
parent = parents.isEmpty() ? null : parents.get(0);
}
if (parent == null) {
environment.getTree().getRootsUnsafe().add(typeRecord.modelElement);
} else {
parent.getChildren().add(typeRecord.modelElement);
}
}
}
void scanField(TypeRecord owningType, VariableElement field) {
if (shouldBeIgnored(field)) {
owningType.inaccessibleDeclaredNonClassMembers.add(field);
return;
}
owningType.accessibleDeclaredNonClassMembers.add(field);
TypeElement fieldType = field.asType().accept(getTypeElement, null);
//fieldType == null means primitive type, if no type can be found, exception is thrown
if (fieldType == null) {
return;
}
addUse(owningType, field, fieldType, UseSite.Type.HAS_TYPE);
addType(fieldType, false);
addTypeParamUses(owningType, field, field.asType());
field.getAnnotationMirrors().forEach(a -> scanAnnotation(owningType, field, a));
}
void scanMethod(TypeRecord owningType, ExecutableElement method) {
if (shouldBeIgnored(method)) {
owningType.inaccessibleDeclaredNonClassMembers.add(method);
return;
}
owningType.accessibleDeclaredNonClassMembers.add(method);
TypeElement returnType = method.getReturnType().accept(getTypeElement, null);
if (returnType != null) {
addUse(owningType, method, returnType, UseSite.Type.RETURN_TYPE);
addType(returnType, false);
addTypeParamUses(owningType, method, method.getReturnType());
}
int idx = 0;
for (VariableElement p : method.getParameters()) {
TypeElement pt = p.asType().accept(getTypeElement, null);
if (pt != null) {
addUse(owningType, method, pt, UseSite.Type.PARAMETER_TYPE, idx++);
addType(pt, false);
addTypeParamUses(owningType, method, p.asType());
}
p.getAnnotationMirrors().forEach(a -> scanAnnotation(owningType, p, a));
}
method.getThrownTypes().forEach(t -> {
TypeElement ex = t.accept(getTypeElement, null);
if (ex != null) {
addUse(owningType, method, ex, UseSite.Type.IS_THROWN);
addType(ex, false);
addTypeParamUses(owningType, method, t);
}
t.getAnnotationMirrors().forEach(a -> scanAnnotation(owningType, method, a));
});
method.getAnnotationMirrors().forEach(a -> scanAnnotation(owningType, method, a));
}
void scanAnnotation(TypeRecord owningType, Element annotated, AnnotationMirror annotation) {
TypeElement type = annotation.getAnnotationType().accept(getTypeElement, null);
if (type != null) {
addUse(owningType, annotated, type, UseSite.Type.ANNOTATES);
addType(type, true);
}
}
boolean addType(TypeElement type, boolean isAnnotation) {
if (processed.contains(type)) {
return true;
}
requiredTypes.put(type, isAnnotation);
return false;
}
void addTypeParamUses(TypeRecord userType, Element user, TypeMirror usedType) {
HashSet<String> visited = new HashSet<>(4);
usedType.accept(new SimpleTypeVisitor8<Void, Void>() {
@Override public Void visitIntersection(IntersectionType t, Void aVoid) {
t.getBounds().forEach(b -> b.accept(this, null));
return null;
}
@Override public Void visitArray(ArrayType t, Void ignored) {
return t.getComponentType().accept(this, null);
}
@Override public Void visitDeclared(DeclaredType t, Void ignored) {
String type = Util.toUniqueString(t);
if (!visited.contains(type)) {
visited.add(type);
if (t != usedType) {
TypeElement typeEl = (TypeElement) t.asElement();
addType(typeEl, false);
addUse(userType, user, typeEl, UseSite.Type.TYPE_PARAMETER_OR_BOUND);
}
t.getTypeArguments().forEach(a -> a.accept(this, null));
}
return null;
}
@Override public Void visitTypeVariable(TypeVariable t, Void ignored) {
return t.getUpperBound().accept(this, null);
}
@Override public Void visitWildcard(WildcardType t, Void ignored) {
TypeMirror bound = t.getExtendsBound();
if (bound != null) {
bound.accept(this, null);
}
bound = t.getSuperBound();
if (bound != null) {
bound.accept(this, null);
}
return null;
}
}, null);
}
void addUse(TypeRecord userType, Element user, TypeElement used, UseSite.Type useType) {
addUse(userType, user, used, useType, -1);
}
void addUse(TypeRecord userType, Element user, TypeElement used, UseSite.Type useType, int indexInParent) {
TypeRecord usedTr = getTypeRecord(used);
Set<ClassPathUseSite> sites = usedTr.useSites;
sites.add(new ClassPathUseSite(useType, user, indexInParent));
Set<TypeRecord> usedTypes = userType.usedTypes.get(useType);
if (usedTypes == null) {
usedTypes = new HashSet<>(4);
userType.usedTypes.put(useType, usedTypes);
}
usedTypes.add(usedTr);
}
TypeRecord getTypeRecord(TypeElement type) {
TypeRecord rec = types.get(type);
if (rec == null) {
rec = new TypeRecord();
rec.javacElement = type;
int depth = 0;
Element e = type.getEnclosingElement();
while (e != null && e instanceof TypeElement) {
depth++;
e = e.getEnclosingElement();
}
rec.nestingDepth = depth;
types.put(type, rec);
}
return rec;
}
void initEnvironment() {
if (ignoreMissingAnnotations && !requiredTypes.isEmpty()) {
removeAnnotatesUses();
}
moveInnerClassesOfPrimariesToApi();
determineApiStatus();
initChildren();
Set<TypeRecord> types = constructTree();
if (!requiredTypes.isEmpty()) {
handleMissingClasses(types);
}
environment.setTypeMap(types.stream().collect(toMap(tr -> tr.javacElement, tr -> tr.modelElement)));
}
private void handleMissingClasses(Set<TypeRecord> types) {
Elements els = environment.getElementUtils();
switch (missingClassReporting) {
case ERROR:
List<String> reallyMissing = requiredTypes.keySet().stream()
.map(t -> els.getTypeElement(t.getQualifiedName()))
.filter(t -> els.getTypeElement(t.getQualifiedName()) == null)
.map(t -> t.getQualifiedName().toString())
.sorted()
.collect(toList());
if (!reallyMissing.isEmpty()) {
throw new IllegalStateException(
"The following classes that contribute to the public API of " +
environment.getApi() +
" could not be located: " +
reallyMissing);
}
break;
case REPORT:
for (TypeElement t : requiredTypes.keySet()) {
TypeElement type = els.getTypeElement(t.getQualifiedName());
if (type == null) {
TypeRecord tr = this.types.get(t);
String bin = els.getBinaryName(t).toString();
MissingClassElement mce = new MissingClassElement(environment, bin,
t.getQualifiedName().toString());
if (tr == null) {
tr = new TypeRecord();
}
mce.setInApi(tr.inApi);
mce.setInApiThroughUse(tr.inApiThroughUse);
mce.setRawUseSites(tr.useSites);
tr.javacElement = mce.getDeclaringElement();
tr.modelElement = mce;
types.add(tr);
environment.getTree().getRootsUnsafe().add(mce);
}
}
}
}
private Set<TypeRecord> constructTree() {
Set<TypeRecord> types = new HashSet<>();
Set<TypeElement> ignored = new HashSet<>();
Comparator<Map.Entry<TypeElement, TypeRecord>> byNestingDepth = (a, b) -> {
TypeRecord ar = a.getValue();
TypeRecord br = b.getValue();
int ret = ar.nestingDepth - br.nestingDepth;
if (ret == 0) {
ret = a.getKey().getQualifiedName().toString().compareTo(b.getKey().getQualifiedName().toString());
}
//the less nested classes need to come first
return ret;
};
this.types.entrySet().stream().sorted(byNestingDepth).forEach(e -> {
TypeElement t = e.getKey();
TypeRecord r = e.getValue();
//the model element will be null for missing types. Additionally, we don't want the system classpath
//in our tree, because that is superfluous.
if (r.modelElement != null && !r.modelElement.getArchive().getName().equals(SYSTEM_CLASSPATH_NAME)) {
String cn = t.getQualifiedName().toString();
boolean includes = r.explicitlyIncluded;
boolean excludes = r.explicitlyExcluded;
if (includes) {
environment.addExplicitInclusion(cn);
}
if (excludes) {
environment.addExplicitExclusion(cn);
ignored.add(t);
} else {
boolean include = defaultInclusionCase || includes;
Element owner = t.getEnclosingElement();
if (owner != null && owner instanceof TypeElement) {
ArrayDeque<TypeElement> owners = new ArrayDeque<>();
while (owner != null && owner instanceof TypeElement) {
owners.push((TypeElement) owner);
owner = owner.getEnclosingElement();
}
//find the first owning class that is part of our model
List<TypeElement> siblings = environment.getTree().getRootsUnsafe().stream().map(
org.revapi.java.model.TypeElement::getDeclaringElement).collect(Collectors.toList());
while (!owners.isEmpty()) {
if (ignored.contains(owners.peek()) || siblings.contains(owners.peek())) {
break;
}
owners.pop();
}
//if the user doesn't want this type included explicitly, we need to check in the parents
//if some of them wasn't explicitly excluded
if (!includes && !owners.isEmpty()) {
do {
TypeElement o = owners.pop();
include = !ignored.contains(o) && siblings.contains(o);
siblings = ElementFilter.typesIn(o.getEnclosedElements());
} while (include && !owners.isEmpty());
}
}
if (include) {
placeInTree(r);
r.modelElement.setRawUseSites(r.useSites);
types.add(r);
r.modelElement.setInApi(r.inApi);
r.modelElement.setInApiThroughUse(r.inApiThroughUse);
}
}
}
});
return types;
}
private void determineApiStatus() {
Set<TypeRecord> undetermined = new HashSet<>(this.types.values());
while (!undetermined.isEmpty()) {
undetermined = undetermined.stream()
.filter(tr -> !tr.explicitlyExcluded)
.filter(tr -> tr.inApi)
.flatMap(tr -> tr.usedTypes.entrySet().stream()
.map(e -> new AbstractMap.SimpleImmutableEntry<>(tr, e)))
.filter(e -> movesToApi(e.getValue().getKey()))
.flatMap(e -> e.getValue().getValue().stream())
.filter(usedTr -> !usedTr.inApi)
.filter(usedTr -> !usedTr.explicitlyExcluded)
.map(usedTr -> {
usedTr.inApi = true;
usedTr.inApiThroughUse = true;
return usedTr;
})
.collect(toSet());
}
}
private void moveInnerClassesOfPrimariesToApi() {
Set<TypeRecord> primaries = this.types.values().stream()
.filter(tr -> tr.primaryApi)
.filter(tr -> tr.inApi)
.filter(tr -> tr.nestingDepth == 0)
.collect(toSet());
while (!primaries.isEmpty()) {
primaries = primaries.stream()
.flatMap(tr -> tr.usedTypes.getOrDefault(UseSite.Type.CONTAINS, Collections.emptySet()).stream())
.filter(containedTr -> containedTr.modelElement != null)
.filter(containedTr -> !shouldBeIgnored(containedTr.modelElement.getDeclaringElement()))
.map(containedTr -> {
containedTr.inApi = true;
return containedTr;
})
.collect(toSet());
}
}
private void removeAnnotatesUses() {
Map<TypeElement, Boolean> newTypes = requiredTypes.entrySet().stream().filter(e -> {
boolean isAnno = e.getValue();
if (isAnno) {
this.types.get(e.getKey()).useSites.clear();
}
return !isAnno;
}).collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
requiredTypes.clear();
requiredTypes.putAll(newTypes);
}
private void initChildren() {
for (TypeRecord tr : this.types.values()) {
if (tr.modelElement == null) {
continue;
}
//the set of methods' override-sensitive signatures - I.e. this is the list of methods
//actually visible on a type.
Set<String> methods = new HashSet<>(8);
Function<JavaElementBase<?, ?>, JavaElementBase<?, ?>> addOverride = e -> {
if (e instanceof MethodElement) {
MethodElement me = (MethodElement) e;
methods.add(getOverrideMapKey(me.getDeclaringElement()));
}
return e;
};
Function<JavaElementBase<?, ?>, JavaElementBase<?, ?>> initChildren = e -> {
initNonClassElementChildrenAndMoveToApi(tr, e, false);
return e;
};
//add declared stuff
tr.accessibleDeclaredNonClassMembers.stream()
.map(e ->
elementFor(e, e.asType(), environment, tr.modelElement.getArchive()))
.map(addOverride)
.map(initChildren)
.forEach(c -> tr.modelElement.getChildren().add(c));
tr.inaccessibleDeclaredNonClassMembers.stream()
.map(e ->
elementFor(e, e.asType(), environment, tr.modelElement.getArchive()))
.map(addOverride)
.map(initChildren)
.forEach(c -> tr.modelElement.getChildren().add(c));
//now add inherited stuff
tr.superTypes.forEach(str -> addInherited(tr, str, methods));
//and finally the annotations
for (AnnotationMirror m : tr.javacElement.getAnnotationMirrors()) {
tr.modelElement.getChildren().add(new AnnotationElement(environment, tr.modelElement.getArchive(), m));
}
}
}
private void addInherited(TypeRecord target, TypeRecord superType, Set<String> methodOverrideMap) {
Types types = environment.getTypeUtils();
superType.accessibleDeclaredNonClassMembers.stream()
.map(e -> {
if (e instanceof ExecutableElement) {
ExecutableElement me = (ExecutableElement) e;
if (!shouldAddInheritedMethodChild(me, methodOverrideMap)) {
return null;
}
}
TypeMirror elementType = types.asMemberOf((DeclaredType) target.javacElement.asType(), e);
JavaElementBase<?, ?> element = JavaElementFactory
.elementFor(e, elementType, environment, superType.modelElement.getArchive());
element.setInherited(true);
initNonClassElementChildrenAndMoveToApi(target, element, true);
return element;
})
.filter(e -> e != null)
.forEach(c -> target.modelElement.getChildren().add(c));
for (TypeRecord st : superType.superTypes) {
addInherited(target, st, methodOverrideMap);
}
}
private boolean shouldAddInheritedMethodChild(ExecutableElement methodElement, Set<String> overrideMap) {
if (methodElement.getKind() == ElementKind.CONSTRUCTOR) {
return false;
}
String overrideKey = getOverrideMapKey(methodElement);
boolean alreadyIncludedMethod = overrideMap.contains(overrideKey);
if (alreadyIncludedMethod) {
return false;
} else {
//remember this to check if the next super type doesn't declare a method this one overrides
overrideMap.add(overrideKey);
return true;
}
}
private void initNonClassElementChildrenAndMoveToApi(TypeRecord targetType, JavaElementBase<?, ?> parent,
boolean inherited) {
Types types = environment.getTypeUtils();
if (targetType.inApi && !shouldBeIgnored(parent.getDeclaringElement())) {
TypeMirror representation = types.asMemberOf(targetType.modelElement.getModelRepresentation(),
parent.getDeclaringElement());
representation.accept(new SimpleTypeVisitor8<Void, Void>() {
@Override protected Void defaultAction(TypeMirror e, Void aVoid) {
if (e.getKind().isPrimitive() || e.getKind() == TypeKind.VOID) {
return null;
}
TypeElement childType = getTypeElement.visit(e);
if (childType != null) {
TypeRecord tr = Scanner.this.types.get(childType);
if (tr != null && tr.modelElement != null) {
if (!tr.inApi) {
tr.inApiThroughUse = true;
}
tr.inApi = true;
}
}
return null;
}
@Override public Void visitExecutable(ExecutableType t, Void aVoid) {
t.getReturnType().accept(this, null);
t.getParameterTypes().forEach(p -> p.accept(this, null));
return null;
}
}, null);
}
for (Element child : parent.getDeclaringElement().getEnclosedElements()) {
if (child.getKind().isClass() || child.getKind().isInterface()) {
continue;
}
TypeMirror representation = types.asMemberOf(targetType.modelElement.getModelRepresentation(),
child);
JavaElementBase<?, ?> childEl = JavaElementFactory.elementFor(child, representation, environment,
parent.getArchive());
childEl.setInherited(inherited);
parent.getChildren().add(childEl);
initNonClassElementChildrenAndMoveToApi(targetType, childEl, inherited);
}
for (AnnotationMirror m : parent.getDeclaringElement().getAnnotationMirrors()) {
parent.getChildren().add(new AnnotationElement(environment, parent.getArchive(), m));
}
}
}
private static String getOverrideMapKey(ExecutableElement method) {
return method.getSimpleName() + "#" + Util.toUniqueString(method.asType());
}
private static final class ArchiveLocation implements JavaFileManager.Location {
private final Archive archive;
private ArchiveLocation(Archive archive) {
this.archive = archive;
}
Archive getArchive() {
return archive;
}
@Override
public String getName() {
return "archiveLocation_" + archive.getName();
}
@Override
public boolean isOutputLocation() {
return false;
}
}
private static final class TypeRecord {
Set<ClassPathUseSite> useSites = new HashSet<>(2);
TypeElement javacElement;
org.revapi.java.model.TypeElement modelElement;
Map<UseSite.Type, Set<TypeRecord>> usedTypes = new EnumMap<>(UseSite.Type.class);
Set<Element> accessibleDeclaredNonClassMembers = new HashSet<>(4);
Set<Element> inaccessibleDeclaredNonClassMembers = new HashSet<>(4);
//important for this to be a linked hashset so that superclasses are processed prior to implemented interfaces
Set<TypeRecord> superTypes = new LinkedHashSet<>(2);
boolean explicitlyExcluded;
boolean explicitlyIncluded;
boolean inApi;
boolean inApiThroughUse;
boolean primaryApi;
int nestingDepth;
@Override public String toString() {
final StringBuilder sb = new StringBuilder("TypeRecord[");
sb.append("inApi=").append(inApi);
sb.append(", modelElement=").append(modelElement);
sb.append(']');
return sb.toString();
}
}
private static boolean movesToApi(UseSite.Type useType) {
return useType.isMovingToApi();
}
static boolean shouldBeIgnored(Element element) {
return Collections.disjoint(element.getModifiers(), ACCESSIBLE_MODIFIERS);
}
}