diff options
Diffstat (limited to 'src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins/dependency/StrictJavaDepsPlugin.java')
-rw-r--r-- | src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins/dependency/StrictJavaDepsPlugin.java | 397 |
1 files changed, 397 insertions, 0 deletions
diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins/dependency/StrictJavaDepsPlugin.java b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins/dependency/StrictJavaDepsPlugin.java new file mode 100644 index 0000000000..1bdc73b621 --- /dev/null +++ b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins/dependency/StrictJavaDepsPlugin.java @@ -0,0 +1,397 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// 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 com.google.devtools.build.buildjar.javac.plugins.dependency; + +import static com.google.devtools.build.buildjar.javac.plugins.dependency.DependencyModule.StrictJavaDeps.ERROR; +import static com.google.devtools.build.buildjar.javac.plugins.dependency.ImplicitDependencyExtractor.getPlatformClasses; +import static com.google.devtools.build.buildjar.javac.plugins.dependency.ImplicitDependencyExtractor.unwrapFileObject; + +import com.google.common.annotations.VisibleForTesting; +import com.google.devtools.build.buildjar.javac.plugins.BlazeJavaCompilerPlugin; +import com.google.devtools.build.lib.view.proto.Deps; +import com.google.devtools.build.lib.view.proto.Deps.Dependency; + +import com.sun.tools.javac.code.Flags; +import com.sun.tools.javac.code.Kinds; +import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.code.Symbol.ClassSymbol; +import com.sun.tools.javac.comp.AttrContext; +import com.sun.tools.javac.comp.Env; +import com.sun.tools.javac.file.ZipArchive; +import com.sun.tools.javac.file.ZipFileIndexArchive; +import com.sun.tools.javac.main.JavaCompiler; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.tree.TreeScanner; +import com.sun.tools.javac.util.Context; +import com.sun.tools.javac.util.Log; +import com.sun.tools.javac.util.Log.WriterKind; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.text.MessageFormat; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.TreeSet; + +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; + +/** + * A plugin for BlazeJavaCompiler that checks for types referenced directly + * in the source, but included through transitive dependencies. To get this + * information, we hook into the type attribution phase of the BlazeJavaCompiler + * (thus the overhead is another tree scan with the classic visitor). The + * constructor takes a map from jar names to target names, only for the jars that + * come from transitive dependencies (Blaze computes this information). + */ +public final class StrictJavaDepsPlugin extends BlazeJavaCompilerPlugin { + + @VisibleForTesting + static String targetMapping = + "com/google/devtools/build/buildjar/javac/resources/target.properties"; + + private static final String FIX_MESSAGE = + "%s** Command to add missing strict dependencies:%s\n" + + " add_dep %s%s\n\n"; + + private static final boolean USE_COLOR = true; + private ImplicitDependencyExtractor implicitDependencyExtractor; + private CheckingTreeScanner checkingTreeScanner; + private DependencyModule dependencyModule; + + /** Marks seen compilation toplevels and their import sections */ + private final Set<JCTree.JCCompilationUnit> toplevels; + /** Marks seen ASTs */ + private final Set<JCTree> trees; + + /** Computed missing dependencies */ + private final Set<String> missingTargets; + + private static Properties targetMap; + + private PrintWriter errWriter; + + /** + * On top of javac, we keep Blaze-specific information in the form of two + * maps. Both map jars (exactly as they appear on the classpath) to target + * names, one is used for direct dependencies, the other for the transitive + * dependencies. + * + * <p>This enables the detection of dependency issues. For instance, when a + * type com.Foo is referenced in the source and it's coming from an indirect + * dependency, we emit a warning flagging that dependency. Also, we can check + * whether the direct dependencies were actually necessary, i.e. if their + * associated jars were used at all for looking up class definitions. + */ + public StrictJavaDepsPlugin(DependencyModule dependencyModule) { + this.dependencyModule = dependencyModule; + toplevels = new HashSet<>(); + trees = new HashSet<>(); + targetMap = new Properties(); + missingTargets = new TreeSet<>(); + } + + @Override + public void init(Context context, Log log, JavaCompiler compiler) { + super.init(context, log, compiler); + errWriter = log.getWriter(WriterKind.ERROR); + JavaFileManager fileManager = context.get(JavaFileManager.class); + implicitDependencyExtractor = new ImplicitDependencyExtractor( + dependencyModule.getUsedClasspath(), dependencyModule.getImplicitDependenciesMap(), + fileManager); + checkingTreeScanner = context.get(CheckingTreeScanner.class); + if (checkingTreeScanner == null) { + Set<JavaFileObject> platformClasses = getPlatformClasses(fileManager); + checkingTreeScanner = new CheckingTreeScanner( + dependencyModule, log, missingTargets, platformClasses); + context.put(CheckingTreeScanner.class, checkingTreeScanner); + } + initTargetMap(); + } + + private void initTargetMap() { + try (InputStream is = getClass().getClassLoader().getResourceAsStream(targetMapping)) { + if (is != null) { + targetMap.load(is); + } + } catch (IOException ex) { + log.warning("Error loading Strict Java Deps mapping file: " + targetMapping, ex); + } + } + + /** + * We want to make another pass over the AST and "type-check" the usage + * of direct/transitive dependencies after the type attribution phase. + */ + @Override + public void postAttribute(Env<AttrContext> env) { + // We want to generate warnings/errors as if we were javac, and in order to + // use the internal log properly, we need to set its current source file + // information. The useSource call does just that, and is a common pattern + // from JavaCompiler: set source to current file and save the previous + // value, do work and generate warnings, reset source. + JavaFileObject prev = log.useSource( + env.enclClass.sym.sourcefile != null + ? env.enclClass.sym.sourcefile + : env.toplevel.sourcefile); + if (trees.add(env.tree)) { + checkingTreeScanner.scan(env.tree); + } + if (toplevels.add(env.toplevel)) { + checkingTreeScanner.scan(env.toplevel.getImports()); + } + log.useSource(prev); + } + + @Override + public void finish() { + implicitDependencyExtractor.accumulate(context, checkingTreeScanner.getSeenClasses()); + + if (!missingTargets.isEmpty()) { + StringBuilder missingTargetsStr = new StringBuilder(); + for (String target : missingTargets) { + missingTargetsStr.append(target); + missingTargetsStr.append(" "); + } + errWriter.print(String.format(FIX_MESSAGE, + USE_COLOR ? "\033[35m\033[1m" : "", + USE_COLOR ? "\033[0m" : "", + missingTargetsStr.toString(), + dependencyModule.getTargetLabel())); + } + } + + /** + * An AST visitor that implements our strict_java_deps checks. For now, it + * only emits warnings for types loaded from jar files provided by transitive + * (indirect) dependencies. Each type is considered only once, so at most one + * warning is generated for it. + */ + private static class CheckingTreeScanner extends TreeScanner { + + private static final String transitiveDepMessage = + "[strict] Using type {0} from an indirect dependency (TOOL_INFO: \"{1}\"). " + + "See command below **"; + + /** Lookup for jars coming from transitive dependencies */ + private final Map<String, String> indirectJarsToTargets; + + /** All error reporting is done through javac's log, */ + private final Log log; + + /** The strict_java_deps mode */ + private final DependencyModule.StrictJavaDeps strictJavaDepsMode; + + /** Missing targets */ + private final Set<String> missingTargets; + + /** Collect seen direct dependencies and their associated information */ + private final Map<String, Deps.Dependency> directDependenciesMap; + + /** We only emit one warning/error per class symbol */ + private final Set<ClassSymbol> seenClasses = new HashSet<>(); + private final Set<String> seenTargets = new HashSet<>(); + + /** The set of classes on the compilation bootclasspath. */ + private final Set<JavaFileObject> platformClasses; + + public CheckingTreeScanner(DependencyModule dependencyModule, Log log, + Set<String> missingTargets, Set<JavaFileObject> platformClasses) { + this.indirectJarsToTargets = dependencyModule.getIndirectMapping(); + this.strictJavaDepsMode = dependencyModule.getStrictJavaDeps(); + this.log = log; + this.missingTargets = missingTargets; + this.directDependenciesMap = dependencyModule.getExplicitDependenciesMap(); + this.platformClasses = platformClasses; + } + + Set<ClassSymbol> getSeenClasses() { + return seenClasses; + } + + /** + * Checks an AST node denoting a class type against direct/transitive + * dependencies. + */ + private void checkTypeLiteral(JCTree node) { + if (node == null || node.type.tsym == null) { + return; + } + + Symbol.TypeSymbol sym = node.type.tsym; + String jarName = getJarName(sym.enclClass(), platformClasses); + + // If this type symbol comes from a class file loaded from a jar, check + // whether that jar was a direct dependency and error out otherwise. + if (jarName != null && seenClasses.add(sym.enclClass())) { + collectExplicitDependency(jarName, node, sym); + } + } + + /** + * Marks the provided dependency as a direct/explicit dependency. Additionally, if + * strict_java_deps is enabled, it emits a [strict] compiler warning/error (behavior to be soon + * replaced by the more complete Blaze implementation). + */ + private void collectExplicitDependency(String jarName, JCTree node, Symbol.TypeSymbol sym) { + if (strictJavaDepsMode.isEnabled()) { + // Does it make sense to emit a warning/error for this pair of (type, target)? + // We want to emit only one error/warning per target. + String target = indirectJarsToTargets.get(jarName); + if (target != null && seenTargets.add(target)) { + String canonicalTargetName = canonicalizeTarget(target); + missingTargets.add(canonicalTargetName); + if (strictJavaDepsMode == ERROR) { + log.error(node.pos, "proc.messager", + MessageFormat.format(transitiveDepMessage, sym, canonicalTargetName)); + } else { + log.warning(node.pos, "proc.messager", + MessageFormat.format(transitiveDepMessage, sym, canonicalTargetName)); + } + } + } + + if (!directDependenciesMap.containsKey(jarName)) { + // Also update the dependency proto + Dependency dep = Dependency.newBuilder() + .setPath(jarName) + .setKind(Dependency.Kind.EXPLICIT) + .build(); + directDependenciesMap.put(jarName, dep); + } + } + + @Override + public void visitMethodDef(JCTree.JCMethodDecl method) { + if ((method.mods.flags & Flags.GENERATEDCONSTR) != 0) { + // If this is the constructor for an anonymous inner class, refrain from checking the + // compiler-generated method signature. Don't skip scanning the method body though, there + // might have been an anonymous initializer which still needs to be checked. + scan(method.body); + } else { + super.visitMethodDef(method); + } + } + + /** + * Visits an identifier in the AST. We only care about type symbols. + */ + @Override + public void visitIdent(JCTree.JCIdent tree) { + if (tree.sym != null && tree.sym.kind == Kinds.TYP) { + checkTypeLiteral(tree); + } + } + + /** + * Visits a field selection in the AST. We care because in some cases types + * may appear fully qualified and only inside a field selection + * (e.g., "com.foo.Bar.X", we want to catch the reference to Bar). + */ + @Override + public void visitSelect(JCTree.JCFieldAccess tree) { + scan(tree.selected); + if (tree.sym != null && tree.sym.kind == Kinds.TYP) { + checkTypeLiteral(tree); + } + } + + /** + * Visits an import statement. Static imports must not be omitted, as they + * are the only place we'll see the containing class references. + */ + @Override + public void visitImport(JCTree.JCImport tree) { + if (tree.isStatic()) { + scan(tree.getQualifiedIdentifier()); + } + } + } + + /** + * Returns the canonical version of the target name. Package private for testing. + */ + static String canonicalizeTarget(String target) { + String replacement = targetMap.getProperty(target); + if (replacement != null) { + return replacement; + } + int colonIndex = target.indexOf(':'); + if (colonIndex == -1) { + // No ':' in target, nothing to do. + return target; + } + int lastSlash = target.lastIndexOf('/', colonIndex); + if (lastSlash == -1) { + // No '/' or target is actually a filename in label format, return unmodified. + return target; + } + String packageName = target.substring(lastSlash + 1, colonIndex); + String suffix = target.substring(colonIndex + 1); + if (packageName.equals(suffix)) { + // target ends in "/something:something", canonicalize. + return target.substring(0, colonIndex); + } + return target; + } + + /** + * Returns the name of the jar file from which the given class symbol was + * loaded, if available, and null otherwise. Implicitly filters out jars + * from the compilation bootclasspath. + * @param platformClasses classes on javac's bootclasspath + */ + static String getJarName(ClassSymbol classSymbol, Set<JavaFileObject> platformClasses) { + if (classSymbol != null) { + // Ignore symbols that appear in the sourcepath: + if (haveSourceForSymbol(classSymbol)) { + return null; + } + JavaFileObject classfile = unwrapFileObject(classSymbol.classfile); + if (classfile instanceof ZipArchive.ZipFileObject + || classfile instanceof ZipFileIndexArchive.ZipFileIndexFileObject) { + String name = classfile.getName(); + // Here name will be something like blaze-out/.../com/foo/libfoo.jar(Bar.class) + String jarName = name.split("\\(")[0]; + if (!platformClasses.contains(classfile)) { + return jarName; + } + } + } + return null; + } + + /** + * Returns true if the given classSymbol corresponds to one of the sources being compiled. + */ + private static boolean haveSourceForSymbol(ClassSymbol classSymbol) { + if (classSymbol.sourcefile == null) { + return false; + } + + try { + // The classreader uses metadata to populate the symbol's sourcefile with a fake file object. + // Call getLastModified() to check if it's a real file: + classSymbol.sourcefile.getLastModified(); + } catch (UnsupportedOperationException e) { + return false; + } + + return true; + } +} |