package de.is24.deadcode4j.analyzer; import de.is24.deadcode4j.AnalysisContext; import de.is24.deadcode4j.analyzer.javassist.ClassPathFilter; import de.is24.guava.NonNullFunction; import javassist.ClassPool; import javassist.CtClass; import javassist.bytecode.annotation.Annotation; import javax.annotation.Nonnull; import java.lang.annotation.Inherited; import java.util.List; import java.util.Set; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Sets.newHashSet; import static de.is24.deadcode4j.analyzer.javassist.ClassPoolAccessor.classPoolAccessorFor; import static de.is24.javassist.CtClasses.*; import static java.lang.annotation.ElementType.PACKAGE; import static java.lang.annotation.ElementType.TYPE; import static java.util.Collections.disjoint; import static java.util.Collections.emptySet; /** * Serves as a base class with which to mark classes as being in use if they carry one of the specified annotations. * * @since 1.3 */ public abstract class AnnotationsAnalyzer extends ByteCodeAnalyzer { private static final Set<String> DEAD_ENDS = newHashSet( "java.lang.annotation.Documented", "java.lang.annotation.Inherited", "java.lang.annotation.Retention", "java.lang.annotation.Target"); private final String dependerId; private final NonNullFunction<AnalysisContext, Set<String>> supplyAnnotationsFoundInClassPath; private final NonNullFunction<AnalysisContext, List<String>> supplyAnnotationsMarkedAsInherited = new NonNullFunction<AnalysisContext, List<String>>() { @Nonnull @Override public List<String> apply(@Nonnull AnalysisContext analysisContext) { List<String> inheritedAnnotations = newArrayList(); ClassPool classPool = classPoolAccessorFor(analysisContext).getClassPool(); for (String annotation : getAnnotationsFoundInClassPath(analysisContext)) { CtClass annotationClazz = classPool.getOrNull(annotation); if (annotationClazz == null) { logger.debug("Annotation [{}] cannot be found on the class path; skipping detection", annotation); continue; } try { if (annotationClazz.getAnnotation(Inherited.class) != null) { inheritedAnnotations.add(annotation); } } catch (ClassNotFoundException e) { logger.debug("@Inherited is not available; this is quite disturbing."); } } logger.debug("Found those inheritable annotations: {}", inheritedAnnotations); return inheritedAnnotations; } }; private AnnotationsAnalyzer(@Nonnull String dependerId, @Nonnull Set<String> annotations) { checkArgument(!annotations.isEmpty(), "annotations cannot by empty!"); this.dependerId = dependerId; this.supplyAnnotationsFoundInClassPath = new ClassPathFilter(annotations); } /** * Creates a new <code>AnnotationsAnalyzer</code>. * * @param dependerId a description of the <i>depending entity</i> with which to * call {@link de.is24.deadcode4j.AnalysisContext#addDependencies(String, Iterable)} * @param annotations a list of fully qualified (annotation) class names indicating a class is still in use * @since 1.3 */ protected AnnotationsAnalyzer(@Nonnull String dependerId, @Nonnull Iterable<String> annotations) { this(dependerId, newHashSet(annotations)); } /** * Creates a new <code>AnnotationsAnalyzer</code>. * * @param dependerId a description of the <i>depending entity</i> with which to * call {@link de.is24.deadcode4j.AnalysisContext#addDependencies(String, Iterable)} * @param annotations a list of fully qualified (annotation) class names indicating a class is still in use * @since 1.4 */ protected AnnotationsAnalyzer(@Nonnull String dependerId, @Nonnull String... annotations) { this(dependerId, newHashSet(annotations)); } @Override protected final void analyzeClass(@Nonnull AnalysisContext analysisContext, @Nonnull CtClass clazz) { Set<String> availableAnnotations = getAnnotationsFoundInClassPath(analysisContext); if (availableAnnotations.isEmpty()) { return; } String className = clazz.getName(); analysisContext.addAnalyzedClass(className); Set<String> allAnnotations = newHashSet(); addAnnotations(clazz, allAnnotations); allAnnotations.addAll(getInheritedAnnotations(analysisContext, clazz)); if (!disjoint(availableAnnotations, allAnnotations)) { analysisContext.addDependencies(this.dependerId, className); } } private void addAnnotations(@Nonnull CtClass clazz, Set<String> knownAnnotations) { for (Annotation annotation : getAnnotations(clazz, PACKAGE, TYPE)) { String annotationClassName = annotation.getTypeName(); if (!knownAnnotations.add(annotationClassName)) { continue; } if (DEAD_ENDS.contains(annotationClassName)) { continue; } CtClass annotationClazz = getCtClass(clazz.getClassPool(), annotationClassName); if (annotationClazz != null) { addAnnotations(annotationClazz, knownAnnotations); } } } @Nonnull private Set<String> getInheritedAnnotations(@Nonnull final AnalysisContext analysisContext, @Nonnull final CtClass clazz) { List<String> annotationsMarkedAsInherited = getAnnotationsMarkedAsInherited(analysisContext); if (annotationsMarkedAsInherited.isEmpty()) { return emptySet(); } Set<String> inheritedAnnotations = newHashSet(); CtClass loopClass = getSuperclassOf(clazz); while (loopClass != null && !isJavaLangObject(loopClass)) { for (Annotation annotation : getAnnotations(loopClass, PACKAGE, TYPE)) { inheritedAnnotations.add(annotation.getTypeName()); } loopClass = getSuperclassOf(loopClass); } return inheritedAnnotations; } @Nonnull protected final Set<String> getAnnotationsFoundInClassPath(@Nonnull AnalysisContext analysisContext) { return analysisContext.getOrCreateCacheEntry(getClass().getName() + "|knownAnnotations", supplyAnnotationsFoundInClassPath); } @Nonnull private List<String> getAnnotationsMarkedAsInherited(@Nonnull AnalysisContext analysisContext) { return analysisContext.getOrCreateCacheEntry(getClass().getName() + "|inheritableAnnotations", supplyAnnotationsMarkedAsInherited); } }