// Copyright 2014 The Bazel Authors. 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.common.collect.Iterables.getOnlyElement;
import static com.google.devtools.build.buildjar.javac.plugins.dependency.DependencyModule.StrictJavaDeps.ERROR;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Ordering;
import com.google.devtools.build.buildjar.JarOwner;
import com.google.devtools.build.buildjar.javac.plugins.BlazeJavaCompilerPlugin;
import com.google.devtools.build.buildjar.javac.plugins.dependency.DependencyModule.StrictJavaDeps;
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.main.JavaCompiler;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeInfo;
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.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.annotation.Generated;
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 boolean USE_COLOR = true;
private ImplicitDependencyExtractor implicitDependencyExtractor;
private CheckingTreeScanner checkingTreeScanner;
private final 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<JarOwner> missingTargets;
private static Properties targetMap;
private JavaFileManager fileManager;
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 HashSet<>();
}
@Override
public void init(Context context, Log log, JavaCompiler compiler) {
super.init(context, log, compiler);
errWriter = log.getWriter(WriterKind.ERROR);
fileManager = context.get(JavaFileManager.class);
implicitDependencyExtractor =
new ImplicitDependencyExtractor(
dependencyModule.getUsedClasspath(),
dependencyModule.getImplicitDependenciesMap(),
dependencyModule.getPlatformJars());
checkingTreeScanner = context.get(CheckingTreeScanner.class);
if (checkingTreeScanner == null) {
Set<String> platformJars = dependencyModule.getPlatformJars();
checkingTreeScanner =
new CheckingTreeScanner(dependencyModule, log, missingTargets, platformJars, fileManager);
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) {
JavaFileObject previousSource =
log.useSource(
env.enclClass.sym.sourcefile != null
? env.enclClass.sym.sourcefile
: env.toplevel.sourcefile);
boolean previousExemption = checkingTreeScanner.isStrictDepsExempt;
try {
ProcessorDependencyMode mode = isAnnotationProcessorExempt(env.toplevel);
if (mode == ProcessorDependencyMode.EXEMPT_NORECORD) {
return;
}
checkingTreeScanner.isStrictDepsExempt |= mode == ProcessorDependencyMode.EXEMPT_RECORD;
if (trees.add(env.tree)) {
checkingTreeScanner.scan(env.tree);
}
if (toplevels.add(env.toplevel)) {
checkingTreeScanner.scan(env.toplevel.getImports());
dependencyModule.addPackage(env.toplevel.packge);
}
} finally {
checkingTreeScanner.isStrictDepsExempt = previousExemption;
log.useSource(previousSource);
}
}
@Override
public void finish() {
implicitDependencyExtractor.accumulate(context, checkingTreeScanner.getSeenClasses());
if (!missingTargets.isEmpty()) {
String canonicalizedLabel =
dependencyModule.getTargetLabel() == null
? null
// we don't use the target mapping for the target, just the missing deps
: canonicalizeTarget(dependencyModule.getTargetLabel());
List<JarOwner> canonicalizedMissing = new ArrayList<>();
for (JarOwner owner :
Ordering.natural().onResultOf(JarOwner.LABEL).immutableSortedCopy(missingTargets)) {
// for dependencies that are missing we canonicalize and remap the target so we don't
// suggest private build labels.
String actualTarget = canonicalizeTarget(remapTarget(owner.label()));
canonicalizedMissing.add(JarOwner.create(actualTarget, owner.aspect()));
}
errWriter.print(
dependencyModule
.getFixMessage()
.get(canonicalizedMissing, canonicalizedLabel, USE_COLOR));
}
}
/**
* 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 TRANSITIVE_DEP_MESSAGE =
"[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, JarOwner> indirectJarsToTargets;
/** All error reporting is done through javac's log, */
private final Log log;
/** The compilation's file manager. */
private final JavaFileManager fileManager;
/** The strict_java_deps mode */
private final StrictJavaDeps strictJavaDepsMode;
/** Missing targets */
private final Set<JarOwner> 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<JarOwner> seenTargets = new HashSet<>();
/** The set of jars on the compilation bootclasspath. */
private final Set<String> platformJars;
/** Was the node being visited generated by an exempt annotation processor? */
private boolean isStrictDepsExempt = false;
public CheckingTreeScanner(
DependencyModule dependencyModule,
Log log,
Set<JarOwner> missingTargets,
Set<String> platformJars,
JavaFileManager fileManager) {
this.indirectJarsToTargets = dependencyModule.getIndirectMapping();
this.strictJavaDepsMode = dependencyModule.getStrictJavaDeps();
this.log = log;
this.missingTargets = missingTargets;
this.directDependenciesMap = dependencyModule.getExplicitDependenciesMap();
this.platformJars = platformJars;
this.fileManager = fileManager;
}
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(fileManager, sym.enclClass(), platformJars);
// 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() && !isStrictDepsExempt) {
// Does it make sense to emit a warning/error for this pair of (type, owner)?
// We want to emit only one error/warning per owner.
JarOwner owner = indirectJarsToTargets.get(jarName);
if (owner != null && seenTargets.add(owner)) {
// owner is of the form "//label/of:rule <Aspect name>" where <Aspect name> is optional.
String canonicalTargetName = canonicalizeTarget(remapTarget(owner.label()));
missingTargets.add(owner);
String toolInfo =
owner.aspect() == null
? canonicalTargetName
: String.format("%s wrapped in %s", canonicalTargetName, owner.aspect());
if (strictJavaDepsMode == ERROR) {
log.error(
node.pos,
"proc.messager",
MessageFormat.format(TRANSITIVE_DEP_MESSAGE, sym, toolInfo));
} else {
log.warning(
node.pos,
"proc.messager",
MessageFormat.format(TRANSITIVE_DEP_MESSAGE, sym, toolInfo));
}
}
}
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.Kind.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.Kind.TYP) {
checkTypeLiteral(tree);
}
}
@Override
public void visitLambda(JCTree.JCLambda tree) {
if (tree.paramKind != JCTree.JCLambda.ParameterKind.IMPLICIT) {
// don't record type uses for implicitly typed lambda parameters
scan(tree.params);
}
scan(tree.body);
}
}
private static final String DAGGER_PROCESSOR_PREFIX = "dagger.";
enum ProcessorDependencyMode {
DEFAULT,
EXEMPT_RECORD,
EXEMPT_NORECORD;
}
/**
* Returns true if the compilation unit contains a single top-level class generated by an exempt
* annotation processor (according to its {@link @Generated} annotation).
*
* <p>Annotation processors are expected to never generate more than one top level class, as
* required by the style guide.
*/
public ProcessorDependencyMode isAnnotationProcessorExempt(JCTree.JCCompilationUnit unit) {
if (unit.getTypeDecls().size() != 1) {
return ProcessorDependencyMode.DEFAULT;
}
Symbol sym = TreeInfo.symbolFor(getOnlyElement(unit.getTypeDecls()));
if (sym == null) {
return ProcessorDependencyMode.DEFAULT;
}
Generated generated = sym.getAnnotation(Generated.class);
if (generated == null) {
return ProcessorDependencyMode.DEFAULT;
}
for (String value : generated.value()) {
// Relax strict deps for dagger-generated code (b/17979436).
if (value.startsWith(DAGGER_PROCESSOR_PREFIX)) {
return ProcessorDependencyMode.EXEMPT_NORECORD;
}
if (dependencyModule.getExemptGenerators().contains(value)) {
return ProcessorDependencyMode.EXEMPT_RECORD;
}
}
return ProcessorDependencyMode.DEFAULT;
}
/** Replace the given target with a configured replacement. Package private for testing. */
static String remapTarget(String target) {
String replacement = targetMap.getProperty(target);
if (replacement != null) {
return replacement;
}
return target;
}
/** Returns the canonical version of the target name. Package private for testing. */
static String canonicalizeTarget(String target) {
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 platformJars jars on javac's bootclasspath
*/
static String getJarName(
JavaFileManager fileManager, ClassSymbol classSymbol, Set<String> platformJars) {
if (classSymbol == null) {
return null;
}
// Ignore symbols that appear in the sourcepath:
if (haveSourceForSymbol(classSymbol)) {
return null;
}
JavaFileObject classfile = classSymbol.classfile;
String name = ImplicitDependencyExtractor.getJarName(classfile);
if (name == null) {
return null;
}
// Filter out classes on bootclasspath
if (platformJars.contains(name)) {
return null;
}
return name;
}
/** 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;
}
}