diff --git a/framework/build.gradle b/framework/build.gradle index 662b905a5a86..22313d269f17 100644 --- a/framework/build.gradle +++ b/framework/build.gradle @@ -53,6 +53,8 @@ dependencies { implementation(libs.plume.util) implementation(libs.reflection.util) implementation(libs.classgraph) + implementation "com.fasterxml.jackson.core:jackson-databind:2.15.0" + implementation "com.contrastsecurity:java-sarif:2.0" testImplementation(libs.junit) testImplementation(project(":framework-test")) diff --git a/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java b/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java new file mode 100644 index 000000000000..1f6027580133 --- /dev/null +++ b/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java @@ -0,0 +1,185 @@ +package org.checkerframework.framework.report; + +import com.contrastsecurity.sarif.ArtifactLocation; +import com.contrastsecurity.sarif.Location; +import com.contrastsecurity.sarif.Message; +import com.contrastsecurity.sarif.PhysicalLocation; +import com.contrastsecurity.sarif.Region; +import com.contrastsecurity.sarif.Result; +import com.contrastsecurity.sarif.Result.Level; +import com.contrastsecurity.sarif.Run; +import com.contrastsecurity.sarif.SarifSchema210; +import com.contrastsecurity.sarif.Tool; +import com.contrastsecurity.sarif.ToolComponent; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.LineMap; +import com.sun.source.tree.Tree; +import com.sun.source.util.SourcePositions; +import com.sun.source.util.Trees; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Properties; +import javax.annotation.processing.ProcessingEnvironment; +import javax.tools.Diagnostic; + +/** + * Generates SARIF (Static Analysis Results Interchange Format) report files from checker + * diagnostics. + * + *
This class collects diagnostic messages during type checking and generates a SARIF 2.1.0
+ * compliant JSON report file. The report includes error and warning messages with their locations
+ * in the source code.
+ */
+public class SarifReportGenerator {
+
+ private final ProcessingEnvironment processingEnv;
+ private final List Only ERROR and MANDATORY_WARNING diagnostics are collected. Other diagnostic kinds (NOTE,
+ * etc.) are ignored.
+ *
+ * @param kind the diagnostic kind
+ * @param message the message text
+ * @param messageKey the message key (rule ID)
+ * @param source the source tree node where the diagnostic was reported
+ * @param root the compilation unit containing the source
+ */
+ public void addResult(
+ javax.tools.Diagnostic.Kind kind,
+ String message,
+ String messageKey,
+ Tree source,
+ CompilationUnitTree root) {
+ // Only collect ERROR and WARNING diagnostics
+ if (kind != javax.tools.Diagnostic.Kind.ERROR
+ && kind != javax.tools.Diagnostic.Kind.MANDATORY_WARNING) {
+ return;
+ }
+ String fileUri = getFileUri(root);
+
+ Region region = getRegion(source, root);
+
+ Result result =
+ new Result()
+ .withRuleId(messageKey)
+ .withLevel(kind == javax.tools.Diagnostic.Kind.ERROR ? Level.ERROR : Level.WARNING)
+ .withMessage(new Message().withText(message))
+ .withLocations(
+ Collections.singletonList(
+ new Location()
+ .withPhysicalLocation(
+ new PhysicalLocation()
+ .withArtifactLocation(new ArtifactLocation().withUri(fileUri))
+ .withRegion(region))));
+ results.add(result);
+ }
+
+ /**
+ * Returns the Checker Framework version from git.properties resource file.
+ *
+ * If the version cannot be read from git.properties, returns a default fallback version.
+ *
+ * @return the Checker Framework version string
+ */
+ private String getCheckerVersion() {
+ try (InputStream in = getClass().getResourceAsStream("/git.properties")) {
+ if (in != null) {
+ Properties gitProperties = new Properties();
+ gitProperties.load(in);
+ String version = gitProperties.getProperty("git.build.version");
+ if (version != null && !version.isEmpty()) {
+ return version;
+ }
+ }
+ } catch (IOException e) {
+ // Fall through to return default version
+ }
+ return "Unknown";
+ }
+
+ /**
+ * Write the SARIF report to file.
+ *
+ * @param outputPath the output file path
+ */
+ public void writeReport(String outputPath) throws IOException {
+
+ SarifSchema210 sarifLog =
+ new SarifSchema210()
+ .withVersion(SarifSchema210.Version._2_1_0)
+ .withRuns(
+ Collections.singletonList(
+ new Run()
+ .withTool(
+ new Tool()
+ .withDriver(
+ new ToolComponent()
+ .withName("Checker Framework")
+ .withVersion(getCheckerVersion())))
+ .withResults(results)));
+
+ // Write SARIF log to JSON file
+ ObjectMapper mapper = new ObjectMapper();
+ Path path = Paths.get(outputPath);
+ mapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), sarifLog);
+ }
+}
diff --git a/framework/src/main/java/org/checkerframework/framework/source/AggregateChecker.java b/framework/src/main/java/org/checkerframework/framework/source/AggregateChecker.java
index aba28fb67137..af9548d45c74 100644
--- a/framework/src/main/java/org/checkerframework/framework/source/AggregateChecker.java
+++ b/framework/src/main/java/org/checkerframework/framework/source/AggregateChecker.java
@@ -18,7 +18,7 @@
* Though each checker is run on a whole compilation unit before the next checker is run, error
* and warning messages are collected and sorted based on the location in the source file before
* being printed. (See {@link #printOrStoreMessage(Diagnostic.Kind, String, Tree,
- * CompilationUnitTree)}.)
+ * CompilationUnitTree, String)}.)
*
* This class delegates {@code AbstractTypeProcessor} responsibilities to each component checker.
*
diff --git a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java
index 9e7e55523307..a8dbaf3ad6ba 100644
--- a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java
+++ b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java
@@ -76,6 +76,7 @@
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.common.reflection.MethodValChecker;
import org.checkerframework.framework.qual.AnnotatedFor;
+import org.checkerframework.framework.report.SarifReportGenerator;
import org.checkerframework.framework.type.AnnotatedTypeFactory;
import org.checkerframework.framework.util.CheckerMain;
import org.checkerframework.framework.util.OptionConfiguration;
@@ -442,7 +443,8 @@
// Converts type argument inference crashes into errors. By default, this option is true.
// Use "-AconvertTypeArgInferenceCrashToWarning=false" to turn this option off and allow type
// argument inference crashes to crash the type checker.
- "convertTypeArgInferenceCrashToWarning"
+ "convertTypeArgInferenceCrashToWarning",
+ "sarifOutput"
})
public abstract class SourceChecker extends AbstractTypeProcessor implements OptionConfiguration {
@@ -627,6 +629,15 @@ public abstract class SourceChecker extends AbstractTypeProcessor implements Opt
*/
protected @Nullable SourceChecker parentChecker;
+ /** True if the -AsarifOutput command-line argument was passed. */
+ private boolean sarifOutputEnabled = false;
+
+ /** Path to SARIF output file. */
+ private @Nullable String sarifOutputPath = null;
+
+ /** SARIF report generator, if enabled. */
+ private @Nullable SarifReportGenerator sarifReportGenerator = null;
+
/** List of upstream checker names. Includes the current checker. */
protected @MonotonicNonNull List<@FullyQualifiedName String> upstreamCheckerNames;
@@ -749,6 +760,19 @@ protected void setParentChecker(SourceChecker parentChecker) {
return this.parentChecker;
}
+ /**
+ * Returns the root (ancestor) checker that has no parent.
+ *
+ * @return the root checker (this checker if it has no parent, otherwise the ultimate ancestor)
+ */
+ protected SourceChecker getRootChecker() {
+ SourceChecker root = this;
+ while (root.parentChecker != null) {
+ root = root.parentChecker;
+ }
+ return root;
+ }
+
/**
* Invoked when the current compilation unit root changes.
*
@@ -1080,6 +1104,16 @@ public void typeProcessingOver() {
checker.typeProcessingOver();
}
+ if (parentChecker == null && sarifReportGenerator != null) {
+ assert sarifOutputPath != null;
+ try {
+ sarifReportGenerator.writeReport(sarifOutputPath);
+ message(Diagnostic.Kind.NOTE, "SARIF report written to: " + sarifOutputPath);
+ } catch (IOException e) {
+ message(Diagnostic.Kind.WARNING, "Failed to write SARIF report: " + e.getMessage());
+ }
+ }
+
super.typeProcessingOver();
}
@@ -1129,6 +1163,15 @@ public void initChecker() {
requirePrefixInWarningSuppressions = hasOption("requirePrefixInWarningSuppressions");
showPrefixInWarningMessages = hasOption("showPrefixInWarningMessages");
warnUnneededSuppressions = hasOption("warnUnneededSuppressions");
+ sarifOutputEnabled = hasOption("sarifOutput");
+ // Only create sarifReportGenerator in root checker (no parentChecker)
+ if (sarifOutputEnabled && parentChecker == null) {
+ sarifOutputPath = getOption("sarifOutput");
+ if (sarifOutputPath == null) {
+ throw new UserError("Must supply an argument to -AsarifOutput");
+ }
+ sarifReportGenerator = new SarifReportGenerator(processingEnv);
+ }
}
/** Output the warning about source level at most once. */
@@ -1506,7 +1549,7 @@ private void report(
if (source instanceof Element elem) {
messager.printMessage(kind, messageText, elem);
} else if (source instanceof Tree sourceTree) {
- printOrStoreMessage(kind, messageText, sourceTree, currentRoot);
+ printOrStoreMessage(kind, messageText, sourceTree, currentRoot, messageKey);
} else {
throw new BugInCF("invalid position source of class " + source.getClass() + ": " + source);
}
@@ -1589,15 +1632,25 @@ public Collection Though each checker is run on a whole compilation unit before the next checker is run, error
* and warning messages are collected and sorted based on the location in the source file before
* being printed. (See {@link #printOrStoreMessage(Diagnostic.Kind, String, Tree,
- * CompilationUnitTree)}.)
+ * CompilationUnitTree, String)}.)
*
* WARNING: Circular dependencies are not supported. (In other words, if checker A depends on
* checker B, checker B cannot depend on checker A.) The Checker Framework does not check for
@@ -2299,7 +2353,7 @@ protected void printStoredMessages(CompilationUnitTree unit) {
return;
}
for (CheckerMessage msg : messageStore) {
- printOrStoreMessage(msg.kind, msg.message, msg.source, unit, msg.trace);
+ printOrStoreMessage(msg.kind, msg.message, msg.source, unit, msg.trace, msg.messageKey);
}
}
@@ -3529,6 +3583,9 @@ protected static class CheckerMessage implements Comparable