package de.is24.deadcode4j.analyzer; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.TypeParameter; import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; import com.github.javaparser.ast.body.ConstructorDeclaration; import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.expr.MethodReferenceExpr; import com.github.javaparser.ast.type.*; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; import com.google.common.base.Optional; import de.is24.deadcode4j.AnalysisContext; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Deque; import java.util.List; import java.util.Map; import java.util.Set; import static com.google.common.collect.Lists.newLinkedList; import static com.google.common.collect.Maps.newHashMap; import static com.google.common.collect.Sets.newHashSet; import static de.is24.deadcode4j.Utils.emptyIfNull; import static de.is24.deadcode4j.Utils.getOrAddMappedSet; import static de.is24.javaparser.Nodes.getTypeName; import static java.util.Collections.emptySet; /** * Analyzes Java files and reports dependencies to classes that are not part of the byte code due to type erasure. * * @since 2.0.0 */ public class TypeErasureAnalyzer extends JavaFileAnalyzer { @Nonnull private static String getFullQualifier(@Nonnull ClassOrInterfaceType classOrInterfaceType) { StringBuilder buffy = new StringBuilder(classOrInterfaceType.getName()); while ((classOrInterfaceType = classOrInterfaceType.getScope()) != null) { buffy.insert(0, '.'); buffy.insert(0, classOrInterfaceType.getName()); } return buffy.toString(); } @Override protected void analyzeCompilationUnit(@Nonnull final AnalysisContext analysisContext, @Nonnull final CompilationUnit compilationUnit) { compilationUnit.accept(new TypeParameterRecordingVisitor<Void>() { private final Map<String, Set<String>> processedReferences = newHashMap(); @Override public void visit(ClassOrInterfaceType n, Void arg) { for (Type type : emptyIfNull(n.getTypeArgs())) { ClassOrInterfaceType referencedType = getReferencedType(type); if (referencedType == null) { continue; } if (typeParameterWithSameNameIsDefined(referencedType)) { continue; } resolveTypeReference(referencedType); this.visit(referencedType, arg); // resolve nested type arguments } } @Nullable private ClassOrInterfaceType getReferencedType(@Nonnull Type type) { final Type nestedType; if (ReferenceType.class.isInstance(type)) { nestedType = ReferenceType.class.cast(type).getType(); } else if (WildcardType.class.isInstance(type)) { WildcardType wildcardType = WildcardType.class.cast(type); ReferenceType referenceType = wildcardType.getExtends(); if (referenceType == null) { referenceType = wildcardType.getSuper(); } if (referenceType == null) { // unbounded wildcard - nothing ro refer to return null; } nestedType = referenceType.getType(); } else { logger.warn("Encountered unexpected Type [{}:{}]; please create an issue at https://github.com/ImmobilienScout24/deadcode4j.", type.getClass(), type); return null; } if (PrimitiveType.class.isInstance(nestedType)) { // references to primitives won't be reported return null; } if (!ClassOrInterfaceType.class.isInstance(nestedType)) { logger.warn("[{}:{}] is no ClassOrInterfaceType; please create an issue at https://github.com/ImmobilienScout24/deadcode4j.", type.getClass(), type); return null; } return ClassOrInterfaceType.class.cast(nestedType); } private void resolveTypeReference(final ClassOrInterfaceType referencedType) { if (!needsProcessing(referencedType)) { return; } Optional<String> resolvedClass = resolveType(analysisContext, new ClassOrInterfaceTypeQualifier(referencedType)); String depender = getTypeName(referencedType); if (resolvedClass.isPresent()) { analysisContext.addDependencies(depender, resolvedClass.get()); } else { logger.debug("Could not resolve Type Argument [{}] used by [{}].", getFullQualifier(referencedType), depender); } } private boolean needsProcessing(ClassOrInterfaceType referencedType) { Set<String> references = getOrAddMappedSet(this.processedReferences, getTypeName(referencedType)); return references.add(getFullQualifier(referencedType)); } }, null); } private static class TypeParameterRecordingVisitor<A> extends VoidVisitorAdapter<A> { private final Deque<Set<String>> definedTypeParameters = newLinkedList(); @Override public void visit(ClassOrInterfaceDeclaration n, A arg) { this.definedTypeParameters.addLast(getTypeParameterNames(n.getTypeParameters())); try { super.visit(n, arg); } finally { this.definedTypeParameters.removeLast(); } } @Override public void visit(ConstructorDeclaration n, A arg) { this.definedTypeParameters.addLast(getTypeParameterNames(n.getTypeParameters())); try { super.visit(n, arg); } finally { this.definedTypeParameters.removeLast(); } } @Override public void visit(MethodDeclaration n, A arg) { this.definedTypeParameters.addLast(getTypeParameterNames(n.getTypeParameters())); try { super.visit(n, arg); } finally { this.definedTypeParameters.removeLast(); } } @Override public void visit(MethodReferenceExpr n, A arg) { this.definedTypeParameters.addLast(getTypeParameterNames(n.getTypeParameters())); try { super.visit(n, arg); } finally { this.definedTypeParameters.removeLast(); } } protected boolean typeParameterWithSameNameIsDefined(@Nonnull ClassOrInterfaceType nestedClassOrInterface) { if (nestedClassOrInterface.getScope() != null) { return false; } for (Set<String> definedTypeNames : this.definedTypeParameters) { if (definedTypeNames.contains(nestedClassOrInterface.getName())) { return true; } } return false; } @Nonnull private Set<String> getTypeParameterNames(@Nullable List<TypeParameter> typeParameters) { if (typeParameters == null) { return emptySet(); } Set<String> parameters = newHashSet(); for (TypeParameter typeParameter : typeParameters) { parameters.add(typeParameter.getName()); } return parameters; } } private static class ClassOrInterfaceTypeQualifier extends Qualifier<ClassOrInterfaceType> { public ClassOrInterfaceTypeQualifier(ClassOrInterfaceType referencedType) { super(referencedType, null); } private ClassOrInterfaceTypeQualifier(ClassOrInterfaceType referencedType, ClassOrInterfaceTypeQualifier parent) { super(referencedType, parent); } @Nonnull @Override protected String getName(@Nonnull ClassOrInterfaceType referencedType) { return referencedType.getName(); } @Nonnull @Override protected String getFullQualifier(@Nonnull ClassOrInterfaceType referencedType) { return TypeErasureAnalyzer.getFullQualifier(referencedType); } @Nullable @Override protected Qualifier getScopeQualifier(@Nonnull ClassOrInterfaceType referencedType) { ClassOrInterfaceType scope = referencedType.getScope(); return scope == null ? null : new ClassOrInterfaceTypeQualifier(scope, this); } @Override public boolean allowsPartialResolving() { return false; } } }