/* * Copyright 2015 Lukas Krejci * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package org.revapi.java; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.text.MessageFormat; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.Deque; import java.util.EnumMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Objects; import java.util.ResourceBundle; import java.util.Set; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.lang.model.type.DeclaredType; import javax.tools.ToolProvider; import org.revapi.AnalysisContext; import org.revapi.Difference; import org.revapi.DifferenceAnalyzer; import org.revapi.Element; import org.revapi.Report; import org.revapi.Stats; import org.revapi.java.compilation.CompilationValve; import org.revapi.java.compilation.ProbingEnvironment; import org.revapi.java.model.AnnotationElement; import org.revapi.java.model.FieldElement; import org.revapi.java.model.MethodElement; import org.revapi.java.model.MethodParameterElement; import org.revapi.java.model.TypeElement; import org.revapi.java.spi.Check; import org.revapi.java.spi.JavaElement; import org.revapi.java.spi.JavaModelElement; import org.revapi.java.spi.JavaTypeElement; import org.revapi.java.spi.UseSite; import org.revapi.java.spi.Util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Lukas Krejci * @since 0.1 */ public final class JavaElementDifferenceAnalyzer implements DifferenceAnalyzer { private static final Logger LOG = LoggerFactory.getLogger(JavaElementDifferenceAnalyzer.class); //see #forceClearCompilerCache for what these are private static final Method CLEAR_COMPILER_CACHE; private static final Object SHARED_ZIP_FILE_INDEX_CACHE; static { Method clearCompilerCache = null; Object sharedInstance = null; try { Class<?> zipFileIndexCacheClass = ToolProvider.getSystemToolClassLoader() .loadClass("com.sun.tools.javac.file.ZipFileIndexCache"); clearCompilerCache = zipFileIndexCacheClass.getDeclaredMethod("clearCache"); Method getSharedInstance = zipFileIndexCacheClass.getDeclaredMethod("getSharedInstance"); sharedInstance = getSharedInstance.invoke(null); } catch (Exception e) { LOG.warn("Failed to initialize the force-clearing of javac file caches. We will probably leak resources.", e); } if (clearCompilerCache != null && sharedInstance != null) { CLEAR_COMPILER_CACHE = clearCompilerCache; SHARED_ZIP_FILE_INDEX_CACHE = sharedInstance; } else { CLEAR_COMPILER_CACHE = null; SHARED_ZIP_FILE_INDEX_CACHE = null; } } private final Iterable<Check> checks; private final CompilationValve oldCompilationValve; private final CompilationValve newCompilationValve; private final AnalysisConfiguration analysisConfiguration; private final ResourceBundle messages; private final ProbingEnvironment oldEnvironment; private final ProbingEnvironment newEnvironment; private final Map<Check.Type, List<Check>> checksByInterest; private final Deque<Check.Type> checkTypeStack = new ArrayDeque<>(); // NOTE: this doesn't have to be a stack of lists only because of the fact that annotations // are always sorted as last amongst sibling model elements. // So, when reported for their parent element, we can be sure that there are no more children // coming for given parent. private List<Difference> lastAnnotationResults; public JavaElementDifferenceAnalyzer(AnalysisContext analysisContext, ProbingEnvironment oldEnvironment, CompilationValve oldValve, ProbingEnvironment newEnvironment, CompilationValve newValve, Iterable<Check> checks, AnalysisConfiguration analysisConfiguration) { this.oldCompilationValve = oldValve; this.newCompilationValve = newValve; this.checks = checks; for (Check c : checks) { c.initialize(analysisContext); c.setOldTypeEnvironment(oldEnvironment); c.setNewTypeEnvironment(newEnvironment); } this.analysisConfiguration = analysisConfiguration; messages = ResourceBundle.getBundle("org.revapi.java.messages", analysisContext.getLocale()); this.oldEnvironment = oldEnvironment; this.newEnvironment = newEnvironment; this.checksByInterest = new EnumMap<>(Check.Type.class); for (Check.Type c : Check.Type.values()) { checksByInterest.put(c, new ArrayList<>()); } for (Check c : checks) { for (Check.Type t : c.getInterest()) { List<Check> cs = checksByInterest.get(t); cs.add(c); } } } @Override public void open() { Timing.LOG.debug("Opening difference analyzer."); } @Override public void close() { Timing.LOG.debug("About to close difference analyzer."); oldCompilationValve.removeCompiledResults(); newCompilationValve.removeCompiledResults(); forceClearCompilerCache(); Timing.LOG.debug("Difference analyzer closed."); } @Override public void beginAnalysis(@Nullable Element oldElement, @Nullable Element newElement) { Timing.LOG.trace("Beginning analysis of {} and {}.", oldElement, newElement); if (conforms(oldElement, newElement, TypeElement.class)) { checkTypeStack.push(Check.Type.CLASS); for (Check c : checksByInterest.get(Check.Type.CLASS)) { Stats.of(c.getClass().getName()).start(); c.visitClass(oldElement == null ? null : (TypeElement) oldElement, newElement == null ? null : (TypeElement) newElement); Stats.of(c.getClass().getName()).end(oldElement, newElement); } } else if (conforms(oldElement, newElement, AnnotationElement.class)) { // annotation are always terminal elements and they also always sort as last elements amongst siblings, so // treat them a bit differently if (lastAnnotationResults == null) { lastAnnotationResults = new ArrayList<>(); } //DO NOT push the ANNOTATION type to the checkTypeStack. Annotations are handled differently and this would //lead to the stack corruption and missed problems!!! for (Check c : checksByInterest.get(Check.Type.ANNOTATION)) { Stats.of(c.getClass().getName()).start(); List<Difference> cps = c .visitAnnotation(oldElement == null ? null : (AnnotationElement) oldElement, newElement == null ? null : (AnnotationElement) newElement); if (cps != null) { lastAnnotationResults.addAll(cps); } Stats.of(c.getClass().getName()).end(oldElement, newElement); } } else if (conforms(oldElement, newElement, FieldElement.class)) { doRestrictedCheck((FieldElement) oldElement, (FieldElement) newElement, Check.Type.FIELD); } else if (conforms(oldElement, newElement, MethodElement.class)) { doRestrictedCheck((MethodElement) oldElement, (MethodElement) newElement, Check.Type.METHOD); } else if (conforms(oldElement, newElement, MethodParameterElement.class)) { doRestrictedCheck((MethodParameterElement) oldElement, (MethodParameterElement) newElement, Check.Type.METHOD_PARAMETER); } } private <T extends JavaModelElement> void doRestrictedCheck(T oldElement, T newElement, Check.Type interest) { if (!(isCheckedElsewhere(oldElement, oldEnvironment) && isCheckedElsewhere(newElement, newEnvironment))) { checkTypeStack.push(interest); for (Check c : checksByInterest.get(interest)) { Stats.of(c.getClass().getName()).start(); switch (interest) { case FIELD: c.visitField((FieldElement) oldElement, (FieldElement) newElement); break; case METHOD: c.visitMethod((MethodElement) oldElement, (MethodElement) newElement); break; case METHOD_PARAMETER: c.visitMethodParameter((MethodParameterElement) oldElement, (MethodParameterElement) newElement); break; } Stats.of(c.getClass().getName()).end(oldElement, newElement); } } else { //this is horrible hack - we don't store the annotations on the stack but need a value representing //"ignore what's on the stack because no checks actually happened". //ArrayDeque doesn't support null elements so we have to have something to represent this state. So we //abuse the ANNOTATION check type for this, because it is otherwise not used in the stack. checkTypeStack.push(Check.Type.ANNOTATION); } } @Override public Report endAnalysis(@Nullable Element oldElement, @Nullable Element newElement) { if (conforms(oldElement, newElement, AnnotationElement.class)) { //the annotations are always reported at the parent element return new Report(Collections.<Difference>emptyList(), oldElement, newElement); } List<Difference> differences = new ArrayList<>(); Check.Type lastInterest = checkTypeStack.pop(); //see #doRestrictedCheck for why we use ANNOTATION as "no checks happened"... if (lastInterest != Check.Type.ANNOTATION) { for (Check c : checksByInterest.get(lastInterest)) { List<Difference> p = c.visitEnd(); if (p != null) { differences.addAll(p); } } } if (lastAnnotationResults != null && !lastAnnotationResults.isEmpty()) { differences.addAll(lastAnnotationResults); lastAnnotationResults.clear(); } if (!differences.isEmpty()) { LOG.trace("Detected following problems: {}", differences); } Timing.LOG.trace("Ended analysis of {} and {}.", oldElement, newElement); ListIterator<Difference> it = differences.listIterator(); while (it.hasNext()) { Difference d = it.next(); if (analysisConfiguration.getUseReportingCodes().contains(d.code)) { StringBuilder newDesc = new StringBuilder(d.description == null ? "" : d.description); newDesc.append("\n"); newDesc.append(messages.getString("revapi.java.uses.old")); newDesc.append(" "); appendUses(oldElement, newDesc); newDesc.append("\n"); newDesc.append(messages.getString("revapi.java.uses.new")); newDesc.append(" "); appendUses(newElement, newDesc); d = Difference.builder().addAttachments(d.attachments).addClassifications(d.classification) .withCode(d.code).withName(d.name).withDescription(newDesc.toString()).build(); } it.set(d); } return new Report(differences, oldElement, newElement); } private <T> boolean conforms(Object a, Object b, Class<T> cls) { boolean ca = a == null || cls.isAssignableFrom(a.getClass()); boolean cb = b == null || cls.isAssignableFrom(b.getClass()); return ca && cb; } private void append(StringBuilder bld, TypeAndUseSite typeAndUseSite) { String message; switch (typeAndUseSite.useSite.getUseType()) { case ANNOTATES: message = "revapi.java.uses.annotates"; break; case HAS_TYPE: message = "revapi.java.uses.hasType"; break; case IS_IMPLEMENTED: message = "revapi.java.uses.isImplemented"; break; case IS_INHERITED: message = "revapi.java.uses.isInherited"; break; case IS_THROWN: message = "revapi.java.uses.isThrown"; break; case PARAMETER_TYPE: message = "revapi.java.uses.parameterType"; break; case RETURN_TYPE: message = "revapi.java.uses.returnType"; break; case CONTAINS: message = "revapi.java.uses.contains"; break; case TYPE_PARAMETER_OR_BOUND: message = "revapi.java.uses.typeParameterOrBound"; break; default: throw new AssertionError("Invalid use type: " + typeAndUseSite.useSite.getUseType()); } message = messages.getString(message); message = MessageFormat.format(message, typeAndUseSite.useSite.getSite().getFullHumanReadableString(), Util.toHumanReadableString(typeAndUseSite.type)); bld.append(message); } private void appendUses(Element element, final StringBuilder bld) { LOG.trace("Reporting uses of {}", element); if (element == null) { bld.append("<null>"); return; } while (element != null && !(element instanceof JavaTypeElement)) { element = element.getParent(); } if (element == null) { return; } JavaTypeElement usedType = (JavaTypeElement) element; if (usedType.isInAPI() && !usedType.isInApiThroughUse()) { String message = MessageFormat.format(messages.getString("revapi.java.uses.partOfApi"), usedType.getFullHumanReadableString()); bld.append(message); return; } usedType.visitUseSites(new UseSite.Visitor<Object, Void>() { @Nullable @Override public Object visit(@Nonnull DeclaredType type, @Nonnull UseSite use, @Nullable Void parameter) { if (appendUse(usedType, bld, type, use)) { return Boolean.TRUE; //just a non-null values } return null; } @Nullable @Override public Object end(DeclaredType type, @Nullable Void parameter) { return null; } }, null); } private boolean appendUse(JavaTypeElement usedType, StringBuilder bld, DeclaredType type, UseSite use) { if (!use.getUseType().isMovingToApi()) { return false; } List<TypeAndUseSite> chain = getExamplePathToApiArchive(usedType, type, use); Iterator<TypeAndUseSite> chainIt = chain.iterator(); if (chain.isEmpty()) { if (LOG.isDebugEnabled()) { LOG.debug("Could not find example path to API element for type {} starting with use {}", ((javax.lang.model.element.TypeElement) type.asElement()).getQualifiedName().toString(), use); } return false; } TypeAndUseSite last = null; if (chainIt.hasNext()) { last = chainIt.next(); append(bld, last); } while (chainIt.hasNext()) { bld.append(" <- "); last = chainIt.next(); append(bld, last); } String message = MessageFormat.format(messages.getString("revapi.java.uses.partOfApi"), last.useSite.getSite().getFullHumanReadableString()); bld.append(" (").append(message).append(")"); return true; } private List<TypeAndUseSite> getExamplePathToApiArchive(JavaTypeElement usedType, DeclaredType type, UseSite bottomUse) { ArrayList<TypeAndUseSite> ret = new ArrayList<>(); traverseToApi(usedType, type, bottomUse, ret, new HashSet<>()); return ret; } private boolean traverseToApi(final JavaTypeElement usedType, final DeclaredType type, final UseSite currentUse, final List<TypeAndUseSite> path, final Set<javax.lang.model.element.TypeElement> visitedTypes) { if (!currentUse.getUseType().isMovingToApi()) { return false; } JavaTypeElement ut = findClassOf(currentUse.getSite()); javax.lang.model.element.TypeElement useType = ut.getDeclaringElement(); if (visitedTypes.contains(useType)) { return false; } visitedTypes.add(useType); if (ut.isInAPI() && !ut.isInApiThroughUse() && !ut.equals(usedType)) { //the class is in the primary API path.add(0, new TypeAndUseSite(type, currentUse)); return true; } else { Boolean ret = ut.visitUseSites(new UseSite.Visitor<Boolean, Void>() { @Nullable @Override public Boolean visit(@Nonnull DeclaredType visitedType, @Nonnull UseSite use, @Nullable Void parameter) { if (traverseToApi(usedType, visitedType, use, path, visitedTypes)) { path.add(0, new TypeAndUseSite(type, currentUse)); return true; } return null; } @Nullable @Override public Boolean end(DeclaredType type, @Nullable Void parameter) { return null; } }, null); return ret == null ? false : ret; } } private JavaTypeElement findClassOf(JavaElement element) { while (element != null && !(element instanceof JavaTypeElement)) { element = (JavaElement) element.getParent(); } return (JavaTypeElement) element; } private javax.lang.model.element.TypeElement findTypeOf(javax.lang.model.element.Element element) { while (element != null && !(element.getKind().isClass() || element.getKind().isInterface())) { element = element.getEnclosingElement(); } return (javax.lang.model.element.TypeElement) element; } private boolean isCheckedElsewhere(JavaModelElement element, ProbingEnvironment env) { if (element == null) { return false; } if (!element.isInherited()) { return false; } String elementSig = Util.toUniqueString(element.getModelRepresentation()); String declSig = Util.toUniqueString(element.getDeclaringElement().asType()); if (!Objects.equals(elementSig, declSig)) { return false; } javax.lang.model.element.TypeElement declaringType = findTypeOf(element.getDeclaringElement()); JavaTypeElement declaringClass = env.getTypeMap().get(declaringType); return declaringClass != null && declaringClass.isInAPI(); } //Javac's standard file manager is leaking resources across compilation tasks because it doesn't clear a shared //"zip file index" cache, when it is close()'d. We try to clear it by force. private static void forceClearCompilerCache() { if (CLEAR_COMPILER_CACHE != null && SHARED_ZIP_FILE_INDEX_CACHE != null) { try { CLEAR_COMPILER_CACHE.invoke(SHARED_ZIP_FILE_INDEX_CACHE); } catch (IllegalAccessException | InvocationTargetException e) { LOG.warn("Failed to force-clear compiler caches, even though it should have been possible." + "This will probably leak memory", e); } } } private static class TypeAndUseSite { final DeclaredType type; final UseSite useSite; public TypeAndUseSite(DeclaredType type, UseSite useSite) { this.type = type; this.useSite = useSite; } } }