Skip to content
Open
2 changes: 2 additions & 0 deletions framework/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ dependencies {
implementation("org.plumelib:plume-util:${plumeUtilVersion}")
implementation("org.plumelib:reflection-util:${reflectionUtilVersion}")
implementation("io.github.classgraph:classgraph:4.8.184")
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.0'
implementation 'com.contrastsecurity:java-sarif:2.0'

testImplementation("junit:junit:${junitVersion}")
testImplementation(project(":framework-test"))
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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<Result> results = new ArrayList<>();

public SarifReportGenerator(ProcessingEnvironment processingEnv) {
this.processingEnv = processingEnv;
}

/**
* Converts the source file path to a file URI.
*
* @param root the compilation unit
* @return the file URI as a string, or "file:///unknown" if conversion fails
*/
private String getFileUri(CompilationUnitTree root) {
try {
return root.getSourceFile().toUri().toString();
} catch (IllegalArgumentException | SecurityException e) {
return "file:///unknown";
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
Gaoyan1999 marked this conversation as resolved.

/**
* Extracts the source code region (line and column numbers) for a tree node.
*
* @param source the tree node
* @param root the compilation unit containing the tree
* @return a Region with start and end line/column numbers, or default values if position cannot
* be determined
*/
private Region getRegion(Tree source, CompilationUnitTree root) {
Trees trees = Trees.instance(processingEnv);
SourcePositions sourcePositions = trees.getSourcePositions();

long startPos = sourcePositions.getStartPosition(root, source);
long endPos = sourcePositions.getEndPosition(root, source);

if (startPos == Diagnostic.NOPOS || endPos == Diagnostic.NOPOS) {
return new Region().withStartLine(1).withStartColumn(1).withEndLine(1).withEndColumn(1);
}
Comment thread
Gaoyan1999 marked this conversation as resolved.

LineMap lineMap = root.getLineMap();
long startLine = lineMap.getLineNumber(startPos);
long startCol = lineMap.getColumnNumber(startPos);
long endLine = lineMap.getLineNumber(endPos);
long endCol = lineMap.getColumnNumber(endPos);

return new Region()
.withStartLine((int) startLine)
.withStartColumn((int) startCol)
.withEndLine((int) endLine)
.withEndColumn((int) endCol);
}
Comment thread
Gaoyan1999 marked this conversation as resolved.

/**
* Add a diagnostic result to the report.
*
* <p>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);
}
Comment thread
Gaoyan1999 marked this conversation as resolved.

/**
* Returns the Checker Framework version from git.properties resource file.
*
* <p>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);
}
Comment thread
Gaoyan1999 marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -433,7 +434,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 {

Expand Down Expand Up @@ -618,6 +620,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;

Expand Down Expand Up @@ -736,6 +747,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.
*
Expand Down Expand Up @@ -1057,6 +1081,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());
}
}
Comment thread
Gaoyan1999 marked this conversation as resolved.

super.typeProcessingOver();
}

Expand Down Expand Up @@ -1106,6 +1140,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. */
Expand Down Expand Up @@ -1474,7 +1517,7 @@ private void report(
if (source instanceof Element) {
messager.printMessage(kind, messageText, (Element) source);
} else if (source instanceof Tree) {
printOrStoreMessage(kind, messageText, (Tree) source, currentRoot);
printOrStoreMessage(kind, messageText, (Tree) source, currentRoot, messageKey);
} else {
throw new BugInCF("invalid position source of class " + source.getClass() + ": " + source);
}
Expand Down Expand Up @@ -1557,15 +1600,25 @@ public Collection<String> getSuppressWarningsPrefixesOfSubcheckers() {
* @param root the compilation unit
*/
protected void printOrStoreMessage(
javax.tools.Diagnostic.Kind kind, String message, Tree source, CompilationUnitTree root) {
javax.tools.Diagnostic.Kind kind,
String message,
Tree source,
CompilationUnitTree root,
String messageKey) {
assert this.currentRoot == root;
StackTraceElement[] trace = Thread.currentThread().getStackTrace();
if (messageStore == null) {
printOrStoreMessage(kind, message, source, root, trace);
printOrStoreMessage(kind, message, source, root, trace, messageKey);
} else {
CheckerMessage checkerMessage = new CheckerMessage(kind, message, source, this, trace);
CheckerMessage checkerMessage =
new CheckerMessage(kind, message, source, this, trace, messageKey);
messageStore.add(checkerMessage);
}
// Use root checker's sarifReportGenerator so all subcheckers share the same instance
SourceChecker rootChecker = getRootChecker();
if (rootChecker.sarifReportGenerator != null) {
rootChecker.sarifReportGenerator.addResult(kind, message, messageKey, source, root);
}
}

/**
Expand All @@ -1586,7 +1639,8 @@ protected void printOrStoreMessage(
String message,
Tree source,
CompilationUnitTree root,
StackTraceElement[] trace) {
StackTraceElement[] trace,
String messageKey) {
Trees.instance(processingEnv).printMessage(kind, message, source, root);
printStackTrace(trace);
}
Expand Down Expand Up @@ -2265,7 +2319,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);
}
}

Expand Down Expand Up @@ -3495,6 +3549,9 @@ protected static class CheckerMessage implements Comparable<CheckerMessage> {
/** The stack trace when the message was created. */
final StackTraceElement[] trace;

/** The message key (rule ID) for this message. */
final String messageKey;

/**
* Create a new CheckerMessage.
*
Expand All @@ -3503,18 +3560,21 @@ protected static class CheckerMessage implements Comparable<CheckerMessage> {
* @param source tree node causing the error
* @param checker the type-checker in use
* @param trace the stack trace when the message is created
* @param messageKey the message key (rule ID)
*/
protected CheckerMessage(
Diagnostic.Kind kind,
String message,
@FindDistinct Tree source,
@FindDistinct SourceChecker checker,
StackTraceElement[] trace) {
StackTraceElement[] trace,
String messageKey) {
this.kind = kind;
this.message = message;
this.source = source;
this.checker = checker;
this.trace = trace;
this.messageKey = messageKey;
}

@Override
Expand Down