Bazel Blog

Strict Java Deps and `unused_deps`

This blog post describes how Bazel implements "strict deps" for Java compilations ("SJD"), and how it is leveraged in unused_deps, a tool to remove unused dependencies. It is my hope this knowledge will help write rules for similar JVM-based languages such as Scala and Kotlin.

What's "Strict Deps"?

By "strict deps", we loosely mean that all directly used classes are loaded from jars provided by a rule's direct dependencies. In other words, if a Java file mentions another class, then it must be reflected in the BUILD file.
(the concept is similar to Buck's "first-order dependencies")

class A {
  void foo(B b) { }  // <---- B is used, therefore we must depend on :B
}
java_library(
    name = "A",
    srcs = ["A.java"],
    deps = [":B"], # <--- this dependency is required
)

Note that any dependencies of B itself are not listed in the deps of A.

The initial motivation for SJD was the ability to remove unused dependencies.

Consider a dependency chain A -> B -> C without strict deps. It's impossible to know if C can be removed from B's deps just by looking at it - all transitive users of B must be considered to make this decision. Strict deps mandates that if A also uses C, it must depend on it directly, therefore making it safe to remove C from B's deps.

unused_deps

unused_deps is a tool to remove dependencies that aren't needed from a java_library (and other Java rules).

When Bazel builds a Java rule :Foo on the command line, it writes two files - Foo.jar-2.params and Foo.jdeps. The former contains the command-line arguments to the Java compiler and the latter contains a serialized src/main/protobuf/deps.proto which specifies which jars were loaded during compilation.

unused_deps loads the two files and figures out which rules aren't needed in a rule's deps attribute, and emits Buildozer commands to delete them.

Implementation

Bazel always passes the entire transitive classpath to javac, not only the direct classpath. This frees the user from having to aggregate their transitive dependencies manually. In other words, javac never fails because of a missing symbol, as long as every rule specifies its direct dependencies. This is done at JavaCompileAction.java#L729.

SJD is enforced by a compiler plugin implemented in StrictJavaDepsPlugin.java. When Bazel constructs the command-line to javac, it specifies which jars come from indirect dependencies using the --indirect_dependency flag. The plugin then walks the .java sources and reports any symbols that come from indirect jars.
(A sketch of how it works: The compiler stores the name of the jar from which a symbol was loaded. The plugin walks the AST after the type annotation phase, and stops at each 'type expression', then checks whether the originating jar is an --indirect_dependency. If it is the plugin generates an error message. The message includes the missing direct dependency to add.)

This approach has the advantage that violations are easy to fix - Bazel tells the user exactly what to do.

Summary

  • Bazel passes all jars from the transitive dependencies of a rule.
  • Bazel notifies the SJD compiler plugin which jars are indirect.
  • During compilation, the compiler plugin reports any symbol mentioned in the Java file that is loaded from an indirect jar.