An abstraction over the Java Compiler API focused on analysis and refactoring tasks.
import me.djmm.java.code_analysis.*;
// Define sources
var sources = List.of(
SourceText.of("com/example/Greeter.java", """
package com.example;
public class Greeter {
public String greet(String name) {
return "Hello, " + name;
}
}
"""),
SourceText.of("com/example/Main.java", """
package com.example;
public class Main {
public static void main(String[] args) {
System.out.println(new Greeter().greet("world"));
}
}
""")
);
// Compile
var compilation = Compilation.create(sources);
// Check diagnostics
for (var diag : compilation.diagnostics()) {
System.out.println(diag.getMessage(null));
}
// Access syntax trees
for (var entry : compilation.syntaxTrees().entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue().getKind());
}
// Use the semantic model for type-level queries
var model = compilation.semanticModel();
var trees = model.trees();
var elements = model.elements();| Type | Purpose |
|---|---|
SourceText |
Immutable representation of a source file (URI + content + version) |
Compilation |
Immutable result of compiling sources — syntax trees, diagnostics, semantic model |
IncrementalCompiler |
Stateful compiler that reuses javac state across compilations for fast re-compilation |
CompilerOptions |
Configuration for compilation (classpath, compiler flags) |
SemanticModel |
Access to javac's Trees, Elements, and Types for semantic queries |
CompilationPhase |
Tracks compilation progress (PARSED, ANALYZED) |
For interactive scenarios (language servers, live analysis) where the same set of
sources is compiled repeatedly with small changes, use IncrementalCompiler. It
reuses javac's internal Context — JDK symbol tables, module system state, and
type caches — so subsequent compilations skip cold-start overhead.
import me.djmm.java.code_analysis.*;
import java.net.URI;
try (var compiler = IncrementalCompiler.create()) {
// Seed the compiler with initial sources
compiler.addOrUpdateSource(SourceText.of("com/example/Foo.java",
"package com.example; public class Foo { public int value() { return 1; } }"));
compiler.addOrUpdateSource(SourceText.of("com/example/Bar.java",
"package com.example; public class Bar { int x = new Foo().value(); }"));
// First compilation — pays the cold-start cost once
var first = compiler.compile();
// Edit Foo.java — bump the version and update
compiler.addOrUpdateSource(new SourceText(
URI.create("mem:///com/example/Foo.java"),
"package com.example; public class Foo { public int value() { return 42; } }",
1
));
// Re-compile — reuses cached javac state
var second = compiler.compile();
assert second != first; // fresh result
assert second.diagnostics().isEmpty(); // still valid
// No changes since last compile — returns the cached result (same identity)
var third = compiler.compile();
assert third == second;
// Remove a source
compiler.removeSource(URI.create("mem:///com/example/Bar.java"));
var fourth = compiler.compile();
// Replace the entire source set atomically
compiler.setSources(List.of(
SourceText.of("com/example/NewFile.java", "package com.example; class NewFile {}")
));
var fifth = compiler.compile();
}IncrementalCompiler uses a generation counter to determine when re-compilation
is needed. Mutation methods only bump the generation when something actually
changes:
addOrUpdateSource(src)— no-op ifsrcequals the existing entry (same URI, content, and version). Bumps the generation otherwise.removeSource(uri)— no-op if the URI is not tracked.setSources(collection)— no-op if the resulting map equals the current one.
compile() returns the cached Compilation (same object identity) when the
generation has not changed since the last call.
Each successful compile() call supersedes the previous compilation. The
returned Compilation object itself remains valid — its syntax trees,
diagnostics, and other immutable state can be read at any time. However, the
SemanticModel from a superseded Compilation must not be used, because it
holds references to javac state that has been reset:
var r1 = compiler.compile();
var model1 = r1.semanticModel(); // valid
var r2 = compiler.compile(); // supersedes r1
var model2 = r2.semanticModel(); // valid
// model1.trees() ← DO NOT — its underlying task has been torn down
r1.diagnostics(); // still fine — immutable snapshotClose the compiler when finished to release resources deterministically. The try-with-resources pattern shown above is the recommended usage.
IncrementalCompiler is not thread-safe. All method calls on a single
instance must be serialised by the caller. Concurrent compilations require
separate IncrementalCompiler instances.
The library targets --release 21 bytecode and is validated against the
following JDK runtimes:
| Runtime | Status |
|---|---|
| JDK 21 (LTS) | Tested |
| JDK 25 (LTS) | Tested |
Because code_analysis calls internal com.sun.tools.javac.* APIs, behaviour
can shift between JDK feature releases (e.g. Log.clear() changed from
package-private to public in JDK 25). Every JUnit 5 test is therefore
executed once per supported runtime.
# Runs every test on every supported JDK in a single invocation:
bazel test //code_analysis:testsThe Bazel test suite //code_analysis:tests fans out via a Starlark transition
(see //:build_defs/multi_jdk_test.bzl) into
one child target per JDK — for example:
//code_analysis:tests_jdk21— the suite executed on JDK 21//code_analysis:tests_jdk25— the suite executed on JDK 25
Failures surface with the JDK-specific target name in the test log so it is immediately clear which runtime regressed.
bazel test //code_analysis:tests_jdk21 # or :tests_jdk25You can also change the ambient runtime used by every Java target in the repo — useful when experimenting outside the test suite:
bazel test //code_analysis:tests --config=jdk25- Ensure the JDK is available through
rules_java's prebuilt remote toolchains (currently 21 and 25 out of the box) or register a custom toolchain inMODULE.bazel. - Add the runtime version string (e.g.
"remotejdk_23") tojava_runtime_versionsin BUILD.bazel. - Optionally add a nickname mapping in
multi_jdk_test.bzlfor a friendlier suffix (jdk23instead of the auto-generatedjdk23).