A customizable wrapper around javac that aggregates and outputs compiler diagnostics in a desired format using a pluggable architecture.
The javac diagnostics wrapper is part of the larger https://eisopux.github.io/ project.
./gradlew assemblewill generate build/libs/javac-diagnostics-wrapper-all.jar.
Simply use instead of your usual javac command.
Instead of
javac [flags] File1.java File2.java(where [flags] is a placeholder for 0 or more actual javac flags you're using)
use
java \
-cp /path/to/javac-diagnostics-wrapper-all.jar \
io.github.eisopux.diagnostics.builtin.JsonDiagnostics \
[flags] File1.java File2.java(where [flags] is a placeholder for 0 or more actual javac flags you're using)
There are currently three builtin outputs using a Diagnostics Collector:
io.github.eisopux.diagnostics.builtin.LspDiagnosticsproduces output in the LSP JSON format.io.github.eisopux.diagnostics.builtin.JsonDiagnosticsproduces output in a JSON format directly corresponding to the javac diagnostics.io.github.eisopux.diagnostics.builtin.SarifDiagnosticsproduces output in the SARIF 2.1.0 format. Experimental: seeSarifReporter's class Javadoc for why (#152).
Normal compilation of a file with errors, using the javac format:
java \
-cp /path/to/javac-diagnostics-wrapper-all.jar \
io.github.eisopux.diagnostics.builtin.JsonDiagnostics \
File1.javaresults in
{
"diagnostics": [
{
"source": "file:///.../File1.java",
"kind": "ERROR",
"position": 29,
"startPosition": 29,
"endPosition": 30,
"lineNumber": 2,
"columnNumber": 16,
"code": "compiler.err.prob.found.req",
"message": "incompatible types: int cannot be converted to java.lang.String"
},
{
"source": "file:///.../File1.java",
"kind": "ERROR",
"position": 64,
"startPosition": 64,
"endPosition": 69,
"lineNumber": 4,
"columnNumber": 16,
"code": "compiler.err.prob.found.req",
"message": "incompatible types: unexpected return value"
}
]
}
Compilation of a file using the Checker Framework, using the LSP format:
java \
-cp /path/to/javac-diagnostics-wrapper-all.jar \
io.github.eisopux.diagnostics.builtin.LspDiagnostics \
-classpath /path/to/checker-framework/checker/dist/checker.jar \
-processor org.checkerframework.checker.nullness.NullnessChecker \
-AshowPrefixInWarningMessages
File2.javaresults in:
[
{
"uri": "file://.../File2.java",
"diagnostics": [
{
"range": {
"start": {
"line": 12,
"character": 14
},
"end": {
"line": 12,
"character": 16
}
},
"severity": 1,
"code": "compiler.err.proc.messager",
"source": "javac",
"message": "[argument.type.incompatible] incompatible types in argument.\n found : @Initialized @Nullable InputStream\n required: @Initialized @NonNull InputStream"
}
]
}
]
Note that the -AshowPrefixInWarningMessages is an optional Checker Framework flag
and will attach correct processor information to formats that support this information.
Compilation of a file with an error, using the SARIF format:
java \
-cp /path/to/javac-diagnostics-wrapper-all.jar \
io.github.eisopux.diagnostics.builtin.SarifDiagnostics \
File1.javaresults in
{
"version": "2.1.0",
"runs": [
{
"tool": {
"driver": {
"name": "javac",
"rules": [
{
"id": "compiler.err.prob.found.req"
}
]
}
},
"results": [
{
"ruleId": "compiler.err.prob.found.req",
"level": "error",
"message": {
"text": "incompatible types: java.lang.String cannot be converted to int"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "file:///.../File1.java"
},
"region": {
"startLine": 3,
"startColumn": 17,
"charOffset": 50,
"charLength": 12
}
}
}
]
}
]
}
]
}
(trimmed here to the fields this project sets; the actual output additionally carries a handful
of schema-default fields such as driver.language/run.newlineSequences that the SARIF library
fills in on its own.)
To format the source code, run ./gradlew spotlessApply.
The javac diagnostics wrapper features a modular, pluggable design that decouples data collection from output formatting. It utilizes a collector-reporter interface that allows developers to easily implement custom diagnostic gathering and presentation formats.
Components that hook into the javac compilation process to gather desired information.
Each collector implements the Collector interface and contributes its collected data as
a list of key/value pairs to a centralized CompilationReportData instance.
Components that process the aggregated CompilationReportData and generate output in various formats
(e.g., console text, JSON, or LSP diagnostics). Each reporter implements the Reporter interface and is responsible for
formatting the data according to its output standard.
Create a new class that implements io.github.eisopux.diagnostics.core.Collector. Override:
onBeforeCompile(CompilationTaskBuilder builder)if you need to attach listeners or initialize data structures.onAfterCompile(CompilationReportData reportData)to finalize your data and populate a report section as a list of key/value pairs.
Create a new class that implements io.github.eisopux.diagnostics.core.Reporter and its
generateReport(CompilationReportData reportData) method. Format the data according
to your output requirements.
The javac-diagnositc-wrapper produces output by combining one or more Collectors with exactly one
Reporter. To create an easy-to-use output configuration, add a new
class to theio.github.eisopux.diagnostics.builtin package. This class will encapsulate
the desired collectors and reporter, providing a convenient entry
point for generating an output. An example implementation is shown below:
public class JsonDiagnostics {
public static void main(String[] args) {
CompilerRunner runner =
new CompilerRunner()
.addCollector(new DiagnosticCollector())
.setReporter(new JsonReporter());
System.exit(runner.run(args) ? 0 : 1);
}
}Where .addCollector should be called one or more times to combine
multiple collectors and .setReporter should be called exactly once to select
the desired output format.
demo/checker-framework/: using this wrapper together with the EISOP Checker Framework to get a SARIF baseline of a checker's findings on an existing codebase.
This project publishes to Maven Central under the io.github.eisopux groupId, using plain
Gradle maven-publish/signing (no third-party publishing plugin), the same approach
eisop/checker-framework uses
-- see that doc for the general explanation of why a Central Publishing Portal plugin isn't used,
and for background on the publish/staging/signing flow.
One-time maintainer setup, before the first release:
- Publish rights on the
io.github.eisopuxnamespace in the Central Portal. This is a separate namespace from checker-framework's ownio.github.eisop, verified against theeisopuxGitHub org, and needs its own verification even if you already holdio.github.eisop. - A GPG key on a public keyserver, and
signing.gnupg.keyNameset to it (in~/.gradle/gradle.propertiesor via-Psigning.gnupg.keyName=...). - A Central Portal user token, set as
SONATYPE_NEXUS_USERNAME/SONATYPE_NEXUS_PASSWORDGradle properties.
To publish a release:
./gradlew publish -Prelease --no-parallelthen log in to the Central Portal and manually publish the staged deployment (this project has not automated that last click; see the checker-framework doc above for how it could be).
Note that com.jetbrains.qodana:qodana-sarif (see SarifReporter's class Javadoc) is not on
Maven Central, so a consumer of this project's published artifact who uses SarifDiagnostics
also needs the same custom repository declaration this project's own build.gradle uses.