package de.is24.deadcode4j.analyzer; import com.github.javaparser.JavaParser; import com.github.javaparser.ParseException; import com.github.javaparser.TokenMgrError; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.ImportDeclaration; import com.github.javaparser.ast.Node; import com.github.javaparser.ast.PackageDeclaration; import com.github.javaparser.ast.body.BodyDeclaration; import com.github.javaparser.ast.body.TypeDeclaration; import com.google.common.base.Optional; import com.google.common.cache.LoadingCache; import de.is24.deadcode4j.AnalysisContext; import de.is24.deadcode4j.analyzer.javassist.ClassPoolAccessor; import de.is24.guava.NonNullFunction; import de.is24.guava.SequentialLoadingCache; import de.is24.javaparser.Nodes; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import javassist.CtClass; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.*; import java.util.ArrayList; import java.util.Collections; import java.util.List; import static com.google.common.base.Optional.absent; import static com.google.common.base.Optional.of; import static com.google.common.base.Predicates.and; import static com.google.common.base.Predicates.not; import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.collect.Lists.newArrayList; import static de.is24.deadcode4j.Utils.emptyIfNull; import static de.is24.guava.NonNullFunctions.or; import static de.is24.guava.NonNullFunctions.toFunction; import static de.is24.javaparser.ImportDeclarations.isAsterisk; import static de.is24.javaparser.ImportDeclarations.refersTo; import static de.is24.javaparser.Nodes.getTypeName; import static de.is24.javaparser.Nodes.prepend; import static de.is24.javassist.CtClasses.*; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static org.apache.commons.io.IOUtils.closeQuietly; /** * Serves as a base class with which to analyze java files. * * @since 2.0.0 */ public abstract class JavaFileAnalyzer extends AnalyzerAdapter { private static final String JAVA_PARSER_KEY = JavaFileAnalyzer.class.getName() + ":JavaParser"; private static final NonNullFunction<AnalysisContext, LoadingCache<File, Optional<CompilationUnit>>> JAVA_PARSER_SUPPLIER = new JavaParserSupplier(true); private static final String TYPE_RESOLVER_KEY = JavaFileAnalyzer.class.getName() + ":TypeResolver"; private static final NonNullFunction<AnalysisContext, NonNullFunction<Qualifier<?>, Optional<String>>> TYPE_RESOLVER_SUPPLIER = new NonNullFunction<AnalysisContext, NonNullFunction<Qualifier<?>, Optional<String>>>() { @Nonnull @Override public NonNullFunction<Qualifier<?>, Optional<String>> apply(@Nonnull AnalysisContext analysisContext) { final ClassPoolAccessor classPoolAccessor = ClassPoolAccessor.classPoolAccessorFor(analysisContext); return new NonNullFunction<Qualifier<?>, Optional<String>>() { @Nonnull @Override @SuppressWarnings("unchecked") public Optional<String> apply(@Nonnull Qualifier<?> input) { return or( new FullyQualifiedTypeResolver(classPoolAccessor), new InnerTypeResolver(), new InheritedTypeResolver(classPoolAccessor), new ImportedTypeResolver(classPoolAccessor), new PackageTypeResolver(classPoolAccessor), new AsteriskImportedTypeResolver(classPoolAccessor), new JavaLangTypeResolver(classPoolAccessor) ).apply(input); } }; } }; private static LoadingCache<File, Optional<CompilationUnit>> getJavaFileParser(AnalysisContext analysisContext) { return analysisContext.getOrCreateCacheEntry(JAVA_PARSER_KEY, JAVA_PARSER_SUPPLIER); } private static NonNullFunction<Qualifier<?>, Optional<String>> getTypeResolver(AnalysisContext analysisContext) { return analysisContext.getOrCreateCacheEntry(TYPE_RESOLVER_KEY, TYPE_RESOLVER_SUPPLIER); } /** * Resolves a type reference by means of the given {@code Qualifier}. * * @see de.is24.deadcode4j.analyzer.JavaFileAnalyzer.Qualifier * @since 2.0.0 */ @Nonnull protected static Optional<String> resolveType(@Nonnull final AnalysisContext analysisContext, @Nonnull Qualifier qualifier) { Optional<String> resolvedClass = getTypeResolver(analysisContext).apply(qualifier); if (!qualifier.allowsPartialResolving() && resolvedClass.isPresent() && !isFullyResolved(resolvedClass.get(), qualifier)) { return absent(); } return resolvedClass; } protected static boolean isFullyResolved(@Nonnull String resolvedClass, @Nonnull Qualifier qualifier) { return resolvedClass.replace('$', '.').endsWith(qualifier.getFullQualifier().replace('$', '.')); } @Override public final void doAnalysis(@Nonnull AnalysisContext analysisContext, @Nonnull File file) { if (file.getName().endsWith(".java")) { Optional<CompilationUnit> compilationUnit = getJavaFileParser(analysisContext).getUnchecked(file); if (compilationUnit.isPresent()) { logger.debug("Analyzing Java file [{}]...", file); analyzeCompilationUnit(analysisContext, compilationUnit.get()); } } } /** * Perform an analysis for the specified java file. * Results must be reported via the capabilities of the {@link AnalysisContext}. * * @since 2.0.0 */ protected abstract void analyzeCompilationUnit(@Nonnull AnalysisContext analysisContext, @Nonnull CompilationUnit compilationUnit); /** * Subclasses of {@code Qualifier} are used to resolve types by providing an environment to analyze. * * @see #resolveType(de.is24.deadcode4j.AnalysisContext, de.is24.deadcode4j.analyzer.JavaFileAnalyzer.Qualifier) * @since 2.0.0 */ protected static abstract class Qualifier<T extends Node> { @Nonnull private final T reference; @Nonnull private final String name; @Nonnull private final String fullQualifier; @Nullable private final Qualifier<?> parentQualifier; @Nullable private final Qualifier<?> scopeQualifier; protected Qualifier(@Nonnull T reference, @Nullable Qualifier<?> parent) { this.reference = reference; this.parentQualifier = parent; this.scopeQualifier = getScopeQualifier(reference); this.name = getName(reference); this.fullQualifier = getFullQualifier(reference); } protected Qualifier(@Nonnull T reference) { this(reference, null); } /** * Must return the name of the level/scope this qualifier represents. * * @since 2.0.0 */ @Nonnull protected abstract String getName(@Nonnull T reference); /** * Must return the full qualifier name of this level/scope and below. * * @since 2.0.0 */ @Nonnull protected abstract String getFullQualifier(@Nonnull T reference); /** * Must return the qualifier of the level/scope below. * * @since 2.0.0 */ @Nullable protected abstract Qualifier<?> getScopeQualifier(@Nonnull T reference); /** * Indicates if this qualifier can be resolved partially or must be resolved completely. * * @since 2.0.0 */ protected abstract boolean allowsPartialResolving(); @Nonnull protected final T getNode() { return this.reference; } @Nonnull protected final String getName() { return this.name; } @Nonnull protected final String getFullQualifier() { return fullQualifier; } @Nullable protected final Qualifier<?> getParentQualifier() { return this.parentQualifier; } @Nullable protected final Qualifier<?> getScopeQualifier() { return this.scopeQualifier; } @Nonnull protected final Qualifier<?> getFirstQualifier() { for (Qualifier<?> currentScope = this, nextScope; ; ) { nextScope = currentScope.getScopeQualifier(); if (nextScope == null) { return currentScope; } currentScope = nextScope; } } protected final boolean isSingleQualifier() { return this == getFirstQualifier(); } @Nonnull protected final Iterable<? extends Qualifier> getTypeCandidates() { if (!allowsPartialResolving()) { return Collections.<Qualifier<? extends Node>>singleton(this); } List<Qualifier<?>> candidates = newArrayList(); for (Qualifier<?> loopQualifier = this; ; ) { candidates.add(loopQualifier); loopQualifier = loopQualifier.getScopeQualifier(); if (loopQualifier == null) { return candidates; } } } /** * This hook allows to further analyze an inherited type. * * @return the name of the class this qualifier refers to * @since 2.0.0 */ @Nonnull protected Optional<String> examineInheritedType(@Nonnull CtClass referencingClazz, @Nonnull CtClass inheritedClazz) { return absent(); } } private static abstract class RequiresClassPoolAccessor { @Nonnull protected final ClassPoolAccessor classPoolAccessor; protected RequiresClassPoolAccessor(@Nonnull ClassPoolAccessor classPoolAccessor) { this.classPoolAccessor = classPoolAccessor; } } private static abstract class CandidatesResolver extends RequiresClassPoolAccessor implements NonNullFunction<Qualifier<?>, Optional<String>> { protected CandidatesResolver(@Nonnull ClassPoolAccessor classPoolAccessor) { super(classPoolAccessor); } @Nonnull protected Iterable<String> calculatePrefixes(@Nonnull Qualifier<?> topQualifier) { String prefix = calculatePrefix(topQualifier); return prefix != null ? singletonList(prefix) : Collections.<String>emptyList(); } @Nullable protected String calculatePrefix(@Nonnull Qualifier<?> topQualifier) { return null; } protected boolean skipResolvingFor(@Nonnull Qualifier<?> candidate) { return false; } @Nonnull @Override public final Optional<String> apply(@Nonnull Qualifier<?> input) { for (CharSequence prefix : calculatePrefixes(input)) { for (Qualifier candidate : input.getTypeCandidates()) { if (skipResolvingFor(candidate)) { continue; } Optional<String> resolvedClass = classPoolAccessor.resolveClass(prefix + candidate.getFullQualifier()); if (resolvedClass.isPresent()) { return resolvedClass; } } } return absent(); } } private static class FullyQualifiedTypeResolver extends CandidatesResolver { public FullyQualifiedTypeResolver(ClassPoolAccessor classPoolAccessor) { super(classPoolAccessor); } @Nonnull @Override protected String calculatePrefix(@Nonnull Qualifier<?> topQualifier) { return ""; } @Override protected boolean skipResolvingFor(@Nonnull Qualifier<?> candidate) { return candidate.isSingleQualifier(); } } private static class InnerTypeResolver implements NonNullFunction<Qualifier<?>, Optional<String>> { @Nonnull @Override public Optional<String> apply(@Nonnull Qualifier<?> typeReference) { Qualifier firstQualifier = typeReference.getFirstQualifier(); for (Node loopNode = typeReference.getNode(); ; ) { Optional<String> reference; if (TypeDeclaration.class.isInstance(loopNode)) { TypeDeclaration typeDeclaration = TypeDeclaration.class.cast(loopNode); reference = resolveInnerReference(firstQualifier, singleton(typeDeclaration)); if (reference.isPresent()) { return reference; } reference = resolveInnerReference(firstQualifier, typeDeclaration.getMembers()); if (reference.isPresent()) { return reference; } } else if (CompilationUnit.class.isInstance(loopNode)) { reference = resolveInnerReference(firstQualifier, CompilationUnit.class.cast(loopNode).getTypes()); if (reference.isPresent()) { return reference; } } loopNode = loopNode.getParentNode(); if (loopNode == null) { return absent(); } } } @Nonnull private Optional<String> resolveInnerReference( @Nonnull Qualifier firstQualifier, @Nullable Iterable<? extends BodyDeclaration> bodyDeclarations) { for (TypeDeclaration typeDeclaration : emptyIfNull(bodyDeclarations).filter(TypeDeclaration.class)) { if (firstQualifier.getName().equals(typeDeclaration.getName())) { return of(resolveReferencedType(firstQualifier, typeDeclaration)); } } return absent(); } @Nonnull private String resolveReferencedType(@Nonnull Qualifier qualifier, @Nonnull TypeDeclaration type) { Qualifier parentQualifier = qualifier.getParentQualifier(); if (parentQualifier != null) { for (TypeDeclaration innerType : emptyIfNull(type.getMembers()).filter(TypeDeclaration.class)) { if (parentQualifier.getName().equals(innerType.getName())) { return resolveReferencedType(parentQualifier, innerType); } } } return getTypeName(type); } } private static class InheritedTypeResolver extends RequiresClassPoolAccessor implements NonNullFunction<Qualifier<?>, Optional<String>> { public InheritedTypeResolver(@Nonnull ClassPoolAccessor classPoolAccessor) { super(classPoolAccessor); } @Nonnull @Override public Optional<String> apply(@Nonnull Qualifier<?> typeReference) { String typeName = getTypeName(typeReference.getNode()); CtClass clazz = getCtClass(classPoolAccessor.getClassPool(), typeName); if (clazz == null) { return absent(); } Qualifier firstQualifier = typeReference.getFirstQualifier(); for (CtClass declaringClazz : getDeclaringClassesOf(clazz)) { Optional<String> inheritedType = resolveInheritedType(clazz, declaringClazz, firstQualifier); if (inheritedType.isPresent()) { return inheritedType; } } return absent(); } @Nonnull private Optional<String> resolveInheritedType(@Nonnull CtClass referencingClazz, @Nonnull CtClass clazz, @Nonnull Qualifier firstQualifier) { @SuppressWarnings("unchecked") Optional<String> result = firstQualifier.examineInheritedType(referencingClazz, clazz); if (result.isPresent()) { return result; } result = checkNestedClasses(referencingClazz, getSuperclassOf(clazz), firstQualifier); if (result.isPresent()) { return result; } for (CtClass interfaceClazz : getInterfacesOf(clazz)) { result = checkNestedClasses(referencingClazz, interfaceClazz, firstQualifier); if (result.isPresent()) { return result; } } return absent(); } @Nonnull private Optional<String> checkNestedClasses(@Nonnull CtClass referencingClazz, @Nullable CtClass clazz, @Nonnull Qualifier firstQualifier) { if (clazz == null || isJavaLangObject(clazz)) { return absent(); } for (CtClass nestedClass : getNestedClassesOf(clazz)) { if (nestedClass.getName().substring(clazz.getName().length() + 1).equals(firstQualifier.getName())) { return resolveNestedType(firstQualifier, nestedClass); } } return resolveInheritedType(referencingClazz, clazz, firstQualifier); } private Optional<String> resolveNestedType(Qualifier qualifier, CtClass clazz) { Qualifier parentQualifier = qualifier.getParentQualifier(); if (parentQualifier != null) { for (CtClass nestedClass : getNestedClassesOf(clazz)) { if (nestedClass.getName().substring(clazz.getName().length() + 1) .equals(parentQualifier.getName())) { return resolveNestedType(parentQualifier, nestedClass); } } } return of(clazz.getName()); } } private static class ImportedTypeResolver extends CandidatesResolver { public ImportedTypeResolver(ClassPoolAccessor classPoolAccessor) { super(classPoolAccessor); } @Nullable @Override protected String calculatePrefix(@Nonnull Qualifier<?> topQualifier) { String firstQualifier = topQualifier.getFirstQualifier().getName(); CompilationUnit compilationUnit = Nodes.getCompilationUnit(topQualifier.getNode()); ImportDeclaration importDeclaration = getOnlyElement(emptyIfNull(compilationUnit.getImports()).filter( and(not(isAsterisk()), refersTo(firstQualifier))), null); if (importDeclaration == null) { return null; } StringBuilder buffy = prepend(importDeclaration.getName(), new StringBuilder()); int beginIndex = buffy.length() - firstQualifier.length(); return beginIndex == 0 ? "" : buffy.replace(beginIndex - 1, buffy.length(), importDeclaration.isStatic() ? "$" : ".").toString(); } } private static class PackageTypeResolver extends CandidatesResolver { public PackageTypeResolver(@Nonnull ClassPoolAccessor classPoolAccessor) { super(classPoolAccessor); } @Nonnull @Override protected String calculatePrefix(@Nonnull Qualifier<?> topQualifier) { PackageDeclaration aPackage = Nodes.getCompilationUnit(topQualifier.getNode()).getPackage(); if (aPackage == null) { return ""; } return prepend(aPackage.getName(), new StringBuilder("")).append(".").toString(); } } private static class AsteriskImportedTypeResolver extends CandidatesResolver { public AsteriskImportedTypeResolver(@Nonnull ClassPoolAccessor classPoolAccessor) { super(classPoolAccessor); } @Nonnull @Override protected Iterable<String> calculatePrefixes(@Nonnull Qualifier<?> topQualifier) { ArrayList<String> asteriskImports = newArrayList(); CompilationUnit compilationUnit = Nodes.getCompilationUnit(topQualifier.getNode()); for (ImportDeclaration importDeclaration : emptyIfNull(compilationUnit.getImports()).filter(isAsterisk())) { StringBuilder buffy = prepend(importDeclaration.getName(), new StringBuilder()); buffy.append(importDeclaration.isStatic() ? '$' : '.'); asteriskImports.add(buffy.toString()); } return asteriskImports; } } private static class JavaLangTypeResolver extends CandidatesResolver { public JavaLangTypeResolver(@Nonnull ClassPoolAccessor classPoolAccessor) { super(classPoolAccessor); } @Nonnull @Override protected String calculatePrefix(@Nonnull Qualifier<?> topQualifier) { return "java.lang."; } } private static class JavaParserSupplier implements NonNullFunction<AnalysisContext, LoadingCache<File, Optional<CompilationUnit>>> { private final Logger logger = LoggerFactory.getLogger(getClass()); private final boolean ignoreParsingErrors; JavaParserSupplier(boolean ignoreParsingErrors) { this.ignoreParsingErrors = ignoreParsingErrors; } @Nonnull @Override @SuppressWarnings("PMD.AvoidCatchingThrowable") // unfortunately, JavaParser throws an Error when parsing fails public LoadingCache<File, Optional<CompilationUnit>> apply(@Nonnull final AnalysisContext analysisContext) { return SequentialLoadingCache.createSingleValueCache(toFunction(new NonNullFunction<File, Optional<CompilationUnit>>() { @Nonnull @Override @SuppressFBWarnings(value = "DM_DEFAULT_ENCODING", justification = "The MavenProject does not provide the proper encoding") public Optional<CompilationUnit> apply(@Nonnull File file) { Reader reader = null; try { reader = analysisContext.getModule().getEncoding() != null ? new InputStreamReader(new FileInputStream(file), analysisContext.getModule().getEncoding()) : new FileReader(file); return of(JavaParser.parse(reader, false)); } catch (Throwable t) { return handleThrowable(file, t); } finally { closeQuietly(reader); } } })); } private Optional<CompilationUnit> handleThrowable(File file, Throwable t) { String message = "Failed to parse [" + file + "]!"; if ((TokenMgrError.class.isInstance(t) || ParseException.class.isInstance(t)) && ignoreParsingErrors) { logger.debug(message, t); return absent(); } if (Error.class.isInstance(t) && !TokenMgrError.class.isInstance(t)) { throw Error.class.cast(t); } throw new RuntimeException(message, t); } } }