From d8cce5e40544505ff74e5fd9e24e063e42fbc980 Mon Sep 17 00:00:00 2001
From: Daniel Gao <1803483451@qq.com>
Date: Wed, 3 Dec 2025 02:41:53 +1100
Subject: [PATCH 1/7] wip: the simplest workflow
---
PHASE1_IMPLEMENTATION_STEPS.md | 853 ++++++++++++++++++
SARIF_REPORT_DESIGN.md | 572 ++++++++++++
framework/build.gradle | 2 +
.../report/SarifReportGenerator.java | 89 ++
.../framework/source/SourceChecker.java | 72 +-
test.sarif | 25 +
6 files changed, 1605 insertions(+), 8 deletions(-)
create mode 100644 PHASE1_IMPLEMENTATION_STEPS.md
create mode 100644 SARIF_REPORT_DESIGN.md
create mode 100644 framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java
create mode 100644 test.sarif
diff --git a/PHASE1_IMPLEMENTATION_STEPS.md b/PHASE1_IMPLEMENTATION_STEPS.md
new file mode 100644
index 000000000000..0889c030bbb5
--- /dev/null
+++ b/PHASE1_IMPLEMENTATION_STEPS.md
@@ -0,0 +1,853 @@
+# Phase 1 (POC) 实现步骤详解
+
+## 步骤 1: 添加命令行选项(仅参数,无实现)
+
+### 目标
+添加 `-AsarifOutput` 命令行选项,当启用时输出日志信息。
+
+### 具体操作
+
+1. **在 `SourceChecker.java` 的 `@SupportedOptions` 注解中添加选项**
+ - 文件:`framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java`
+ - 位置:找到 `@SupportedOptions({...})` 注解(大约第 111 行)
+ - 添加:
+ ```java
+ // Generate SARIF report file
+ // -AsarifOutput=path/to/report.sarif
+ "sarifOutput",
+ ```
+
+2. **在 `SourceChecker` 类中添加字段**
+ - 位置:在类的字段声明区域(大约第 640 行附近)
+ - 添加:
+ ```java
+ /** True if the -AsarifOutput command-line argument was passed. */
+ private boolean sarifOutputEnabled = false;
+
+ /** Path to SARIF output file. */
+ private @Nullable String sarifOutputPath = null;
+ ```
+
+3. **在 `initChecker()` 方法中读取选项**
+ - 位置:`initChecker()` 方法中(大约第 1103 行附近,在设置其他选项的地方)
+ - 添加:
+ ```java
+ sarifOutputEnabled = hasOption("sarifOutput");
+ if (sarifOutputEnabled) {
+ sarifOutputPath = getOption("sarifOutput");
+ if (sarifOutputPath == null) {
+ throw new UserError("Must supply an argument to -AsarifOutput");
+ }
+ // TODO: 临时日志输出,验证选项是否生效
+ message(Diagnostic.Kind.NOTE,
+ "SARIF output enabled: " + sarifOutputPath);
+ }
+ ```
+
+### 验证方法
+
+运行测试命令:
+```bash
+javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
+```
+
+预期输出:应该看到 NOTE 消息:"SARIF output enabled: test.sarif"
+
+### 调试提示
+
+- 如果看不到 NOTE 消息,检查 `hasOption("sarifOutput")` 是否正确
+- 如果抛出 UserError,检查选项值是否正确传递
+
+---
+
+## 步骤 2: 添加依赖并创建空的 SarifReportGenerator 类
+
+### 目标
+添加 java-sarif 依赖,创建 SarifReportGenerator 类的骨架(不实现功能)。
+
+### 具体操作
+
+1. **添加 Maven/Gradle 依赖**
+ - 文件:`framework/build.gradle`
+ - 在 `dependencies` 块中添加:
+ ```gradle
+ dependencies {
+ // ... 现有依赖 ...
+ implementation 'com.contrastsecurity:java-sarif:2.0'
+ implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.0'
+ }
+ ```
+
+2. **创建 SarifReportGenerator 类文件**
+ - 文件:`framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java`
+ - 创建基本类结构:
+ ```java
+ package org.checkerframework.framework.report;
+
+ import javax.annotation.processing.ProcessingEnvironment;
+
+ /**
+ * Generates SARIF report files from checker diagnostics.
+ *
+ *
This is a POC implementation for Phase 1.
+ */
+ public class SarifReportGenerator {
+
+ private final ProcessingEnvironment processingEnv;
+
+ public SarifReportGenerator(ProcessingEnvironment processingEnv) {
+ this.processingEnv = processingEnv;
+ }
+
+ /**
+ * Add a diagnostic result to the report.
+ *
+ * @param kind the diagnostic kind
+ * @param message the message text
+ * @param messageKey the message key (rule ID)
+ */
+ public void addResult(
+ javax.tools.Diagnostic.Kind kind,
+ String message,
+ String messageKey) {
+ // TODO: Phase 1 - 暂时不实现,只记录日志
+ System.out.println("[SARIF] Would add result: " + messageKey + " - " + message);
+ }
+
+ /**
+ * Write the SARIF report to file.
+ *
+ * @param outputPath the output file path
+ */
+ public void writeReport(String outputPath) {
+ // TODO: Phase 1 - 暂时不实现,只记录日志
+ System.out.println("[SARIF] Would write report to: " + outputPath);
+ }
+ }
+ ```
+
+3. **在 `SourceChecker` 中添加字段和初始化**
+ - 文件:`framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java`
+ - 在字段声明区域添加:
+ ```java
+ /** SARIF report generator, if enabled. */
+ private @Nullable SarifReportGenerator sarifReportGenerator = null;
+ ```
+ - 在 `initChecker()` 中(步骤 1 的代码之后)添加:
+ ```java
+ if (sarifOutputEnabled) {
+ sarifOutputPath = getOption("sarifOutput");
+ if (sarifOutputPath == null) {
+ throw new UserError("Must supply an argument to -AsarifOutput");
+ }
+ sarifReportGenerator = new SarifReportGenerator(processingEnv);
+ message(Diagnostic.Kind.NOTE,
+ "SARIF report generator initialized: " + sarifOutputPath);
+ }
+ ```
+
+### 验证方法
+
+运行测试命令:
+```bash
+javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
+```
+
+预期输出:
+- NOTE 消息:"SARIF report generator initialized: test.sarif"
+- 编译应该成功(没有错误)
+
+### 调试提示
+
+- 如果编译失败,检查依赖是否正确添加
+- 如果找不到类,检查包名和导入语句
+
+---
+
+## 步骤 3: 使用 Mock 数据生成 SARIF 文件
+
+### 目标
+使用硬编码的 mock 数据生成一个有效的 SARIF JSON 文件,验证整个流程。
+
+### 具体操作
+
+1. **更新 `SarifReportGenerator` 类,添加 mock 数据生成**
+ - 文件:`framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java`
+ - 添加必要的导入:
+ ```java
+ import com.contrastsecurity.sarif.*;
+ import com.fasterxml.jackson.databind.ObjectMapper;
+ import java.io.IOException;
+ import java.nio.file.Files;
+ import java.nio.file.Path;
+ import java.nio.file.Paths;
+ import java.util.Arrays;
+ import java.util.Collections;
+ ```
+ - 更新 `writeReport()` 方法:
+ ```java
+ public void writeReport(String outputPath) throws IOException {
+ // Phase 1: 使用 mock 数据生成 SARIF 文件
+ SarifLog sarifLog = new SarifLog()
+ .withVersion("2.1.0")
+ .withRuns(Collections.singletonList(
+ new Run()
+ .withTool(new Tool()
+ .withDriver(new ToolComponent()
+ .withName("Checker Framework")
+ .withVersion("3.51.2-SNAPSHOT")))
+ .withResults(Collections.singletonList(
+ new Result()
+ .withRuleId("mock.rule.id")
+ .withLevel("error")
+ .withMessage(new Message()
+ .withText("This is a mock SARIF result for testing"))
+ .withLocations(Collections.singletonList(
+ new Location()
+ .withPhysicalLocation(new PhysicalLocation()
+ .withArtifactLocation(new ArtifactLocation()
+ .withUri("file:///mock/Test.java"))
+ .withRegion(new Region()
+ .withStartLine(10)
+ .withStartColumn(5))))))
+ ));
+
+ // 写入 JSON 文件
+ ObjectMapper mapper = new ObjectMapper();
+ Path path = Paths.get(outputPath);
+ mapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), sarifLog);
+
+ System.out.println("[SARIF] Mock report written to: " + outputPath);
+ }
+ ```
+
+2. **在 `SourceChecker.typeProcessingOver()` 中调用写入**
+ - 文件:`framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java`
+ - 位置:`typeProcessingOver()` 方法(大约第 1055 行)
+ - 修改:
+ ```java
+ @Override
+ public void typeProcessingOver() {
+ for (SourceChecker checker : getSubcheckers()) {
+ checker.typeProcessingOver();
+ }
+
+ // Phase 1: 生成 SARIF 报告(仅在根 checker)
+ if (parentChecker == null && sarifReportGenerator != 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();
+ }
+ ```
+
+### 验证方法
+
+1. **运行测试命令:**
+ ```bash
+ javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
+ ```
+
+2. **检查输出文件:**
+ ```bash
+ cat test.sarif
+ ```
+
+预期结果:
+- 生成 `test.sarif` 文件
+- 文件包含有效的 JSON
+- JSON 符合 SARIF 2.1.0 格式
+- 包含一个 mock 结果
+
+3. **验证 SARIF 格式(可选):**
+ - 使用在线 SARIF 验证器:https://sarifweb.azurewebsites.net/Validator
+ - 或使用 VS Code SARIF Viewer 插件查看
+
+### 调试提示
+
+- 如果文件没有生成,检查文件路径和权限
+- 如果 JSON 格式错误,检查 ObjectMapper 配置
+- 如果抛出异常,检查依赖是否正确加载
+
+---
+
+## 步骤 4: 在 printOrStoreMessage 中收集消息(不写入文件)
+
+### 目标
+在消息打印/存储时,同时收集到 SarifReportGenerator,但不立即写入文件。
+
+### 具体操作
+
+1. **修改 `CheckerMessage` 类,添加 `messageKey` 字段**
+ - 文件:`framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java`
+ - 位置:`CheckerMessage` 内部类(大约第 3479 行)
+ - 添加字段:
+ ```java
+ /** The message key (rule ID) for this message. */
+ final String messageKey;
+ ```
+ - 更新构造函数:
+ ```java
+ protected CheckerMessage(
+ Diagnostic.Kind kind,
+ String message,
+ @FindDistinct Tree source,
+ @FindDistinct SourceChecker checker,
+ StackTraceElement[] trace,
+ String messageKey) { // 新增参数
+ this.kind = kind;
+ this.message = message;
+ this.source = source;
+ this.checker = checker;
+ this.trace = trace;
+ this.messageKey = messageKey; // 新增赋值
+ }
+ ```
+
+2. **修改 `printOrStoreMessage()` 方法,传递 messageKey**
+ - 文件:`framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java`
+ - 位置:`printOrStoreMessage()` 方法(大约第 1559 行)
+ - 问题:当前方法没有 messageKey 参数
+ - 解决方案:暂时使用 "unknown" 作为占位符,后续步骤会修复
+ - 修改:
+ ```java
+ protected void printOrStoreMessage(
+ javax.tools.Diagnostic.Kind kind,
+ String message,
+ Tree source,
+ CompilationUnitTree root) {
+ assert this.currentRoot == root;
+ StackTraceElement[] trace = Thread.currentThread().getStackTrace();
+ if (messageStore == null) {
+ printOrStoreMessage(kind, message, source, root, trace);
+ } else {
+ // Phase 1: 暂时使用 "unknown" 作为 messageKey
+ String messageKey = "unknown";
+ CheckerMessage checkerMessage = new CheckerMessage(
+ kind, message, source, this, trace, messageKey);
+ messageStore.add(checkerMessage);
+ }
+
+ // Phase 1: 收集消息到 SARIF(如果启用)
+ if (sarifReportGenerator != null && parentChecker == null) {
+ // 暂时使用 "unknown" 作为 messageKey
+ sarifReportGenerator.addResult(kind, message, "unknown");
+ }
+ }
+ ```
+
+3. **更新 `SarifReportGenerator.addResult()` 方法签名**
+ - 文件:`framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java`
+ - 修改方法:
+ ```java
+ public void addResult(
+ javax.tools.Diagnostic.Kind kind,
+ String message,
+ String messageKey) {
+ // Phase 1: 只收集,不处理
+ // 暂时只记录日志,验证消息是否被收集
+ System.out.println("[SARIF] Collected: " + messageKey + " - " +
+ kind + " - " + message.substring(0, Math.min(50, message.length())));
+ }
+ ```
+
+### 验证方法
+
+运行测试命令:
+```bash
+javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
+```
+
+预期输出:
+- 应该看到多个 `[SARIF] Collected:` 日志消息
+- 每个诊断消息都应该被收集
+- 文件仍然包含 mock 数据(因为还没实现真实数据写入)
+
+### 调试提示
+
+- 如果没有看到收集日志,检查 `sarifReportGenerator != null` 条件
+- 如果只看到部分消息,检查 `parentChecker == null` 条件(可能子 checker 也在收集)
+
+---
+
+## 步骤 5: 实现真实数据收集和 SARIF 生成
+
+### 目标
+使用真实收集的消息数据生成 SARIF 文件,替换 mock 数据。
+
+### 具体操作
+
+1. **在 `SarifReportGenerator` 中添加数据存储**
+ - 文件:`framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java`
+ - 添加字段:
+ ```java
+ import java.util.ArrayList;
+ import java.util.HashMap;
+ import java.util.List;
+ import java.util.Map;
+
+ // 在类中添加字段
+ private final List results = new ArrayList<>();
+ private final Map artifacts = new HashMap<>();
+ ```
+ - 更新 `addResult()` 方法:
+ ```java
+ public void addResult(
+ javax.tools.Diagnostic.Kind kind,
+ String message,
+ String messageKey) {
+ // Phase 1: 只收集 ERROR 和 WARNING
+ if (kind != javax.tools.Diagnostic.Kind.ERROR
+ && kind != javax.tools.Diagnostic.Kind.MANDATORY_WARNING) {
+ return;
+ }
+
+ // 创建 Result 对象
+ String level = kind == javax.tools.Diagnostic.Kind.ERROR ? "error" : "warning";
+ Result result = new Result()
+ .withRuleId(messageKey)
+ .withLevel(level)
+ .withMessage(new Message().withText(message));
+
+ results.add(result);
+ }
+ ```
+
+2. **更新 `writeReport()` 方法,使用真实数据**
+ - 文件:`framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java`
+ - 修改方法:
+ ```java
+ public void writeReport(String outputPath) throws IOException {
+ // 创建 Tool 信息
+ ToolComponent driver = new ToolComponent()
+ .withName("Checker Framework")
+ .withVersion(getCheckerVersion()); // 需要实现这个方法
+
+ // 创建 Run
+ Run run = new Run()
+ .withTool(new Tool().withDriver(driver))
+ .withResults(results)
+ .withArtifacts(new ArrayList<>(artifacts.values()));
+
+ // 创建 SarifLog
+ SarifLog sarifLog = new SarifLog()
+ .withVersion("2.1.0")
+ .withRuns(Collections.singletonList(run));
+
+ // 写入文件
+ ObjectMapper mapper = new ObjectMapper();
+ Path path = Paths.get(outputPath);
+ mapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), sarifLog);
+ }
+
+ private String getCheckerVersion() {
+ // Phase 1: 简化版本,返回固定值
+ return "3.51.2-SNAPSHOT";
+ }
+ ```
+
+3. **清空结果列表(在写入后)**
+ - 在 `writeReport()` 方法末尾添加:
+ ```java
+ // 清空结果,为下次运行做准备
+ results.clear();
+ artifacts.clear();
+ ```
+
+### 验证方法
+
+1. **运行测试命令:**
+ ```bash
+ javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
+ ```
+
+2. **检查生成的 SARIF 文件:**
+ ```bash
+ cat test.sarif | jq '.runs[0].results | length'
+ ```
+
+预期结果:
+- SARIF 文件包含真实的结果(不再是 mock 数据)
+- 结果数量应该与收集的消息数量一致
+- 每个结果都有正确的 ruleId、level 和 message
+
+### 调试提示
+
+- 如果结果数量为 0,检查 `addResult()` 是否被正确调用
+- 如果 ruleId 都是 "unknown",需要继续下一步获取真实的 messageKey
+- 如果 JSON 格式错误,检查 ObjectMapper 序列化
+
+---
+
+## 步骤 6: 获取真实的 messageKey
+
+### 目标
+从 `report()` 方法传递真实的 messageKey 到 `printOrStoreMessage()`。
+
+### 具体操作
+
+1. **修改 `report()` 方法,传递 messageKey**
+ - 文件:`framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java`
+ - 位置:`report()` 方法(大约第 1426 行)
+ - 问题:需要将 messageKey 传递到 `printOrStoreMessage()`
+ - 解决方案:修改 `printOrStoreMessage()` 方法签名,添加 messageKey 参数
+ - 修改 `report()` 方法中调用 `printOrStoreMessage()` 的地方:
+ ```java
+ if (source instanceof Tree) {
+ printOrStoreMessage(kind, messageText, (Tree) source, currentRoot, messageKey);
+ }
+ ```
+
+2. **更新 `printOrStoreMessage()` 方法签名**
+ - 文件:`framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java`
+ - 位置:`printOrStoreMessage()` 方法(大约第 1559 行)
+ - 修改方法签名:
+ ```java
+ protected void printOrStoreMessage(
+ 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, messageKey);
+ } else {
+ CheckerMessage checkerMessage = new CheckerMessage(
+ kind, message, source, this, trace, messageKey);
+ messageStore.add(checkerMessage);
+ }
+
+ // 收集消息到 SARIF
+ if (sarifReportGenerator != null && parentChecker == null) {
+ sarifReportGenerator.addResult(kind, message, messageKey);
+ }
+ }
+ ```
+
+3. **更新另一个 `printOrStoreMessage()` 重载方法**
+ - 位置:`printOrStoreMessage()` 的另一个重载(大约第 1584 行)
+ - 修改:
+ ```java
+ protected void printOrStoreMessage(
+ javax.tools.Diagnostic.Kind kind,
+ String message,
+ Tree source,
+ CompilationUnitTree root,
+ StackTraceElement[] trace,
+ String messageKey) { // 新增参数
+ Trees.instance(processingEnv).printMessage(kind, message, source, root);
+ printStackTrace(trace);
+ }
+ ```
+
+4. **更新 `printStoredMessages()` 方法**
+ - 位置:`printStoredMessages()` 方法(大约第 2263 行)
+ - 修改:
+ ```java
+ protected void printStoredMessages(CompilationUnitTree unit) {
+ if (messageStore == null || parentChecker != null) {
+ return;
+ }
+ for (CheckerMessage msg : messageStore) {
+ printOrStoreMessage(msg.kind, msg.message, msg.source, unit, msg.trace, msg.messageKey);
+ }
+ }
+ ```
+
+### 验证方法
+
+运行测试命令:
+```bash
+javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
+```
+
+检查 SARIF 文件中的 ruleId:
+```bash
+cat test.sarif | jq '.runs[0].results[].ruleId'
+```
+
+预期结果:
+- ruleId 应该是真实的 messageKey(如 "assignment.type.incompatible")
+- 不再是 "unknown"
+
+### 调试提示
+
+- 如果 ruleId 仍然是 "unknown",检查方法调用链是否正确传递参数
+- 如果编译错误,检查所有调用 `printOrStoreMessage()` 的地方是否都更新了
+
+---
+
+## 步骤 7: 添加位置信息(文件 URI、行号、列号)
+
+### 目标
+在 SARIF 结果中添加源代码位置信息(文件 URI、行号、列号)。
+
+### 具体操作
+
+1. **修改 `SarifReportGenerator.addResult()` 方法,添加位置参数**
+ - 文件:`framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java`
+ - 添加必要的导入:
+ ```java
+ import com.sun.source.tree.Tree;
+ import com.sun.source.tree.CompilationUnitTree;
+ import com.sun.source.util.SourcePositions;
+ import com.sun.source.util.Trees;
+ import javax.annotation.processing.ProcessingEnvironment;
+ ```
+ - 修改方法签名:
+ ```java
+ public void addResult(
+ javax.tools.Diagnostic.Kind kind,
+ String message,
+ String messageKey,
+ Tree source,
+ CompilationUnitTree root) { // 新增参数
+ ```
+
+2. **实现位置信息提取**
+ - 在 `addResult()` 方法中添加:
+ ```java
+ // 获取文件 URI
+ String fileUri = getFileUri(root);
+
+ // 获取位置信息(行号、列号)
+ Region region = getRegion(source, root);
+
+ // 创建 Location
+ Location location = new Location()
+ .withPhysicalLocation(new PhysicalLocation()
+ .withArtifactLocation(new ArtifactLocation().withUri(fileUri))
+ .withRegion(region));
+
+ // 创建 Result
+ String level = kind == javax.tools.Diagnostic.Kind.ERROR ? "error" : "warning";
+ Result result = new Result()
+ .withRuleId(messageKey)
+ .withLevel(level)
+ .withMessage(new Message().withText(message))
+ .withLocations(Collections.singletonList(location));
+
+ results.add(result);
+ ```
+
+3. **实现辅助方法**
+ - 在 `SarifReportGenerator` 类中添加:
+ ```java
+ private String getFileUri(CompilationUnitTree root) {
+ // Phase 1: 简化版本,使用文件路径转换为 URI
+ try {
+ java.io.File file = new java.io.File(root.getSourceFile().getName());
+ return file.toURI().toString();
+ } catch (Exception e) {
+ return "file:///unknown";
+ }
+ }
+
+ 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 == -1 || endPos == -1) {
+ // 无法获取位置,返回默认值
+ return new Region().withStartLine(1).withStartColumn(1);
+ }
+
+ // 计算行号和列号(简化版本)
+ // Phase 1: 使用简单的行号计算
+ String sourceText = root.getSourceFile().getCharContent(true).toString();
+ int lineNumber = 1;
+ int columnNumber = 1;
+
+ for (int i = 0; i < startPos && i < sourceText.length(); i++) {
+ if (sourceText.charAt(i) == '\n') {
+ lineNumber++;
+ columnNumber = 1;
+ } else {
+ columnNumber++;
+ }
+ }
+
+ return new Region()
+ .withStartLine(lineNumber)
+ .withStartColumn(columnNumber)
+ .withEndLine(lineNumber) // Phase 1: 简化,结束位置同开始位置
+ .withEndColumn(columnNumber);
+ }
+ ```
+
+4. **更新 `SourceChecker` 中的调用**
+ - 文件:`framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java`
+ - 在 `printOrStoreMessage()` 方法中:
+ ```java
+ // 收集消息到 SARIF
+ if (sarifReportGenerator != null && parentChecker == null) {
+ sarifReportGenerator.addResult(kind, message, messageKey, source, root);
+ }
+ ```
+
+### 验证方法
+
+1. **运行测试命令:**
+ ```bash
+ javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
+ ```
+
+2. **检查 SARIF 文件中的位置信息:**
+ ```bash
+ cat test.sarif | jq '.runs[0].results[0].locations[0].physicalLocation'
+ ```
+
+预期结果:
+- 每个结果都有 `physicalLocation`
+- `artifactLocation.uri` 包含文件 URI
+- `region` 包含 `startLine` 和 `startColumn`
+
+### 调试提示
+
+- 如果 URI 是 "file:///unknown",检查文件路径获取逻辑
+- 如果行号/列号不正确,检查 `getRegion()` 方法的计算逻辑
+- 如果位置信息缺失,检查 `source` 和 `root` 参数是否正确传递
+
+---
+
+## 步骤 8: 添加文件内容到 artifacts(可选,但推荐)
+
+### 目标
+在 SARIF 的 `artifacts` 中包含源代码文件内容,方便 SARIF viewer 显示。
+
+### 具体操作
+
+1. **在 `addResult()` 中记录文件信息**
+ - 文件:`framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java`
+ - 在 `addResult()` 方法开始处添加:
+ ```java
+ // 记录文件信息到 artifacts
+ String fileUri = getFileUri(root);
+ if (!artifacts.containsKey(fileUri)) {
+ addArtifact(root, fileUri);
+ }
+ ```
+
+2. **实现 `addArtifact()` 方法**
+ - 在 `SarifReportGenerator` 类中添加:
+ ```java
+ private void addArtifact(CompilationUnitTree root, String fileUri) {
+ try {
+ // 读取文件内容
+ String content = root.getSourceFile().getCharContent(true).toString();
+
+ // 创建 ArtifactContent
+ ArtifactContent artifactContent = new ArtifactContent()
+ .withText(content);
+
+ // 创建 Artifact
+ Artifact artifact = new Artifact()
+ .withLocation(new ArtifactLocation().withUri(fileUri))
+ .withContents(artifactContent);
+
+ artifacts.put(fileUri, artifact);
+ } catch (Exception e) {
+ // Phase 1: 如果读取失败,创建不包含内容的 artifact
+ Artifact artifact = new Artifact()
+ .withLocation(new ArtifactLocation().withUri(fileUri));
+ artifacts.put(fileUri, artifact);
+ }
+ }
+ ```
+
+3. **确保 artifacts 在写入时包含**
+ - 在 `writeReport()` 方法中已经包含了 artifacts,无需修改
+
+### 验证方法
+
+1. **运行测试命令:**
+ ```bash
+ javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
+ ```
+
+2. **检查 SARIF 文件中的 artifacts:**
+ ```bash
+ cat test.sarif | jq '.runs[0].artifacts[0].contents.text' | head -20
+ ```
+
+预期结果:
+- `artifacts` 数组包含分析的文件
+- 每个 artifact 的 `contents.text` 包含源代码内容
+
+### 调试提示
+
+- 如果文件内容为空,检查文件读取逻辑
+- 如果文件很大,考虑是否限制内容大小(Phase 1 可以暂时不限制)
+
+---
+
+## 步骤 9: 清理和优化
+
+### 目标
+移除调试日志,优化代码,确保 POC 版本稳定。
+
+### 具体操作
+
+1. **移除所有 `System.out.println` 调试日志**
+ - 在 `SarifReportGenerator` 中移除所有调试输出
+ - 在 `SourceChecker` 中移除临时的 NOTE 消息(可选,可以保留一个确认消息)
+
+2. **错误处理优化**
+ - 确保所有异常都被正确处理
+ - 在 `typeProcessingOver()` 中,如果写入失败,只记录警告,不影响编译
+
+3. **代码清理**
+ - 移除 TODO 注释中的 "Phase 1" 标记(如果不再需要)
+ - 添加必要的 JavaDoc 注释
+
+4. **最终验证**
+ - 运行完整的测试套件
+ - 验证生成的 SARIF 文件可以被标准工具读取
+
+### 验证方法
+
+1. **运行多个测试用例:**
+ ```bash
+ javac -processor NullnessChecker -AsarifOutput=test.sarif Test1.java Test2.java
+ ```
+
+2. **使用 SARIF 验证器验证文件格式**
+
+3. **使用 VS Code SARIF Viewer 查看报告**
+
+### 完成标准
+
+- ✅ 可以生成有效的 SARIF 2.1.0 格式文件
+- ✅ 包含真实的诊断结果(ruleId、level、message)
+- ✅ 包含位置信息(文件 URI、行号、列号)
+- ✅ 包含源代码内容(可选但推荐)
+- ✅ 不影响现有功能
+- ✅ 错误处理完善
+
+---
+
+## 总结
+
+Phase 1 (POC) 实现完成后的功能:
+- 通过 `-AsarifOutput` 选项启用 SARIF 报告生成
+- 收集所有 ERROR 和 WARNING 类型的诊断消息
+- 生成符合 SARIF 2.1.0 标准的 JSON 文件
+- 包含基本的位置信息和源代码内容
+
+下一步(Phase 2)可以完善:
+- 支持所有消息类型
+- 完善规则信息(从 messages.properties 提取)
+- 支持 compound checker
+- 性能优化
+
diff --git a/SARIF_REPORT_DESIGN.md b/SARIF_REPORT_DESIGN.md
new file mode 100644
index 000000000000..ce1d8fbf4692
--- /dev/null
+++ b/SARIF_REPORT_DESIGN.md
@@ -0,0 +1,572 @@
+# SARIF Report Generation Design Document
+
+## 背景和目标
+
+### 当前问题
+1. Checker Framework 目前只将警告和错误输出到控制台
+2. 在使用 Maven 或 Gradle 等构建系统时,输出顺序可能不一致(已知 bug)
+3. 缺乏可解析的报告文件,难以构建通用的抑制系统
+
+### 目标
+- 生成可解析的 SARIF 格式报告文件
+- 最小侵入地集成到现有代码
+- 保持向后兼容(不影响现有控制台输出)
+- 支持后续扩展(如通用抑制系统)
+
+## 当前代码架构分析
+
+### 消息输出流程
+
+```
+report() / reportError() / reportWarning()
+ ↓
+printOrStoreMessage()
+ ↓
+[如果有 messageStore] → CheckerMessage → messageStore (TreeSet)
+ ↓
+[处理完编译单元后] → printStoredMessages()
+ ↓
+Trees.printMessage() → 控制台输出
+```
+
+### 关键类和字段
+
+1. **SourceChecker**
+ - `messageStore: TreeSet` - 存储消息(仅 compound checker 使用)
+ - `printOrStoreMessage()` - 决定是存储还是立即打印
+ - `printStoredMessages()` - 打印存储的消息
+
+2. **CheckerMessage**
+ - `kind: Diagnostic.Kind` - 消息类型(ERROR, WARNING 等)
+ - `message: String` - 消息文本
+ - `source: Tree` - 源代码位置
+ - `checker: SourceChecker` - 发出消息的 checker
+ - `trace: StackTraceElement[]` - 堆栈跟踪
+
+3. **关键方法调用时机**
+ - `typeProcessingStart()` - 初始化
+ - `typeProcess()` - 处理每个编译单元
+ - `printStoredMessages()` - 每个编译单元处理完后打印
+ - `typeProcessingOver()` - 所有处理完成
+
+## 设计方案
+
+### 方案概述
+
+采用类似 Error Prone 的非侵入式方法:
+1. 在消息存储/打印时,同时收集到 SARIF 数据结构
+2. 在 `typeProcessingOver()` 时生成 SARIF 报告文件
+3. 通过命令行选项控制是否生成报告
+
+### 设计原则
+
+1. **最小侵入**:不改变现有的消息输出逻辑
+2. **可选功能**:通过 `-AsarifOutput` 选项启用
+3. **向后兼容**:默认不生成报告,不影响现有行为
+4. **统一收集**:无论消息是立即打印还是存储,都收集到 SARIF
+
+### 实现方案
+
+#### 1. 新增命令行选项
+
+在 `@SupportedOptions` 中添加:
+```java
+// Generate SARIF report file
+// -AsarifOutput=path/to/report.sarif
+"sarifOutput",
+```
+
+#### 2. 创建 SARIF 报告生成器
+
+新建类:`org.checkerframework.framework.report.SarifReportGenerator`
+
+**职责:**
+- 收集所有诊断消息
+- 转换为 SARIF 格式
+- 写入文件
+
+**关键方法(POC 简化版):**
+```java
+public class SarifReportGenerator {
+ private final List results = new ArrayList<>();
+ private final Map artifacts = new HashMap<>();
+ private final ProcessingEnvironment processingEnv;
+
+ // 添加消息到报告(简化版:只收集 ERROR 和 WARNING)
+ public void addResult(Diagnostic.Kind kind, String message,
+ Tree source, CompilationUnitTree root,
+ SourceChecker checker, String messageKey) {
+ // 只处理 ERROR 和 WARNING
+ if (kind != Diagnostic.Kind.ERROR && kind != Diagnostic.Kind.MANDATORY_WARNING) {
+ return;
+ }
+
+ // 创建 Result 对象
+ Result result = new Result()
+ .withRuleId(messageKey)
+ .withLevel(kind == Diagnostic.Kind.ERROR ? "error" : "warning")
+ .withMessage(new Message().withText(message))
+ .withLocations(Arrays.asList(createLocation(source, root)));
+
+ results.add(result);
+
+ // 记录文件信息
+ addArtifact(root);
+ }
+
+ // 生成并写入 SARIF 文件
+ public void writeReport(Path outputPath) throws IOException {
+ SarifLog sarifLog = new SarifLog()
+ .withVersion("2.1.0")
+ .withRuns(Arrays.asList(
+ new Run()
+ .withTool(createTool())
+ .withArtifacts(new ArrayList<>(artifacts.values()))
+ .withResults(results)
+ ));
+
+ // 使用 Jackson 序列化为 JSON
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.writerWithDefaultPrettyPrinter().writeValue(outputPath.toFile(), sarifLog);
+ }
+
+ // 创建位置信息
+ private Location createLocation(Tree source, CompilationUnitTree root);
+
+ // 添加文件信息(包含源代码内容)
+ private void addArtifact(CompilationUnitTree root);
+
+ // 创建工具信息
+ private Tool createTool();
+}
+```
+
+#### 3. 集成点选择
+
+**选项 A:在 `printOrStoreMessage()` 中收集(推荐)**
+
+优点:
+- 统一收集点,无论消息是存储还是立即打印
+- 最小代码修改
+- 可以获取完整的消息信息
+
+修改点:
+```java
+protected void printOrStoreMessage(
+ javax.tools.Diagnostic.Kind kind,
+ String message,
+ Tree source,
+ CompilationUnitTree root) {
+ // ... 现有代码 ...
+
+ // 新增:收集到 SARIF
+ if (sarifReportGenerator != null) {
+ sarifReportGenerator.addResult(kind, message, source, root, this, messageKey);
+ }
+}
+```
+
+**选项 B:在 `report()` 方法中收集**
+
+优点:
+- 更早的收集点
+- 可以获取原始 messageKey 和 args
+
+缺点:
+- 需要传递更多参数
+- 可能收集到被抑制的消息
+
+#### 4. 初始化 SARIF 报告生成器
+
+在 `initChecker()` 中:
+```java
+public void initChecker() {
+ // ... 现有代码 ...
+
+ // 初始化 SARIF 报告生成器
+ if (hasOption("sarifOutput")) {
+ String outputPath = getOption("sarifOutput");
+ if (outputPath == null) {
+ throw new UserError("Must supply an argument to -AsarifOutput");
+ }
+ sarifReportGenerator = new SarifReportGenerator(processingEnv);
+ }
+}
+```
+
+#### 5. 生成报告文件
+
+在 `typeProcessingOver()` 中:
+```java
+@Override
+public void typeProcessingOver() {
+ for (SourceChecker checker : getSubcheckers()) {
+ checker.typeProcessingOver();
+ }
+
+ // 生成 SARIF 报告(仅在根 checker)
+ if (parentChecker == null && sarifReportGenerator != null) {
+ String outputPath = getOption("sarifOutput");
+ try {
+ sarifReportGenerator.writeReport(Paths.get(outputPath));
+ } catch (IOException e) {
+ logBugInCF(new BugInCF("Failed to write SARIF report", e));
+ }
+ }
+
+ super.typeProcessingOver();
+}
+```
+
+### SARIF 数据结构映射
+
+| Checker Framework | SARIF |
+|-----------------|-------|
+| `Diagnostic.Kind.ERROR` | `result.level: "error"` |
+| `Diagnostic.Kind.WARNING` | `result.level: "warning"` |
+| `messageKey` | `result.ruleId` |
+| `message` | `result.message.text` |
+| `Tree source` | `result.locations[0].physicalLocation` |
+| `SourceChecker` | `run.tool.driver.name` |
+| `CompilationUnitTree` | `run.artifacts[].location` |
+
+### 需要收集的信息
+
+1. **结果信息 (Result)**
+ - 消息类型(ERROR/WARNING)
+ - 消息文本
+ - 消息键(messageKey)
+ - 源代码位置(文件、行号、列号)
+ - 发出消息的 checker
+
+2. **工具信息 (Tool)**
+ - Checker Framework 版本
+ - Checker 名称
+ - 规则信息(从 messages.properties 提取)
+
+3. **文件信息 (Artifact)**
+ - 文件 URI
+ - 文件内容(可选,用于 SARIF viewer)
+
+## 实现细节讨论
+
+### 问题 1:消息键 (messageKey) 的获取
+
+**当前情况:**
+- `printOrStoreMessage()` 只接收格式化后的 `message` 字符串
+- 原始的 `messageKey` 在 `report()` 方法中
+
+**解决方案:**
+- 方案 A:修改 `printOrStoreMessage()` 签名,添加 `messageKey` 参数
+- 方案 B:在 `CheckerMessage` 中添加 `messageKey` 字段
+- 方案 C:从 `message` 中解析(不推荐,不可靠)
+
+**推荐:方案 B** - 修改 `CheckerMessage` 类,添加 `messageKey` 字段
+
+### 问题 2:子 checker 的消息收集
+
+**当前情况:**
+- Compound checker 有多个子 checker
+- 每个子 checker 共享 `messageStore`
+- 只有根 checker 的 `messageStore` 不为 null
+
+**解决方案:**
+- 所有 checker 共享同一个 `SarifReportGenerator` 实例
+- 在根 checker 初始化,传递给子 checker
+- 或者在根 checker 统一收集所有消息
+
+**推荐:** 在根 checker 初始化 `SarifReportGenerator`,子 checker 通过 `parentChecker` 访问
+
+### 问题 3:文件 URI 格式
+
+SARIF 要求使用 URI 格式的文件路径。
+
+**需要考虑:**
+- 相对路径 vs 绝对路径
+- Windows vs Unix 路径格式
+- 工作目录的处理
+
+**解决方案:**
+- 使用 `File.toURI()` 或 `Path.toUri()` 转换为 URI
+- 或者使用相对路径(相对于工作目录)
+
+### 问题 4:报告文件写入时机
+
+**选项 A:每个编译单元处理完后写入**
+- 优点:增量更新,可以看到实时进度
+- 缺点:多次文件 I/O,可能影响性能
+
+**选项 B:所有处理完成后一次性写入(推荐)**
+- 优点:性能好,文件一致
+- 缺点:如果崩溃,可能丢失数据
+
+**推荐:选项 B**,但可以考虑添加选项支持增量写入
+
+### 问题 5:与现有 `-Adetailedmsgtext` 选项的关系
+
+`-Adetailedmsgtext` 已经提供了可解析的输出格式。
+
+**关系:**
+- SARIF 是更标准化的格式
+- `-Adetailedmsgtext` 是自定义格式
+- 两者可以共存,服务于不同场景
+
+**建议:** 保持两者独立,用户可以选择使用哪种格式
+
+## 依赖管理
+
+### 需要添加的依赖
+
+在 `framework/build.gradle` 中添加:
+```gradle
+dependencies {
+ implementation 'com.contrastsecurity:java-sarif:2.0'
+ // Jackson 用于 JSON 序列化(java-sarif 依赖 Jackson)
+ implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.0' // 或兼容版本
+}
+```
+
+**依赖确认:**
+- ✅ 使用 [java-sarif](https://github.com/Contrast-Security-OSS/java-sarif) 库
+- ✅ MIT License,与 Checker Framework 兼容
+- ✅ 版本 2.0,支持 SARIF 2.1.0 规范
+
+## 测试策略
+
+### 单元测试
+1. 测试 SARIF 报告生成器
+2. 测试消息收集逻辑
+3. 测试文件写入
+
+### 集成测试
+1. 使用真实 checker 生成报告
+2. 验证 SARIF 文件格式正确性
+3. 验证与现有功能的兼容性
+
+### 验证工具
+- 使用 SARIF 验证工具验证生成的报告
+- 使用 GitHub 或其他支持 SARIF 的工具查看报告
+
+## 后续扩展
+
+### 可能的扩展方向
+
+1. **抑制系统集成**
+ - 从 SARIF 报告生成抑制文件
+ - 从抑制文件过滤 SARIF 结果
+
+2. **增量报告**
+ - 支持只报告新增问题
+ - 支持问题追踪
+
+3. **报告格式扩展**
+ - 支持其他格式(JSON、XML 等)
+ - 支持自定义格式
+
+4. **构建系统集成**
+ - Maven 插件支持
+ - Gradle 插件支持
+
+## 问题解答
+
+### 1. SARIF JSON 需要什么信息?MessageKey 里面带着吗?
+
+SARIF JSON 的基本结构:
+
+```json
+{
+ "version": "2.1.0",
+ "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
+ "runs": [{
+ "tool": {
+ "driver": {
+ "name": "Checker Framework",
+ "version": "3.51.2",
+ "rules": [...]
+ }
+ },
+ "artifacts": [...],
+ "results": [{
+ "ruleId": "assignment.type.incompatible", // ← messageKey 映射到这里
+ "level": "error",
+ "message": {
+ "text": "incompatible types in assignment"
+ },
+ "locations": [{
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "file:///path/to/File.java"
+ },
+ "region": {
+ "startLine": 10,
+ "startColumn": 5
+ }
+ }
+ }]
+ }]
+ }]
+}
+```
+
+**关键字段说明:**
+- `ruleId`: **messageKey 映射到这里**,用于标识触发该消息的规则
+- `level`: 消息级别(error/warning/note)
+- `message.text`: 格式化后的消息文本
+- `locations[].physicalLocation`: 源代码位置(文件 URI、行号、列号)
+- `artifacts[]`: 分析的文件列表(包含文件内容和 URI)
+
+### 2. 能否使用 java-sarif 库?
+
+**答案:可以!**
+
+根据 [java-sarif 库](https://github.com/Contrast-Security-OSS/java-sarif) 的信息:
+- **许可证**:MIT License(与 Checker Framework 兼容)
+- **版本**:2.0(最新版本)
+- **Maven 坐标**:
+ ```xml
+
+ com.contrastsecurity
+ java-sarif
+ 2.0
+
+ ```
+- **特点**:
+ - 使用 Jackson 进行 JSON 序列化/反序列化
+ - 提供方法链式构建 API
+ - 符合 SARIF 2.1.0 规范
+
+**使用示例:**
+```java
+import com.contrastsecurity.sarif.*;
+
+SarifLog sarifLog = new SarifLog()
+ .withVersion("2.1.0")
+ .withRuns(Arrays.asList(
+ new Run()
+ .withTool(new Tool()
+ .withDriver(new ToolComponent()
+ .withName("Checker Framework")
+ .withVersion("3.51.2")))
+ .withResults(Arrays.asList(
+ new Result()
+ .withRuleId("assignment.type.incompatible")
+ .withLevel("error")
+ .withMessage(new Message().withText("incompatible types"))
+ ))
+ ));
+```
+
+### 3. SARIF 生成完了怎么呈现?是写在一个文件里吗?
+
+**答案:是的,SARIF 是一个 JSON 文件。**
+
+**生成方式:**
+- 通过 `-AsarifOutput=path/to/report.sarif` 选项指定输出路径
+- 在 `typeProcessingOver()` 时一次性写入 JSON 文件
+- 文件格式:标准的 JSON,符合 SARIF 2.1.0 规范
+
+**呈现方式:**
+1. **GitHub Code Scanning**:上传 SARIF 文件到 GitHub,可以在 PR 中看到问题
+2. **VS Code SARIF Viewer**:使用 VS Code 插件查看
+3. **Azure DevOps**:集成到 CI/CD 流程
+4. **其他工具**:任何支持 SARIF 格式的工具都可以读取
+
+**文件示例:**
+```
+$ javac -processor NullnessChecker -AsarifOutput=report.sarif MyFile.java
+$ cat report.sarif
+{
+ "version": "2.1.0",
+ "runs": [...]
+}
+```
+
+### 4. 错误处理:SARIF 生成失败是否影响编译?
+
+**答案:不影响编译。**
+
+- SARIF 报告生成是**可选功能**,失败不应该影响编译
+- 如果生成失败,记录错误但不抛出异常
+- 可以输出警告信息,但编译继续进行
+
+### 5. 报告内容:是否包含源代码内容?
+
+**答案:需要包含。**
+
+原因:
+- 开发者需要源代码内容来定位问题
+- SARIF viewer 需要源代码来高亮显示问题位置
+- 便于离线查看报告
+
+实现方式:
+- 在 `artifacts[]` 中包含文件内容
+- 使用 `artifactLocation.uri` 指向文件
+- 使用 `artifact.contents.text` 存储源代码内容
+
+### 6. 初始实现:POC 版本,越简单越好
+
+**简化方案:**
+
+1. **最小实现范围**:
+ - 只收集基本的错误和警告
+ - 不处理 NOTE 类型的消息
+ - 简化规则信息(暂时不解析 messages.properties)
+
+2. **简化数据结构**:
+ - 只包含必需字段:ruleId, level, message, location
+ - 文件内容可选(先实现基本版本)
+
+3. **分阶段实现**:
+ - **Phase 1 (POC)**:基本消息收集和 SARIF 文件生成
+ - **Phase 2**:完善规则信息、文件内容等
+ - **Phase 3**:优化和扩展功能
+
+## 参考资源
+
+1. SARIF 规范:https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html
+2. Error Prone 讨论:https://github.com/google/error-prone/issues/3766
+3. Java SARIF 库:https://github.com/Contrast-Security-OSS/java-sarif
+4. GitHub SARIF 支持:https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning
+
+## POC 实现计划(简化版)
+
+### Phase 1: 基础实现(POC)
+
+**目标:** 能够生成基本的 SARIF 报告文件
+
+**任务清单:**
+1. ✅ 添加依赖:`com.contrastsecurity:java-sarif:2.0`
+2. 创建 `SarifReportGenerator` 类(简化版)
+3. 在 `SourceChecker` 中添加 `sarifReportGenerator` 字段
+4. 修改 `CheckerMessage` 添加 `messageKey` 字段
+5. 在 `printOrStoreMessage()` 中收集消息
+6. 在 `typeProcessingOver()` 中写入文件
+7. 添加 `-AsarifOutput` 选项
+
+**简化点:**
+- 只收集 ERROR 和 WARNING(忽略 NOTE)
+- 不解析 messages.properties(ruleId 直接用 messageKey)
+- 文件内容可选(先实现基本位置信息)
+- 不处理子 checker 的复杂情况(先支持单个 checker)
+
+### Phase 2: 完善功能
+
+- 支持所有消息类型
+- 完善规则信息(从 messages.properties 提取)
+- 支持 compound checker
+- 包含源代码内容
+
+### Phase 3: 优化和扩展
+
+- 性能优化
+- 增量报告
+- 抑制系统集成
+
+## 下一步行动
+
+1. ✅ 确认设计方案和依赖库
+2. 实现 POC 版本(Phase 1)
+3. 测试基本功能
+4. 验证 SARIF 文件格式
+5. 迭代完善(Phase 2 & 3)
+
diff --git a/framework/build.gradle b/framework/build.gradle
index 6bfa769eefc0..000d026c9b0c 100644
--- a/framework/build.gradle
+++ b/framework/build.gradle
@@ -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"))
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..42298e57a6eb
--- /dev/null
+++ b/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java
@@ -0,0 +1,89 @@
+package org.checkerframework.framework.report;
+
+import com.contrastsecurity.sarif.*;
+import com.contrastsecurity.sarif.Result.Level;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.processing.ProcessingEnvironment;
+
+/**
+ * Generates SARIF report files from checker diagnostics.
+ *
+ * This is a POC implementation for Phase 1.
+ */
+public class SarifReportGenerator {
+
+// private final ProcessingEnvironment processingEnv;
+ private final List results = new ArrayList<>();
+ private final Map artifacts = new HashMap<>();
+
+
+ public SarifReportGenerator(ProcessingEnvironment processingEnv) {
+// this.processingEnv = processingEnv;
+System.out.println("SarifReportGenerator constructor");
+ }
+
+
+ /**
+ * Add a diagnostic result to the report.
+ *
+ * @param kind the diagnostic kind
+ * @param message the message text
+ * @param messageKey the message key (rule ID)
+ */
+ public void addResult(
+ javax.tools.Diagnostic.Kind kind,
+ String message,
+ String messageKey) {
+ // TODO: For POC, just collect error and warning log
+ if (kind != javax.tools.Diagnostic.Kind.ERROR
+ && kind != javax.tools.Diagnostic.Kind.MANDATORY_WARNING) {
+ return;
+ }
+
+ Result result = new Result()
+ .withRuleId(messageKey)
+ .withLevel(kind == javax.tools.Diagnostic.Kind.ERROR ? Level.ERROR : Level.WARNING)
+ .withMessage(new Message().withText(message));
+
+ results.add(result);
+ }
+
+ private String getCheckerVersion() {
+ // Phase 1: 简化版本,返回固定值
+ return "3.51.2-SNAPSHOT";
+ }
+
+ /**
+ * 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)
+ .withArtifacts(new HashSet<>(artifacts.values()))));
+
+ // write into json file
+ ObjectMapper mapper = new ObjectMapper();
+ Path path = Paths.get(outputPath);
+ mapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), sarifLog);
+// results.clear();
+// artifacts.clear();
+ }
+}
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 ffa344b7672c..0da2a145f5a8 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;
@@ -433,7 +434,9 @@
// 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 {
@@ -618,6 +621,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;
@@ -736,6 +748,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.
*
@@ -1057,6 +1082,16 @@ public void typeProcessingOver() {
checker.typeProcessingOver();
}
+ if (parentChecker == null && sarifReportGenerator != 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();
}
@@ -1106,6 +1141,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. */
@@ -1474,7 +1518,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);
}
@@ -1557,15 +1601,20 @@ public Collection 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);
+ }
}
/**
@@ -1586,7 +1635,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);
}
@@ -2265,7 +2315,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);
}
}
@@ -3495,6 +3545,9 @@ protected static class CheckerMessage implements Comparable {
/** 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.
*
@@ -3503,18 +3556,21 @@ protected static class CheckerMessage implements Comparable {
* @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
diff --git a/test.sarif b/test.sarif
new file mode 100644
index 000000000000..a448ead93d1e
--- /dev/null
+++ b/test.sarif
@@ -0,0 +1,25 @@
+{
+ "version" : "2.1.0",
+ "runs" : [ {
+ "tool" : {
+ "driver" : {
+ "name" : "Checker Framework",
+ "version" : "3.51.2-SNAPSHOT"
+ }
+ },
+ "artifacts" : [ ],
+ "results" : [ {
+ "ruleId" : "assignment",
+ "level" : "error",
+ "message" : {
+ "text" : "[assignment] incompatible types in assignment.\nfound : Set<@KeyFor(\"sharedCounts1\") String>\nrequired: Set<@KeyFor({\"sharedBooks\", \"sharedCounts1\"}) String>"
+ }
+ }, {
+ "ruleId" : "assignment",
+ "level" : "error",
+ "message" : {
+ "text" : "[assignment] incompatible types in assignment.\nfound : Set<@KeyFor(\"sharedCounts2\") String>\nrequired: Set<@KeyFor({\"sharedBooks\", \"sharedCounts2\"}) String>"
+ }
+ } ]
+ } ]
+}
\ No newline at end of file
From 4dd55cfd871f0ec6d606b87157d5ba6c3d18233b Mon Sep 17 00:00:00 2001
From: Daniel Gao <1803483451@qq.com>
Date: Wed, 3 Dec 2025 02:43:35 +1100
Subject: [PATCH 2/7] wip: the simplest workflow
---
PHASE1_IMPLEMENTATION_STEPS.md | 82 +++++++++----------
SARIF_REPORT_DESIGN.md | 30 +++----
.../report/SarifReportGenerator.java | 53 ++++++------
.../framework/source/SourceChecker.java | 17 ++--
4 files changed, 93 insertions(+), 89 deletions(-)
diff --git a/PHASE1_IMPLEMENTATION_STEPS.md b/PHASE1_IMPLEMENTATION_STEPS.md
index 0889c030bbb5..3785264055fb 100644
--- a/PHASE1_IMPLEMENTATION_STEPS.md
+++ b/PHASE1_IMPLEMENTATION_STEPS.md
@@ -23,7 +23,7 @@
```java
/** True if the -AsarifOutput command-line argument was passed. */
private boolean sarifOutputEnabled = false;
-
+
/** Path to SARIF output file. */
private @Nullable String sarifOutputPath = null;
```
@@ -39,7 +39,7 @@
throw new UserError("Must supply an argument to -AsarifOutput");
}
// TODO: 临时日志输出,验证选项是否生效
- message(Diagnostic.Kind.NOTE,
+ message(Diagnostic.Kind.NOTE,
"SARIF output enabled: " + sarifOutputPath);
}
```
@@ -83,25 +83,25 @@ javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
- 创建基本类结构:
```java
package org.checkerframework.framework.report;
-
+
import javax.annotation.processing.ProcessingEnvironment;
-
+
/**
* Generates SARIF report files from checker diagnostics.
- *
+ *
* This is a POC implementation for Phase 1.
*/
public class SarifReportGenerator {
-
+
private final ProcessingEnvironment processingEnv;
-
+
public SarifReportGenerator(ProcessingEnvironment processingEnv) {
this.processingEnv = processingEnv;
}
-
+
/**
* Add a diagnostic result to the report.
- *
+ *
* @param kind the diagnostic kind
* @param message the message text
* @param messageKey the message key (rule ID)
@@ -113,10 +113,10 @@ javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
// TODO: Phase 1 - 暂时不实现,只记录日志
System.out.println("[SARIF] Would add result: " + messageKey + " - " + message);
}
-
+
/**
* Write the SARIF report to file.
- *
+ *
* @param outputPath the output file path
*/
public void writeReport(String outputPath) {
@@ -141,7 +141,7 @@ javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
throw new UserError("Must supply an argument to -AsarifOutput");
}
sarifReportGenerator = new SarifReportGenerator(processingEnv);
- message(Diagnostic.Kind.NOTE,
+ message(Diagnostic.Kind.NOTE,
"SARIF report generator initialized: " + sarifOutputPath);
}
```
@@ -211,12 +211,12 @@ javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
.withStartLine(10)
.withStartColumn(5))))))
));
-
+
// 写入 JSON 文件
ObjectMapper mapper = new ObjectMapper();
Path path = Paths.get(outputPath);
mapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), sarifLog);
-
+
System.out.println("[SARIF] Mock report written to: " + outputPath);
}
```
@@ -231,18 +231,18 @@ javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
for (SourceChecker checker : getSubcheckers()) {
checker.typeProcessingOver();
}
-
+
// Phase 1: 生成 SARIF 报告(仅在根 checker)
if (parentChecker == null && sarifReportGenerator != null) {
try {
sarifReportGenerator.writeReport(sarifOutputPath);
message(Diagnostic.Kind.NOTE, "SARIF report written to: " + sarifOutputPath);
} catch (IOException e) {
- message(Diagnostic.Kind.WARNING,
+ message(Diagnostic.Kind.WARNING,
"Failed to write SARIF report: " + e.getMessage());
}
}
-
+
super.typeProcessingOver();
}
```
@@ -333,7 +333,7 @@ javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
kind, message, source, this, trace, messageKey);
messageStore.add(checkerMessage);
}
-
+
// Phase 1: 收集消息到 SARIF(如果启用)
if (sarifReportGenerator != null && parentChecker == null) {
// 暂时使用 "unknown" 作为 messageKey
@@ -352,7 +352,7 @@ javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
String messageKey) {
// Phase 1: 只收集,不处理
// 暂时只记录日志,验证消息是否被收集
- System.out.println("[SARIF] Collected: " + messageKey + " - " +
+ System.out.println("[SARIF] Collected: " + messageKey + " - " +
kind + " - " + message.substring(0, Math.min(50, message.length())));
}
```
@@ -391,7 +391,7 @@ javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-
+
// 在类中添加字段
private final List results = new ArrayList<>();
private final Map artifacts = new HashMap<>();
@@ -403,18 +403,18 @@ javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
String message,
String messageKey) {
// Phase 1: 只收集 ERROR 和 WARNING
- if (kind != javax.tools.Diagnostic.Kind.ERROR
+ if (kind != javax.tools.Diagnostic.Kind.ERROR
&& kind != javax.tools.Diagnostic.Kind.MANDATORY_WARNING) {
return;
}
-
+
// 创建 Result 对象
String level = kind == javax.tools.Diagnostic.Kind.ERROR ? "error" : "warning";
Result result = new Result()
.withRuleId(messageKey)
.withLevel(level)
.withMessage(new Message().withText(message));
-
+
results.add(result);
}
```
@@ -428,24 +428,24 @@ javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
ToolComponent driver = new ToolComponent()
.withName("Checker Framework")
.withVersion(getCheckerVersion()); // 需要实现这个方法
-
+
// 创建 Run
Run run = new Run()
.withTool(new Tool().withDriver(driver))
.withResults(results)
.withArtifacts(new ArrayList<>(artifacts.values()));
-
+
// 创建 SarifLog
SarifLog sarifLog = new SarifLog()
.withVersion("2.1.0")
.withRuns(Collections.singletonList(run));
-
+
// 写入文件
ObjectMapper mapper = new ObjectMapper();
Path path = Paths.get(outputPath);
mapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), sarifLog);
}
-
+
private String getCheckerVersion() {
// Phase 1: 简化版本,返回固定值
return "3.51.2-SNAPSHOT";
@@ -524,7 +524,7 @@ javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
kind, message, source, this, trace, messageKey);
messageStore.add(checkerMessage);
}
-
+
// 收集消息到 SARIF
if (sarifReportGenerator != null && parentChecker == null) {
sarifReportGenerator.addResult(kind, message, messageKey);
@@ -617,16 +617,16 @@ cat test.sarif | jq '.runs[0].results[].ruleId'
```java
// 获取文件 URI
String fileUri = getFileUri(root);
-
+
// 获取位置信息(行号、列号)
Region region = getRegion(source, root);
-
+
// 创建 Location
Location location = new Location()
.withPhysicalLocation(new PhysicalLocation()
.withArtifactLocation(new ArtifactLocation().withUri(fileUri))
.withRegion(region));
-
+
// 创建 Result
String level = kind == javax.tools.Diagnostic.Kind.ERROR ? "error" : "warning";
Result result = new Result()
@@ -634,7 +634,7 @@ cat test.sarif | jq '.runs[0].results[].ruleId'
.withLevel(level)
.withMessage(new Message().withText(message))
.withLocations(Collections.singletonList(location));
-
+
results.add(result);
```
@@ -650,25 +650,25 @@ cat test.sarif | jq '.runs[0].results[].ruleId'
return "file:///unknown";
}
}
-
+
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 == -1 || endPos == -1) {
// 无法获取位置,返回默认值
return new Region().withStartLine(1).withStartColumn(1);
}
-
+
// 计算行号和列号(简化版本)
// Phase 1: 使用简单的行号计算
String sourceText = root.getSourceFile().getCharContent(true).toString();
int lineNumber = 1;
int columnNumber = 1;
-
+
for (int i = 0; i < startPos && i < sourceText.length(); i++) {
if (sourceText.charAt(i) == '\n') {
lineNumber++;
@@ -677,7 +677,7 @@ cat test.sarif | jq '.runs[0].results[].ruleId'
columnNumber++;
}
}
-
+
return new Region()
.withStartLine(lineNumber)
.withStartColumn(columnNumber)
@@ -746,16 +746,16 @@ cat test.sarif | jq '.runs[0].results[].ruleId'
try {
// 读取文件内容
String content = root.getSourceFile().getCharContent(true).toString();
-
+
// 创建 ArtifactContent
ArtifactContent artifactContent = new ArtifactContent()
.withText(content);
-
+
// 创建 Artifact
Artifact artifact = new Artifact()
.withLocation(new ArtifactLocation().withUri(fileUri))
.withContents(artifactContent);
-
+
artifacts.put(fileUri, artifact);
} catch (Exception e) {
// Phase 1: 如果读取失败,创建不包含内容的 artifact
diff --git a/SARIF_REPORT_DESIGN.md b/SARIF_REPORT_DESIGN.md
index ce1d8fbf4692..6b3f86e3effc 100644
--- a/SARIF_REPORT_DESIGN.md
+++ b/SARIF_REPORT_DESIGN.md
@@ -91,29 +91,29 @@ public class SarifReportGenerator {
private final List results = new ArrayList<>();
private final Map artifacts = new HashMap<>();
private final ProcessingEnvironment processingEnv;
-
+
// 添加消息到报告(简化版:只收集 ERROR 和 WARNING)
- public void addResult(Diagnostic.Kind kind, String message,
- Tree source, CompilationUnitTree root,
+ public void addResult(Diagnostic.Kind kind, String message,
+ Tree source, CompilationUnitTree root,
SourceChecker checker, String messageKey) {
// 只处理 ERROR 和 WARNING
if (kind != Diagnostic.Kind.ERROR && kind != Diagnostic.Kind.MANDATORY_WARNING) {
return;
}
-
+
// 创建 Result 对象
Result result = new Result()
.withRuleId(messageKey)
.withLevel(kind == Diagnostic.Kind.ERROR ? "error" : "warning")
.withMessage(new Message().withText(message))
.withLocations(Arrays.asList(createLocation(source, root)));
-
+
results.add(result);
-
+
// 记录文件信息
addArtifact(root);
}
-
+
// 生成并写入 SARIF 文件
public void writeReport(Path outputPath) throws IOException {
SarifLog sarifLog = new SarifLog()
@@ -124,18 +124,18 @@ public class SarifReportGenerator {
.withArtifacts(new ArrayList<>(artifacts.values()))
.withResults(results)
));
-
+
// 使用 Jackson 序列化为 JSON
ObjectMapper mapper = new ObjectMapper();
mapper.writerWithDefaultPrettyPrinter().writeValue(outputPath.toFile(), sarifLog);
}
-
+
// 创建位置信息
private Location createLocation(Tree source, CompilationUnitTree root);
-
+
// 添加文件信息(包含源代码内容)
private void addArtifact(CompilationUnitTree root);
-
+
// 创建工具信息
private Tool createTool();
}
@@ -158,7 +158,7 @@ protected void printOrStoreMessage(
Tree source,
CompilationUnitTree root) {
// ... 现有代码 ...
-
+
// 新增:收集到 SARIF
if (sarifReportGenerator != null) {
sarifReportGenerator.addResult(kind, message, source, root, this, messageKey);
@@ -182,7 +182,7 @@ protected void printOrStoreMessage(
```java
public void initChecker() {
// ... 现有代码 ...
-
+
// 初始化 SARIF 报告生成器
if (hasOption("sarifOutput")) {
String outputPath = getOption("sarifOutput");
@@ -203,7 +203,7 @@ public void typeProcessingOver() {
for (SourceChecker checker : getSubcheckers()) {
checker.typeProcessingOver();
}
-
+
// 生成 SARIF 报告(仅在根 checker)
if (parentChecker == null && sarifReportGenerator != null) {
String outputPath = getOption("sarifOutput");
@@ -213,7 +213,7 @@ public void typeProcessingOver() {
logBugInCF(new BugInCF("Failed to write SARIF report", e));
}
}
-
+
super.typeProcessingOver();
}
```
diff --git a/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java b/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java
index 42298e57a6eb..537eacb96fb2 100644
--- a/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java
+++ b/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java
@@ -12,7 +12,6 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
/**
@@ -22,38 +21,34 @@
*/
public class SarifReportGenerator {
-// private final ProcessingEnvironment processingEnv;
+ // private final ProcessingEnvironment processingEnv;
private final List results = new ArrayList<>();
private final Map artifacts = new HashMap<>();
-
public SarifReportGenerator(ProcessingEnvironment processingEnv) {
-// this.processingEnv = processingEnv;
-System.out.println("SarifReportGenerator constructor");
+ // this.processingEnv = processingEnv;
+ System.out.println("SarifReportGenerator constructor");
}
-
/**
* Add a diagnostic result to the report.
*
- * @param kind the diagnostic kind
- * @param message the message text
+ * @param kind the diagnostic kind
+ * @param message the message text
* @param messageKey the message key (rule ID)
*/
- public void addResult(
- javax.tools.Diagnostic.Kind kind,
- String message,
- String messageKey) {
+ public void addResult(javax.tools.Diagnostic.Kind kind, String message, String messageKey) {
// TODO: For POC, just collect error and warning log
if (kind != javax.tools.Diagnostic.Kind.ERROR
&& kind != javax.tools.Diagnostic.Kind.MANDATORY_WARNING) {
return;
}
- Result result = new Result()
- .withRuleId(messageKey)
- .withLevel(kind == javax.tools.Diagnostic.Kind.ERROR ? Level.ERROR : Level.WARNING)
- .withMessage(new Message().withText(message));
+ Result result =
+ new Result()
+ .withRuleId(messageKey)
+ .withLevel(kind == javax.tools.Diagnostic.Kind.ERROR ? Level.ERROR : Level.WARNING)
+ .withMessage(new Message().withText(message));
results.add(result);
}
@@ -70,20 +65,26 @@ private String getCheckerVersion() {
*/
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)
- .withArtifacts(new HashSet<>(artifacts.values()))));
+ 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)
+ .withArtifacts(new HashSet<>(artifacts.values()))));
// write into json file
ObjectMapper mapper = new ObjectMapper();
Path path = Paths.get(outputPath);
mapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), sarifLog);
-// results.clear();
-// artifacts.clear();
+ // results.clear();
+ // artifacts.clear();
}
}
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 0da2a145f5a8..c8a32ba7ce4d 100644
--- a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java
+++ b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java
@@ -435,7 +435,6 @@
// Use "-AconvertTypeArgInferenceCrashToWarning=false" to turn this option off and allow type
// argument inference crashes to crash the type checker.
"convertTypeArgInferenceCrashToWarning",
-
"sarifOutput"
})
public abstract class SourceChecker extends AbstractTypeProcessor implements OptionConfiguration {
@@ -1087,8 +1086,7 @@ public void typeProcessingOver() {
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());
+ message(Diagnostic.Kind.WARNING, "Failed to write SARIF report: " + e.getMessage());
}
}
@@ -1518,7 +1516,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,messageKey);
+ printOrStoreMessage(kind, messageText, (Tree) source, currentRoot, messageKey);
} else {
throw new BugInCF("invalid position source of class " + source.getClass() + ": " + source);
}
@@ -1601,13 +1599,18 @@ public Collection getSuppressWarningsPrefixesOfSubcheckers() {
* @param root the compilation unit
*/
protected void printOrStoreMessage(
- javax.tools.Diagnostic.Kind kind, String message, Tree source, CompilationUnitTree root, String messageKey) {
+ 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,messageKey);
+ printOrStoreMessage(kind, message, source, root, trace, messageKey);
} else {
- CheckerMessage checkerMessage = new CheckerMessage(kind, message, source, this, trace,messageKey);
+ 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
From 618c2317ff08419c58b3ba75ea2ce965a6259b07 Mon Sep 17 00:00:00 2001
From: Daniel Gao <1803483451@qq.com>
Date: Wed, 3 Dec 2025 22:58:10 +1100
Subject: [PATCH 3/7] Sarif POC
---
PHASE1_IMPLEMENTATION_STEPS.md | 853 ------------------
SARIF_REPORT_DESIGN.md | 572 ------------
.../report/SarifReportGenerator.java | 143 ++-
.../framework/source/SourceChecker.java | 2 +-
test.sarif | 25 -
5 files changed, 130 insertions(+), 1465 deletions(-)
delete mode 100644 PHASE1_IMPLEMENTATION_STEPS.md
delete mode 100644 SARIF_REPORT_DESIGN.md
delete mode 100644 test.sarif
diff --git a/PHASE1_IMPLEMENTATION_STEPS.md b/PHASE1_IMPLEMENTATION_STEPS.md
deleted file mode 100644
index 3785264055fb..000000000000
--- a/PHASE1_IMPLEMENTATION_STEPS.md
+++ /dev/null
@@ -1,853 +0,0 @@
-# Phase 1 (POC) 实现步骤详解
-
-## 步骤 1: 添加命令行选项(仅参数,无实现)
-
-### 目标
-添加 `-AsarifOutput` 命令行选项,当启用时输出日志信息。
-
-### 具体操作
-
-1. **在 `SourceChecker.java` 的 `@SupportedOptions` 注解中添加选项**
- - 文件:`framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java`
- - 位置:找到 `@SupportedOptions({...})` 注解(大约第 111 行)
- - 添加:
- ```java
- // Generate SARIF report file
- // -AsarifOutput=path/to/report.sarif
- "sarifOutput",
- ```
-
-2. **在 `SourceChecker` 类中添加字段**
- - 位置:在类的字段声明区域(大约第 640 行附近)
- - 添加:
- ```java
- /** True if the -AsarifOutput command-line argument was passed. */
- private boolean sarifOutputEnabled = false;
-
- /** Path to SARIF output file. */
- private @Nullable String sarifOutputPath = null;
- ```
-
-3. **在 `initChecker()` 方法中读取选项**
- - 位置:`initChecker()` 方法中(大约第 1103 行附近,在设置其他选项的地方)
- - 添加:
- ```java
- sarifOutputEnabled = hasOption("sarifOutput");
- if (sarifOutputEnabled) {
- sarifOutputPath = getOption("sarifOutput");
- if (sarifOutputPath == null) {
- throw new UserError("Must supply an argument to -AsarifOutput");
- }
- // TODO: 临时日志输出,验证选项是否生效
- message(Diagnostic.Kind.NOTE,
- "SARIF output enabled: " + sarifOutputPath);
- }
- ```
-
-### 验证方法
-
-运行测试命令:
-```bash
-javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
-```
-
-预期输出:应该看到 NOTE 消息:"SARIF output enabled: test.sarif"
-
-### 调试提示
-
-- 如果看不到 NOTE 消息,检查 `hasOption("sarifOutput")` 是否正确
-- 如果抛出 UserError,检查选项值是否正确传递
-
----
-
-## 步骤 2: 添加依赖并创建空的 SarifReportGenerator 类
-
-### 目标
-添加 java-sarif 依赖,创建 SarifReportGenerator 类的骨架(不实现功能)。
-
-### 具体操作
-
-1. **添加 Maven/Gradle 依赖**
- - 文件:`framework/build.gradle`
- - 在 `dependencies` 块中添加:
- ```gradle
- dependencies {
- // ... 现有依赖 ...
- implementation 'com.contrastsecurity:java-sarif:2.0'
- implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.0'
- }
- ```
-
-2. **创建 SarifReportGenerator 类文件**
- - 文件:`framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java`
- - 创建基本类结构:
- ```java
- package org.checkerframework.framework.report;
-
- import javax.annotation.processing.ProcessingEnvironment;
-
- /**
- * Generates SARIF report files from checker diagnostics.
- *
- * This is a POC implementation for Phase 1.
- */
- public class SarifReportGenerator {
-
- private final ProcessingEnvironment processingEnv;
-
- public SarifReportGenerator(ProcessingEnvironment processingEnv) {
- this.processingEnv = processingEnv;
- }
-
- /**
- * Add a diagnostic result to the report.
- *
- * @param kind the diagnostic kind
- * @param message the message text
- * @param messageKey the message key (rule ID)
- */
- public void addResult(
- javax.tools.Diagnostic.Kind kind,
- String message,
- String messageKey) {
- // TODO: Phase 1 - 暂时不实现,只记录日志
- System.out.println("[SARIF] Would add result: " + messageKey + " - " + message);
- }
-
- /**
- * Write the SARIF report to file.
- *
- * @param outputPath the output file path
- */
- public void writeReport(String outputPath) {
- // TODO: Phase 1 - 暂时不实现,只记录日志
- System.out.println("[SARIF] Would write report to: " + outputPath);
- }
- }
- ```
-
-3. **在 `SourceChecker` 中添加字段和初始化**
- - 文件:`framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java`
- - 在字段声明区域添加:
- ```java
- /** SARIF report generator, if enabled. */
- private @Nullable SarifReportGenerator sarifReportGenerator = null;
- ```
- - 在 `initChecker()` 中(步骤 1 的代码之后)添加:
- ```java
- if (sarifOutputEnabled) {
- sarifOutputPath = getOption("sarifOutput");
- if (sarifOutputPath == null) {
- throw new UserError("Must supply an argument to -AsarifOutput");
- }
- sarifReportGenerator = new SarifReportGenerator(processingEnv);
- message(Diagnostic.Kind.NOTE,
- "SARIF report generator initialized: " + sarifOutputPath);
- }
- ```
-
-### 验证方法
-
-运行测试命令:
-```bash
-javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
-```
-
-预期输出:
-- NOTE 消息:"SARIF report generator initialized: test.sarif"
-- 编译应该成功(没有错误)
-
-### 调试提示
-
-- 如果编译失败,检查依赖是否正确添加
-- 如果找不到类,检查包名和导入语句
-
----
-
-## 步骤 3: 使用 Mock 数据生成 SARIF 文件
-
-### 目标
-使用硬编码的 mock 数据生成一个有效的 SARIF JSON 文件,验证整个流程。
-
-### 具体操作
-
-1. **更新 `SarifReportGenerator` 类,添加 mock 数据生成**
- - 文件:`framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java`
- - 添加必要的导入:
- ```java
- import com.contrastsecurity.sarif.*;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import java.io.IOException;
- import java.nio.file.Files;
- import java.nio.file.Path;
- import java.nio.file.Paths;
- import java.util.Arrays;
- import java.util.Collections;
- ```
- - 更新 `writeReport()` 方法:
- ```java
- public void writeReport(String outputPath) throws IOException {
- // Phase 1: 使用 mock 数据生成 SARIF 文件
- SarifLog sarifLog = new SarifLog()
- .withVersion("2.1.0")
- .withRuns(Collections.singletonList(
- new Run()
- .withTool(new Tool()
- .withDriver(new ToolComponent()
- .withName("Checker Framework")
- .withVersion("3.51.2-SNAPSHOT")))
- .withResults(Collections.singletonList(
- new Result()
- .withRuleId("mock.rule.id")
- .withLevel("error")
- .withMessage(new Message()
- .withText("This is a mock SARIF result for testing"))
- .withLocations(Collections.singletonList(
- new Location()
- .withPhysicalLocation(new PhysicalLocation()
- .withArtifactLocation(new ArtifactLocation()
- .withUri("file:///mock/Test.java"))
- .withRegion(new Region()
- .withStartLine(10)
- .withStartColumn(5))))))
- ));
-
- // 写入 JSON 文件
- ObjectMapper mapper = new ObjectMapper();
- Path path = Paths.get(outputPath);
- mapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), sarifLog);
-
- System.out.println("[SARIF] Mock report written to: " + outputPath);
- }
- ```
-
-2. **在 `SourceChecker.typeProcessingOver()` 中调用写入**
- - 文件:`framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java`
- - 位置:`typeProcessingOver()` 方法(大约第 1055 行)
- - 修改:
- ```java
- @Override
- public void typeProcessingOver() {
- for (SourceChecker checker : getSubcheckers()) {
- checker.typeProcessingOver();
- }
-
- // Phase 1: 生成 SARIF 报告(仅在根 checker)
- if (parentChecker == null && sarifReportGenerator != 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();
- }
- ```
-
-### 验证方法
-
-1. **运行测试命令:**
- ```bash
- javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
- ```
-
-2. **检查输出文件:**
- ```bash
- cat test.sarif
- ```
-
-预期结果:
-- 生成 `test.sarif` 文件
-- 文件包含有效的 JSON
-- JSON 符合 SARIF 2.1.0 格式
-- 包含一个 mock 结果
-
-3. **验证 SARIF 格式(可选):**
- - 使用在线 SARIF 验证器:https://sarifweb.azurewebsites.net/Validator
- - 或使用 VS Code SARIF Viewer 插件查看
-
-### 调试提示
-
-- 如果文件没有生成,检查文件路径和权限
-- 如果 JSON 格式错误,检查 ObjectMapper 配置
-- 如果抛出异常,检查依赖是否正确加载
-
----
-
-## 步骤 4: 在 printOrStoreMessage 中收集消息(不写入文件)
-
-### 目标
-在消息打印/存储时,同时收集到 SarifReportGenerator,但不立即写入文件。
-
-### 具体操作
-
-1. **修改 `CheckerMessage` 类,添加 `messageKey` 字段**
- - 文件:`framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java`
- - 位置:`CheckerMessage` 内部类(大约第 3479 行)
- - 添加字段:
- ```java
- /** The message key (rule ID) for this message. */
- final String messageKey;
- ```
- - 更新构造函数:
- ```java
- protected CheckerMessage(
- Diagnostic.Kind kind,
- String message,
- @FindDistinct Tree source,
- @FindDistinct SourceChecker checker,
- StackTraceElement[] trace,
- String messageKey) { // 新增参数
- this.kind = kind;
- this.message = message;
- this.source = source;
- this.checker = checker;
- this.trace = trace;
- this.messageKey = messageKey; // 新增赋值
- }
- ```
-
-2. **修改 `printOrStoreMessage()` 方法,传递 messageKey**
- - 文件:`framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java`
- - 位置:`printOrStoreMessage()` 方法(大约第 1559 行)
- - 问题:当前方法没有 messageKey 参数
- - 解决方案:暂时使用 "unknown" 作为占位符,后续步骤会修复
- - 修改:
- ```java
- protected void printOrStoreMessage(
- javax.tools.Diagnostic.Kind kind,
- String message,
- Tree source,
- CompilationUnitTree root) {
- assert this.currentRoot == root;
- StackTraceElement[] trace = Thread.currentThread().getStackTrace();
- if (messageStore == null) {
- printOrStoreMessage(kind, message, source, root, trace);
- } else {
- // Phase 1: 暂时使用 "unknown" 作为 messageKey
- String messageKey = "unknown";
- CheckerMessage checkerMessage = new CheckerMessage(
- kind, message, source, this, trace, messageKey);
- messageStore.add(checkerMessage);
- }
-
- // Phase 1: 收集消息到 SARIF(如果启用)
- if (sarifReportGenerator != null && parentChecker == null) {
- // 暂时使用 "unknown" 作为 messageKey
- sarifReportGenerator.addResult(kind, message, "unknown");
- }
- }
- ```
-
-3. **更新 `SarifReportGenerator.addResult()` 方法签名**
- - 文件:`framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java`
- - 修改方法:
- ```java
- public void addResult(
- javax.tools.Diagnostic.Kind kind,
- String message,
- String messageKey) {
- // Phase 1: 只收集,不处理
- // 暂时只记录日志,验证消息是否被收集
- System.out.println("[SARIF] Collected: " + messageKey + " - " +
- kind + " - " + message.substring(0, Math.min(50, message.length())));
- }
- ```
-
-### 验证方法
-
-运行测试命令:
-```bash
-javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
-```
-
-预期输出:
-- 应该看到多个 `[SARIF] Collected:` 日志消息
-- 每个诊断消息都应该被收集
-- 文件仍然包含 mock 数据(因为还没实现真实数据写入)
-
-### 调试提示
-
-- 如果没有看到收集日志,检查 `sarifReportGenerator != null` 条件
-- 如果只看到部分消息,检查 `parentChecker == null` 条件(可能子 checker 也在收集)
-
----
-
-## 步骤 5: 实现真实数据收集和 SARIF 生成
-
-### 目标
-使用真实收集的消息数据生成 SARIF 文件,替换 mock 数据。
-
-### 具体操作
-
-1. **在 `SarifReportGenerator` 中添加数据存储**
- - 文件:`framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java`
- - 添加字段:
- ```java
- import java.util.ArrayList;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
-
- // 在类中添加字段
- private final List results = new ArrayList<>();
- private final Map artifacts = new HashMap<>();
- ```
- - 更新 `addResult()` 方法:
- ```java
- public void addResult(
- javax.tools.Diagnostic.Kind kind,
- String message,
- String messageKey) {
- // Phase 1: 只收集 ERROR 和 WARNING
- if (kind != javax.tools.Diagnostic.Kind.ERROR
- && kind != javax.tools.Diagnostic.Kind.MANDATORY_WARNING) {
- return;
- }
-
- // 创建 Result 对象
- String level = kind == javax.tools.Diagnostic.Kind.ERROR ? "error" : "warning";
- Result result = new Result()
- .withRuleId(messageKey)
- .withLevel(level)
- .withMessage(new Message().withText(message));
-
- results.add(result);
- }
- ```
-
-2. **更新 `writeReport()` 方法,使用真实数据**
- - 文件:`framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java`
- - 修改方法:
- ```java
- public void writeReport(String outputPath) throws IOException {
- // 创建 Tool 信息
- ToolComponent driver = new ToolComponent()
- .withName("Checker Framework")
- .withVersion(getCheckerVersion()); // 需要实现这个方法
-
- // 创建 Run
- Run run = new Run()
- .withTool(new Tool().withDriver(driver))
- .withResults(results)
- .withArtifacts(new ArrayList<>(artifacts.values()));
-
- // 创建 SarifLog
- SarifLog sarifLog = new SarifLog()
- .withVersion("2.1.0")
- .withRuns(Collections.singletonList(run));
-
- // 写入文件
- ObjectMapper mapper = new ObjectMapper();
- Path path = Paths.get(outputPath);
- mapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), sarifLog);
- }
-
- private String getCheckerVersion() {
- // Phase 1: 简化版本,返回固定值
- return "3.51.2-SNAPSHOT";
- }
- ```
-
-3. **清空结果列表(在写入后)**
- - 在 `writeReport()` 方法末尾添加:
- ```java
- // 清空结果,为下次运行做准备
- results.clear();
- artifacts.clear();
- ```
-
-### 验证方法
-
-1. **运行测试命令:**
- ```bash
- javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
- ```
-
-2. **检查生成的 SARIF 文件:**
- ```bash
- cat test.sarif | jq '.runs[0].results | length'
- ```
-
-预期结果:
-- SARIF 文件包含真实的结果(不再是 mock 数据)
-- 结果数量应该与收集的消息数量一致
-- 每个结果都有正确的 ruleId、level 和 message
-
-### 调试提示
-
-- 如果结果数量为 0,检查 `addResult()` 是否被正确调用
-- 如果 ruleId 都是 "unknown",需要继续下一步获取真实的 messageKey
-- 如果 JSON 格式错误,检查 ObjectMapper 序列化
-
----
-
-## 步骤 6: 获取真实的 messageKey
-
-### 目标
-从 `report()` 方法传递真实的 messageKey 到 `printOrStoreMessage()`。
-
-### 具体操作
-
-1. **修改 `report()` 方法,传递 messageKey**
- - 文件:`framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java`
- - 位置:`report()` 方法(大约第 1426 行)
- - 问题:需要将 messageKey 传递到 `printOrStoreMessage()`
- - 解决方案:修改 `printOrStoreMessage()` 方法签名,添加 messageKey 参数
- - 修改 `report()` 方法中调用 `printOrStoreMessage()` 的地方:
- ```java
- if (source instanceof Tree) {
- printOrStoreMessage(kind, messageText, (Tree) source, currentRoot, messageKey);
- }
- ```
-
-2. **更新 `printOrStoreMessage()` 方法签名**
- - 文件:`framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java`
- - 位置:`printOrStoreMessage()` 方法(大约第 1559 行)
- - 修改方法签名:
- ```java
- protected void printOrStoreMessage(
- 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, messageKey);
- } else {
- CheckerMessage checkerMessage = new CheckerMessage(
- kind, message, source, this, trace, messageKey);
- messageStore.add(checkerMessage);
- }
-
- // 收集消息到 SARIF
- if (sarifReportGenerator != null && parentChecker == null) {
- sarifReportGenerator.addResult(kind, message, messageKey);
- }
- }
- ```
-
-3. **更新另一个 `printOrStoreMessage()` 重载方法**
- - 位置:`printOrStoreMessage()` 的另一个重载(大约第 1584 行)
- - 修改:
- ```java
- protected void printOrStoreMessage(
- javax.tools.Diagnostic.Kind kind,
- String message,
- Tree source,
- CompilationUnitTree root,
- StackTraceElement[] trace,
- String messageKey) { // 新增参数
- Trees.instance(processingEnv).printMessage(kind, message, source, root);
- printStackTrace(trace);
- }
- ```
-
-4. **更新 `printStoredMessages()` 方法**
- - 位置:`printStoredMessages()` 方法(大约第 2263 行)
- - 修改:
- ```java
- protected void printStoredMessages(CompilationUnitTree unit) {
- if (messageStore == null || parentChecker != null) {
- return;
- }
- for (CheckerMessage msg : messageStore) {
- printOrStoreMessage(msg.kind, msg.message, msg.source, unit, msg.trace, msg.messageKey);
- }
- }
- ```
-
-### 验证方法
-
-运行测试命令:
-```bash
-javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
-```
-
-检查 SARIF 文件中的 ruleId:
-```bash
-cat test.sarif | jq '.runs[0].results[].ruleId'
-```
-
-预期结果:
-- ruleId 应该是真实的 messageKey(如 "assignment.type.incompatible")
-- 不再是 "unknown"
-
-### 调试提示
-
-- 如果 ruleId 仍然是 "unknown",检查方法调用链是否正确传递参数
-- 如果编译错误,检查所有调用 `printOrStoreMessage()` 的地方是否都更新了
-
----
-
-## 步骤 7: 添加位置信息(文件 URI、行号、列号)
-
-### 目标
-在 SARIF 结果中添加源代码位置信息(文件 URI、行号、列号)。
-
-### 具体操作
-
-1. **修改 `SarifReportGenerator.addResult()` 方法,添加位置参数**
- - 文件:`framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java`
- - 添加必要的导入:
- ```java
- import com.sun.source.tree.Tree;
- import com.sun.source.tree.CompilationUnitTree;
- import com.sun.source.util.SourcePositions;
- import com.sun.source.util.Trees;
- import javax.annotation.processing.ProcessingEnvironment;
- ```
- - 修改方法签名:
- ```java
- public void addResult(
- javax.tools.Diagnostic.Kind kind,
- String message,
- String messageKey,
- Tree source,
- CompilationUnitTree root) { // 新增参数
- ```
-
-2. **实现位置信息提取**
- - 在 `addResult()` 方法中添加:
- ```java
- // 获取文件 URI
- String fileUri = getFileUri(root);
-
- // 获取位置信息(行号、列号)
- Region region = getRegion(source, root);
-
- // 创建 Location
- Location location = new Location()
- .withPhysicalLocation(new PhysicalLocation()
- .withArtifactLocation(new ArtifactLocation().withUri(fileUri))
- .withRegion(region));
-
- // 创建 Result
- String level = kind == javax.tools.Diagnostic.Kind.ERROR ? "error" : "warning";
- Result result = new Result()
- .withRuleId(messageKey)
- .withLevel(level)
- .withMessage(new Message().withText(message))
- .withLocations(Collections.singletonList(location));
-
- results.add(result);
- ```
-
-3. **实现辅助方法**
- - 在 `SarifReportGenerator` 类中添加:
- ```java
- private String getFileUri(CompilationUnitTree root) {
- // Phase 1: 简化版本,使用文件路径转换为 URI
- try {
- java.io.File file = new java.io.File(root.getSourceFile().getName());
- return file.toURI().toString();
- } catch (Exception e) {
- return "file:///unknown";
- }
- }
-
- 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 == -1 || endPos == -1) {
- // 无法获取位置,返回默认值
- return new Region().withStartLine(1).withStartColumn(1);
- }
-
- // 计算行号和列号(简化版本)
- // Phase 1: 使用简单的行号计算
- String sourceText = root.getSourceFile().getCharContent(true).toString();
- int lineNumber = 1;
- int columnNumber = 1;
-
- for (int i = 0; i < startPos && i < sourceText.length(); i++) {
- if (sourceText.charAt(i) == '\n') {
- lineNumber++;
- columnNumber = 1;
- } else {
- columnNumber++;
- }
- }
-
- return new Region()
- .withStartLine(lineNumber)
- .withStartColumn(columnNumber)
- .withEndLine(lineNumber) // Phase 1: 简化,结束位置同开始位置
- .withEndColumn(columnNumber);
- }
- ```
-
-4. **更新 `SourceChecker` 中的调用**
- - 文件:`framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java`
- - 在 `printOrStoreMessage()` 方法中:
- ```java
- // 收集消息到 SARIF
- if (sarifReportGenerator != null && parentChecker == null) {
- sarifReportGenerator.addResult(kind, message, messageKey, source, root);
- }
- ```
-
-### 验证方法
-
-1. **运行测试命令:**
- ```bash
- javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
- ```
-
-2. **检查 SARIF 文件中的位置信息:**
- ```bash
- cat test.sarif | jq '.runs[0].results[0].locations[0].physicalLocation'
- ```
-
-预期结果:
-- 每个结果都有 `physicalLocation`
-- `artifactLocation.uri` 包含文件 URI
-- `region` 包含 `startLine` 和 `startColumn`
-
-### 调试提示
-
-- 如果 URI 是 "file:///unknown",检查文件路径获取逻辑
-- 如果行号/列号不正确,检查 `getRegion()` 方法的计算逻辑
-- 如果位置信息缺失,检查 `source` 和 `root` 参数是否正确传递
-
----
-
-## 步骤 8: 添加文件内容到 artifacts(可选,但推荐)
-
-### 目标
-在 SARIF 的 `artifacts` 中包含源代码文件内容,方便 SARIF viewer 显示。
-
-### 具体操作
-
-1. **在 `addResult()` 中记录文件信息**
- - 文件:`framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java`
- - 在 `addResult()` 方法开始处添加:
- ```java
- // 记录文件信息到 artifacts
- String fileUri = getFileUri(root);
- if (!artifacts.containsKey(fileUri)) {
- addArtifact(root, fileUri);
- }
- ```
-
-2. **实现 `addArtifact()` 方法**
- - 在 `SarifReportGenerator` 类中添加:
- ```java
- private void addArtifact(CompilationUnitTree root, String fileUri) {
- try {
- // 读取文件内容
- String content = root.getSourceFile().getCharContent(true).toString();
-
- // 创建 ArtifactContent
- ArtifactContent artifactContent = new ArtifactContent()
- .withText(content);
-
- // 创建 Artifact
- Artifact artifact = new Artifact()
- .withLocation(new ArtifactLocation().withUri(fileUri))
- .withContents(artifactContent);
-
- artifacts.put(fileUri, artifact);
- } catch (Exception e) {
- // Phase 1: 如果读取失败,创建不包含内容的 artifact
- Artifact artifact = new Artifact()
- .withLocation(new ArtifactLocation().withUri(fileUri));
- artifacts.put(fileUri, artifact);
- }
- }
- ```
-
-3. **确保 artifacts 在写入时包含**
- - 在 `writeReport()` 方法中已经包含了 artifacts,无需修改
-
-### 验证方法
-
-1. **运行测试命令:**
- ```bash
- javac -processor NullnessChecker -AsarifOutput=test.sarif Test.java
- ```
-
-2. **检查 SARIF 文件中的 artifacts:**
- ```bash
- cat test.sarif | jq '.runs[0].artifacts[0].contents.text' | head -20
- ```
-
-预期结果:
-- `artifacts` 数组包含分析的文件
-- 每个 artifact 的 `contents.text` 包含源代码内容
-
-### 调试提示
-
-- 如果文件内容为空,检查文件读取逻辑
-- 如果文件很大,考虑是否限制内容大小(Phase 1 可以暂时不限制)
-
----
-
-## 步骤 9: 清理和优化
-
-### 目标
-移除调试日志,优化代码,确保 POC 版本稳定。
-
-### 具体操作
-
-1. **移除所有 `System.out.println` 调试日志**
- - 在 `SarifReportGenerator` 中移除所有调试输出
- - 在 `SourceChecker` 中移除临时的 NOTE 消息(可选,可以保留一个确认消息)
-
-2. **错误处理优化**
- - 确保所有异常都被正确处理
- - 在 `typeProcessingOver()` 中,如果写入失败,只记录警告,不影响编译
-
-3. **代码清理**
- - 移除 TODO 注释中的 "Phase 1" 标记(如果不再需要)
- - 添加必要的 JavaDoc 注释
-
-4. **最终验证**
- - 运行完整的测试套件
- - 验证生成的 SARIF 文件可以被标准工具读取
-
-### 验证方法
-
-1. **运行多个测试用例:**
- ```bash
- javac -processor NullnessChecker -AsarifOutput=test.sarif Test1.java Test2.java
- ```
-
-2. **使用 SARIF 验证器验证文件格式**
-
-3. **使用 VS Code SARIF Viewer 查看报告**
-
-### 完成标准
-
-- ✅ 可以生成有效的 SARIF 2.1.0 格式文件
-- ✅ 包含真实的诊断结果(ruleId、level、message)
-- ✅ 包含位置信息(文件 URI、行号、列号)
-- ✅ 包含源代码内容(可选但推荐)
-- ✅ 不影响现有功能
-- ✅ 错误处理完善
-
----
-
-## 总结
-
-Phase 1 (POC) 实现完成后的功能:
-- 通过 `-AsarifOutput` 选项启用 SARIF 报告生成
-- 收集所有 ERROR 和 WARNING 类型的诊断消息
-- 生成符合 SARIF 2.1.0 标准的 JSON 文件
-- 包含基本的位置信息和源代码内容
-
-下一步(Phase 2)可以完善:
-- 支持所有消息类型
-- 完善规则信息(从 messages.properties 提取)
-- 支持 compound checker
-- 性能优化
-
diff --git a/SARIF_REPORT_DESIGN.md b/SARIF_REPORT_DESIGN.md
deleted file mode 100644
index 6b3f86e3effc..000000000000
--- a/SARIF_REPORT_DESIGN.md
+++ /dev/null
@@ -1,572 +0,0 @@
-# SARIF Report Generation Design Document
-
-## 背景和目标
-
-### 当前问题
-1. Checker Framework 目前只将警告和错误输出到控制台
-2. 在使用 Maven 或 Gradle 等构建系统时,输出顺序可能不一致(已知 bug)
-3. 缺乏可解析的报告文件,难以构建通用的抑制系统
-
-### 目标
-- 生成可解析的 SARIF 格式报告文件
-- 最小侵入地集成到现有代码
-- 保持向后兼容(不影响现有控制台输出)
-- 支持后续扩展(如通用抑制系统)
-
-## 当前代码架构分析
-
-### 消息输出流程
-
-```
-report() / reportError() / reportWarning()
- ↓
-printOrStoreMessage()
- ↓
-[如果有 messageStore] → CheckerMessage → messageStore (TreeSet)
- ↓
-[处理完编译单元后] → printStoredMessages()
- ↓
-Trees.printMessage() → 控制台输出
-```
-
-### 关键类和字段
-
-1. **SourceChecker**
- - `messageStore: TreeSet` - 存储消息(仅 compound checker 使用)
- - `printOrStoreMessage()` - 决定是存储还是立即打印
- - `printStoredMessages()` - 打印存储的消息
-
-2. **CheckerMessage**
- - `kind: Diagnostic.Kind` - 消息类型(ERROR, WARNING 等)
- - `message: String` - 消息文本
- - `source: Tree` - 源代码位置
- - `checker: SourceChecker` - 发出消息的 checker
- - `trace: StackTraceElement[]` - 堆栈跟踪
-
-3. **关键方法调用时机**
- - `typeProcessingStart()` - 初始化
- - `typeProcess()` - 处理每个编译单元
- - `printStoredMessages()` - 每个编译单元处理完后打印
- - `typeProcessingOver()` - 所有处理完成
-
-## 设计方案
-
-### 方案概述
-
-采用类似 Error Prone 的非侵入式方法:
-1. 在消息存储/打印时,同时收集到 SARIF 数据结构
-2. 在 `typeProcessingOver()` 时生成 SARIF 报告文件
-3. 通过命令行选项控制是否生成报告
-
-### 设计原则
-
-1. **最小侵入**:不改变现有的消息输出逻辑
-2. **可选功能**:通过 `-AsarifOutput` 选项启用
-3. **向后兼容**:默认不生成报告,不影响现有行为
-4. **统一收集**:无论消息是立即打印还是存储,都收集到 SARIF
-
-### 实现方案
-
-#### 1. 新增命令行选项
-
-在 `@SupportedOptions` 中添加:
-```java
-// Generate SARIF report file
-// -AsarifOutput=path/to/report.sarif
-"sarifOutput",
-```
-
-#### 2. 创建 SARIF 报告生成器
-
-新建类:`org.checkerframework.framework.report.SarifReportGenerator`
-
-**职责:**
-- 收集所有诊断消息
-- 转换为 SARIF 格式
-- 写入文件
-
-**关键方法(POC 简化版):**
-```java
-public class SarifReportGenerator {
- private final List results = new ArrayList<>();
- private final Map artifacts = new HashMap<>();
- private final ProcessingEnvironment processingEnv;
-
- // 添加消息到报告(简化版:只收集 ERROR 和 WARNING)
- public void addResult(Diagnostic.Kind kind, String message,
- Tree source, CompilationUnitTree root,
- SourceChecker checker, String messageKey) {
- // 只处理 ERROR 和 WARNING
- if (kind != Diagnostic.Kind.ERROR && kind != Diagnostic.Kind.MANDATORY_WARNING) {
- return;
- }
-
- // 创建 Result 对象
- Result result = new Result()
- .withRuleId(messageKey)
- .withLevel(kind == Diagnostic.Kind.ERROR ? "error" : "warning")
- .withMessage(new Message().withText(message))
- .withLocations(Arrays.asList(createLocation(source, root)));
-
- results.add(result);
-
- // 记录文件信息
- addArtifact(root);
- }
-
- // 生成并写入 SARIF 文件
- public void writeReport(Path outputPath) throws IOException {
- SarifLog sarifLog = new SarifLog()
- .withVersion("2.1.0")
- .withRuns(Arrays.asList(
- new Run()
- .withTool(createTool())
- .withArtifacts(new ArrayList<>(artifacts.values()))
- .withResults(results)
- ));
-
- // 使用 Jackson 序列化为 JSON
- ObjectMapper mapper = new ObjectMapper();
- mapper.writerWithDefaultPrettyPrinter().writeValue(outputPath.toFile(), sarifLog);
- }
-
- // 创建位置信息
- private Location createLocation(Tree source, CompilationUnitTree root);
-
- // 添加文件信息(包含源代码内容)
- private void addArtifact(CompilationUnitTree root);
-
- // 创建工具信息
- private Tool createTool();
-}
-```
-
-#### 3. 集成点选择
-
-**选项 A:在 `printOrStoreMessage()` 中收集(推荐)**
-
-优点:
-- 统一收集点,无论消息是存储还是立即打印
-- 最小代码修改
-- 可以获取完整的消息信息
-
-修改点:
-```java
-protected void printOrStoreMessage(
- javax.tools.Diagnostic.Kind kind,
- String message,
- Tree source,
- CompilationUnitTree root) {
- // ... 现有代码 ...
-
- // 新增:收集到 SARIF
- if (sarifReportGenerator != null) {
- sarifReportGenerator.addResult(kind, message, source, root, this, messageKey);
- }
-}
-```
-
-**选项 B:在 `report()` 方法中收集**
-
-优点:
-- 更早的收集点
-- 可以获取原始 messageKey 和 args
-
-缺点:
-- 需要传递更多参数
-- 可能收集到被抑制的消息
-
-#### 4. 初始化 SARIF 报告生成器
-
-在 `initChecker()` 中:
-```java
-public void initChecker() {
- // ... 现有代码 ...
-
- // 初始化 SARIF 报告生成器
- if (hasOption("sarifOutput")) {
- String outputPath = getOption("sarifOutput");
- if (outputPath == null) {
- throw new UserError("Must supply an argument to -AsarifOutput");
- }
- sarifReportGenerator = new SarifReportGenerator(processingEnv);
- }
-}
-```
-
-#### 5. 生成报告文件
-
-在 `typeProcessingOver()` 中:
-```java
-@Override
-public void typeProcessingOver() {
- for (SourceChecker checker : getSubcheckers()) {
- checker.typeProcessingOver();
- }
-
- // 生成 SARIF 报告(仅在根 checker)
- if (parentChecker == null && sarifReportGenerator != null) {
- String outputPath = getOption("sarifOutput");
- try {
- sarifReportGenerator.writeReport(Paths.get(outputPath));
- } catch (IOException e) {
- logBugInCF(new BugInCF("Failed to write SARIF report", e));
- }
- }
-
- super.typeProcessingOver();
-}
-```
-
-### SARIF 数据结构映射
-
-| Checker Framework | SARIF |
-|-----------------|-------|
-| `Diagnostic.Kind.ERROR` | `result.level: "error"` |
-| `Diagnostic.Kind.WARNING` | `result.level: "warning"` |
-| `messageKey` | `result.ruleId` |
-| `message` | `result.message.text` |
-| `Tree source` | `result.locations[0].physicalLocation` |
-| `SourceChecker` | `run.tool.driver.name` |
-| `CompilationUnitTree` | `run.artifacts[].location` |
-
-### 需要收集的信息
-
-1. **结果信息 (Result)**
- - 消息类型(ERROR/WARNING)
- - 消息文本
- - 消息键(messageKey)
- - 源代码位置(文件、行号、列号)
- - 发出消息的 checker
-
-2. **工具信息 (Tool)**
- - Checker Framework 版本
- - Checker 名称
- - 规则信息(从 messages.properties 提取)
-
-3. **文件信息 (Artifact)**
- - 文件 URI
- - 文件内容(可选,用于 SARIF viewer)
-
-## 实现细节讨论
-
-### 问题 1:消息键 (messageKey) 的获取
-
-**当前情况:**
-- `printOrStoreMessage()` 只接收格式化后的 `message` 字符串
-- 原始的 `messageKey` 在 `report()` 方法中
-
-**解决方案:**
-- 方案 A:修改 `printOrStoreMessage()` 签名,添加 `messageKey` 参数
-- 方案 B:在 `CheckerMessage` 中添加 `messageKey` 字段
-- 方案 C:从 `message` 中解析(不推荐,不可靠)
-
-**推荐:方案 B** - 修改 `CheckerMessage` 类,添加 `messageKey` 字段
-
-### 问题 2:子 checker 的消息收集
-
-**当前情况:**
-- Compound checker 有多个子 checker
-- 每个子 checker 共享 `messageStore`
-- 只有根 checker 的 `messageStore` 不为 null
-
-**解决方案:**
-- 所有 checker 共享同一个 `SarifReportGenerator` 实例
-- 在根 checker 初始化,传递给子 checker
-- 或者在根 checker 统一收集所有消息
-
-**推荐:** 在根 checker 初始化 `SarifReportGenerator`,子 checker 通过 `parentChecker` 访问
-
-### 问题 3:文件 URI 格式
-
-SARIF 要求使用 URI 格式的文件路径。
-
-**需要考虑:**
-- 相对路径 vs 绝对路径
-- Windows vs Unix 路径格式
-- 工作目录的处理
-
-**解决方案:**
-- 使用 `File.toURI()` 或 `Path.toUri()` 转换为 URI
-- 或者使用相对路径(相对于工作目录)
-
-### 问题 4:报告文件写入时机
-
-**选项 A:每个编译单元处理完后写入**
-- 优点:增量更新,可以看到实时进度
-- 缺点:多次文件 I/O,可能影响性能
-
-**选项 B:所有处理完成后一次性写入(推荐)**
-- 优点:性能好,文件一致
-- 缺点:如果崩溃,可能丢失数据
-
-**推荐:选项 B**,但可以考虑添加选项支持增量写入
-
-### 问题 5:与现有 `-Adetailedmsgtext` 选项的关系
-
-`-Adetailedmsgtext` 已经提供了可解析的输出格式。
-
-**关系:**
-- SARIF 是更标准化的格式
-- `-Adetailedmsgtext` 是自定义格式
-- 两者可以共存,服务于不同场景
-
-**建议:** 保持两者独立,用户可以选择使用哪种格式
-
-## 依赖管理
-
-### 需要添加的依赖
-
-在 `framework/build.gradle` 中添加:
-```gradle
-dependencies {
- implementation 'com.contrastsecurity:java-sarif:2.0'
- // Jackson 用于 JSON 序列化(java-sarif 依赖 Jackson)
- implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.0' // 或兼容版本
-}
-```
-
-**依赖确认:**
-- ✅ 使用 [java-sarif](https://github.com/Contrast-Security-OSS/java-sarif) 库
-- ✅ MIT License,与 Checker Framework 兼容
-- ✅ 版本 2.0,支持 SARIF 2.1.0 规范
-
-## 测试策略
-
-### 单元测试
-1. 测试 SARIF 报告生成器
-2. 测试消息收集逻辑
-3. 测试文件写入
-
-### 集成测试
-1. 使用真实 checker 生成报告
-2. 验证 SARIF 文件格式正确性
-3. 验证与现有功能的兼容性
-
-### 验证工具
-- 使用 SARIF 验证工具验证生成的报告
-- 使用 GitHub 或其他支持 SARIF 的工具查看报告
-
-## 后续扩展
-
-### 可能的扩展方向
-
-1. **抑制系统集成**
- - 从 SARIF 报告生成抑制文件
- - 从抑制文件过滤 SARIF 结果
-
-2. **增量报告**
- - 支持只报告新增问题
- - 支持问题追踪
-
-3. **报告格式扩展**
- - 支持其他格式(JSON、XML 等)
- - 支持自定义格式
-
-4. **构建系统集成**
- - Maven 插件支持
- - Gradle 插件支持
-
-## 问题解答
-
-### 1. SARIF JSON 需要什么信息?MessageKey 里面带着吗?
-
-SARIF JSON 的基本结构:
-
-```json
-{
- "version": "2.1.0",
- "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
- "runs": [{
- "tool": {
- "driver": {
- "name": "Checker Framework",
- "version": "3.51.2",
- "rules": [...]
- }
- },
- "artifacts": [...],
- "results": [{
- "ruleId": "assignment.type.incompatible", // ← messageKey 映射到这里
- "level": "error",
- "message": {
- "text": "incompatible types in assignment"
- },
- "locations": [{
- "physicalLocation": {
- "artifactLocation": {
- "uri": "file:///path/to/File.java"
- },
- "region": {
- "startLine": 10,
- "startColumn": 5
- }
- }
- }]
- }]
- }]
-}
-```
-
-**关键字段说明:**
-- `ruleId`: **messageKey 映射到这里**,用于标识触发该消息的规则
-- `level`: 消息级别(error/warning/note)
-- `message.text`: 格式化后的消息文本
-- `locations[].physicalLocation`: 源代码位置(文件 URI、行号、列号)
-- `artifacts[]`: 分析的文件列表(包含文件内容和 URI)
-
-### 2. 能否使用 java-sarif 库?
-
-**答案:可以!**
-
-根据 [java-sarif 库](https://github.com/Contrast-Security-OSS/java-sarif) 的信息:
-- **许可证**:MIT License(与 Checker Framework 兼容)
-- **版本**:2.0(最新版本)
-- **Maven 坐标**:
- ```xml
-
- com.contrastsecurity
- java-sarif
- 2.0
-
- ```
-- **特点**:
- - 使用 Jackson 进行 JSON 序列化/反序列化
- - 提供方法链式构建 API
- - 符合 SARIF 2.1.0 规范
-
-**使用示例:**
-```java
-import com.contrastsecurity.sarif.*;
-
-SarifLog sarifLog = new SarifLog()
- .withVersion("2.1.0")
- .withRuns(Arrays.asList(
- new Run()
- .withTool(new Tool()
- .withDriver(new ToolComponent()
- .withName("Checker Framework")
- .withVersion("3.51.2")))
- .withResults(Arrays.asList(
- new Result()
- .withRuleId("assignment.type.incompatible")
- .withLevel("error")
- .withMessage(new Message().withText("incompatible types"))
- ))
- ));
-```
-
-### 3. SARIF 生成完了怎么呈现?是写在一个文件里吗?
-
-**答案:是的,SARIF 是一个 JSON 文件。**
-
-**生成方式:**
-- 通过 `-AsarifOutput=path/to/report.sarif` 选项指定输出路径
-- 在 `typeProcessingOver()` 时一次性写入 JSON 文件
-- 文件格式:标准的 JSON,符合 SARIF 2.1.0 规范
-
-**呈现方式:**
-1. **GitHub Code Scanning**:上传 SARIF 文件到 GitHub,可以在 PR 中看到问题
-2. **VS Code SARIF Viewer**:使用 VS Code 插件查看
-3. **Azure DevOps**:集成到 CI/CD 流程
-4. **其他工具**:任何支持 SARIF 格式的工具都可以读取
-
-**文件示例:**
-```
-$ javac -processor NullnessChecker -AsarifOutput=report.sarif MyFile.java
-$ cat report.sarif
-{
- "version": "2.1.0",
- "runs": [...]
-}
-```
-
-### 4. 错误处理:SARIF 生成失败是否影响编译?
-
-**答案:不影响编译。**
-
-- SARIF 报告生成是**可选功能**,失败不应该影响编译
-- 如果生成失败,记录错误但不抛出异常
-- 可以输出警告信息,但编译继续进行
-
-### 5. 报告内容:是否包含源代码内容?
-
-**答案:需要包含。**
-
-原因:
-- 开发者需要源代码内容来定位问题
-- SARIF viewer 需要源代码来高亮显示问题位置
-- 便于离线查看报告
-
-实现方式:
-- 在 `artifacts[]` 中包含文件内容
-- 使用 `artifactLocation.uri` 指向文件
-- 使用 `artifact.contents.text` 存储源代码内容
-
-### 6. 初始实现:POC 版本,越简单越好
-
-**简化方案:**
-
-1. **最小实现范围**:
- - 只收集基本的错误和警告
- - 不处理 NOTE 类型的消息
- - 简化规则信息(暂时不解析 messages.properties)
-
-2. **简化数据结构**:
- - 只包含必需字段:ruleId, level, message, location
- - 文件内容可选(先实现基本版本)
-
-3. **分阶段实现**:
- - **Phase 1 (POC)**:基本消息收集和 SARIF 文件生成
- - **Phase 2**:完善规则信息、文件内容等
- - **Phase 3**:优化和扩展功能
-
-## 参考资源
-
-1. SARIF 规范:https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html
-2. Error Prone 讨论:https://github.com/google/error-prone/issues/3766
-3. Java SARIF 库:https://github.com/Contrast-Security-OSS/java-sarif
-4. GitHub SARIF 支持:https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning
-
-## POC 实现计划(简化版)
-
-### Phase 1: 基础实现(POC)
-
-**目标:** 能够生成基本的 SARIF 报告文件
-
-**任务清单:**
-1. ✅ 添加依赖:`com.contrastsecurity:java-sarif:2.0`
-2. 创建 `SarifReportGenerator` 类(简化版)
-3. 在 `SourceChecker` 中添加 `sarifReportGenerator` 字段
-4. 修改 `CheckerMessage` 添加 `messageKey` 字段
-5. 在 `printOrStoreMessage()` 中收集消息
-6. 在 `typeProcessingOver()` 中写入文件
-7. 添加 `-AsarifOutput` 选项
-
-**简化点:**
-- 只收集 ERROR 和 WARNING(忽略 NOTE)
-- 不解析 messages.properties(ruleId 直接用 messageKey)
-- 文件内容可选(先实现基本位置信息)
-- 不处理子 checker 的复杂情况(先支持单个 checker)
-
-### Phase 2: 完善功能
-
-- 支持所有消息类型
-- 完善规则信息(从 messages.properties 提取)
-- 支持 compound checker
-- 包含源代码内容
-
-### Phase 3: 优化和扩展
-
-- 性能优化
-- 增量报告
-- 抑制系统集成
-
-## 下一步行动
-
-1. ✅ 确认设计方案和依赖库
-2. 实现 POC 版本(Phase 1)
-3. 测试基本功能
-4. 验证 SARIF 文件格式
-5. 迭代完善(Phase 2 & 3)
-
diff --git a/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java b/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java
index 537eacb96fb2..4d7158247db1 100644
--- a/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java
+++ b/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java
@@ -3,7 +3,13 @@
import com.contrastsecurity.sarif.*;
import com.contrastsecurity.sarif.Result.Level;
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;
@@ -12,50 +18,161 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Properties;
import javax.annotation.processing.ProcessingEnvironment;
+import javax.tools.Diagnostic;
/**
- * Generates SARIF report files from checker diagnostics.
+ * Generates SARIF (Static Analysis Results Interchange Format) report files from checker
+ * diagnostics.
*
- * This is a POC implementation for Phase 1.
+ *
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 ProcessingEnvironment processingEnv;
private final List results = new ArrayList<>();
private final Map artifacts = new HashMap<>();
public SarifReportGenerator(ProcessingEnvironment processingEnv) {
- // this.processingEnv = processingEnv;
- System.out.println("SarifReportGenerator constructor");
+ 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 {
+ java.io.File file = new java.io.File(root.getSourceFile().getName());
+ return file.toURI().toString();
+ } catch (Exception e) {
+ return "file:///unknown";
+ }
+ }
+
+ /**
+ * 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);
+ }
+
+ 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);
+ }
+
+ /**
+ * Adds a source file artifact to the report.
+ *
+ * If the file content can be read, it is included in the artifact. Otherwise, only the file
+ * location is recorded.
+ *
+ * @param root the compilation unit
+ * @param fileUri the file URI
+ */
+ private void addArtifact(CompilationUnitTree root, String fileUri) {
+ Artifact artifact = new Artifact().withLocation(new ArtifactLocation().withUri(fileUri));
+ try {
+ String content = root.getSourceFile().getCharContent(true).toString();
+ artifact.withContents(new ArtifactContent().withText(content));
+ artifacts.put(fileUri, artifact);
+ } catch (Exception e) {
+ artifacts.put(fileUri, artifact);
+ }
}
/**
* Add a diagnostic result to the report.
*
+ *
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) {
- // TODO: For POC, just collect error and warning log
+ 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);
+ if (!artifacts.containsKey(fileUri)) {
+ addArtifact(root, fileUri);
+ }
+
+ 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));
-
+ .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() {
- // Phase 1: 简化版本,返回固定值
- return "3.51.2-SNAPSHOT";
+ 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";
}
/**
@@ -80,11 +197,9 @@ public void writeReport(String outputPath) throws IOException {
.withResults(results)
.withArtifacts(new HashSet<>(artifacts.values()))));
- // write into json file
+ // Write SARIF log to JSON file
ObjectMapper mapper = new ObjectMapper();
Path path = Paths.get(outputPath);
mapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), sarifLog);
- // results.clear();
- // artifacts.clear();
}
}
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 c8a32ba7ce4d..3b4079c4e624 100644
--- a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java
+++ b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java
@@ -1616,7 +1616,7 @@ protected void printOrStoreMessage(
// 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);
+ rootChecker.sarifReportGenerator.addResult(kind, message, messageKey, source, root);
}
}
diff --git a/test.sarif b/test.sarif
deleted file mode 100644
index a448ead93d1e..000000000000
--- a/test.sarif
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "version" : "2.1.0",
- "runs" : [ {
- "tool" : {
- "driver" : {
- "name" : "Checker Framework",
- "version" : "3.51.2-SNAPSHOT"
- }
- },
- "artifacts" : [ ],
- "results" : [ {
- "ruleId" : "assignment",
- "level" : "error",
- "message" : {
- "text" : "[assignment] incompatible types in assignment.\nfound : Set<@KeyFor(\"sharedCounts1\") String>\nrequired: Set<@KeyFor({\"sharedBooks\", \"sharedCounts1\"}) String>"
- }
- }, {
- "ruleId" : "assignment",
- "level" : "error",
- "message" : {
- "text" : "[assignment] incompatible types in assignment.\nfound : Set<@KeyFor(\"sharedCounts2\") String>\nrequired: Set<@KeyFor({\"sharedBooks\", \"sharedCounts2\"}) String>"
- }
- } ]
- } ]
-}
\ No newline at end of file
From ff6ad1e2dc2874608d4efa04283b0123e01120ef Mon Sep 17 00:00:00 2001
From: Daniel Gao <1803483451@qq.com>
Date: Sun, 7 Dec 2025 15:20:33 +1100
Subject: [PATCH 4/7] resolve comments from coderabbitai
---
.../report/SarifReportGenerator.java | 19 +++++++++++++++----
.../framework/source/SourceChecker.java | 1 +
2 files changed, 16 insertions(+), 4 deletions(-)
diff --git a/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java b/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java
index 4d7158247db1..8f81320dcf7c 100644
--- a/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java
+++ b/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java
@@ -1,7 +1,18 @@
package org.checkerframework.framework.report;
-import com.contrastsecurity.sarif.*;
+import com.contrastsecurity.sarif.Artifact;
+import com.contrastsecurity.sarif.ArtifactContent;
+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;
@@ -50,7 +61,7 @@ private String getFileUri(CompilationUnitTree root) {
try {
java.io.File file = new java.io.File(root.getSourceFile().getName());
return file.toURI().toString();
- } catch (Exception e) {
+ } catch (IllegalArgumentException | SecurityException e) {
return "file:///unknown";
}
}
@@ -71,7 +82,7 @@ private Region getRegion(Tree source, CompilationUnitTree root) {
long endPos = sourcePositions.getEndPosition(root, source);
if (startPos == Diagnostic.NOPOS || endPos == Diagnostic.NOPOS) {
- return new Region().withStartLine(1).withStartColumn(1);
+ return new Region().withStartLine(1).withStartColumn(1).withEndLine(1).withEndColumn(1);
}
LineMap lineMap = root.getLineMap();
@@ -102,7 +113,7 @@ private void addArtifact(CompilationUnitTree root, String fileUri) {
String content = root.getSourceFile().getCharContent(true).toString();
artifact.withContents(new ArtifactContent().withText(content));
artifacts.put(fileUri, artifact);
- } catch (Exception e) {
+ } catch (IOException e) {
artifacts.put(fileUri, artifact);
}
}
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 3b4079c4e624..0a3343b9ac62 100644
--- a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java
+++ b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java
@@ -1082,6 +1082,7 @@ public void typeProcessingOver() {
}
if (parentChecker == null && sarifReportGenerator != null) {
+ assert sarifOutputPath != null;
try {
sarifReportGenerator.writeReport(sarifOutputPath);
message(Diagnostic.Kind.NOTE, "SARIF report written to: " + sarifOutputPath);
From eb1a562287f62481cb3b7aea5dbc8051c875eac0 Mon Sep 17 00:00:00 2001
From: Daniel Gao <1803483451@qq.com>
Date: Sun, 7 Dec 2025 15:51:23 +1100
Subject: [PATCH 5/7] remove artifact code
---
.../report/SarifReportGenerator.java | 30 ++-----------------
1 file changed, 2 insertions(+), 28 deletions(-)
diff --git a/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java b/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java
index 8f81320dcf7c..8b10ef653417 100644
--- a/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java
+++ b/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java
@@ -45,7 +45,6 @@ public class SarifReportGenerator {
private final ProcessingEnvironment processingEnv;
private final List results = new ArrayList<>();
- private final Map artifacts = new HashMap<>();
public SarifReportGenerator(ProcessingEnvironment processingEnv) {
this.processingEnv = processingEnv;
@@ -59,8 +58,7 @@ public SarifReportGenerator(ProcessingEnvironment processingEnv) {
*/
private String getFileUri(CompilationUnitTree root) {
try {
- java.io.File file = new java.io.File(root.getSourceFile().getName());
- return file.toURI().toString();
+ return root.getSourceFile().toUri().toString();
} catch (IllegalArgumentException | SecurityException e) {
return "file:///unknown";
}
@@ -98,26 +96,6 @@ private Region getRegion(Tree source, CompilationUnitTree root) {
.withEndColumn((int) endCol);
}
- /**
- * Adds a source file artifact to the report.
- *
- * If the file content can be read, it is included in the artifact. Otherwise, only the file
- * location is recorded.
- *
- * @param root the compilation unit
- * @param fileUri the file URI
- */
- private void addArtifact(CompilationUnitTree root, String fileUri) {
- Artifact artifact = new Artifact().withLocation(new ArtifactLocation().withUri(fileUri));
- try {
- String content = root.getSourceFile().getCharContent(true).toString();
- artifact.withContents(new ArtifactContent().withText(content));
- artifacts.put(fileUri, artifact);
- } catch (IOException e) {
- artifacts.put(fileUri, artifact);
- }
- }
-
/**
* Add a diagnostic result to the report.
*
@@ -142,9 +120,6 @@ public void addResult(
return;
}
String fileUri = getFileUri(root);
- if (!artifacts.containsKey(fileUri)) {
- addArtifact(root, fileUri);
- }
Region region = getRegion(source, root);
@@ -205,8 +180,7 @@ public void writeReport(String outputPath) throws IOException {
new ToolComponent()
.withName("Checker Framework")
.withVersion(getCheckerVersion())))
- .withResults(results)
- .withArtifacts(new HashSet<>(artifacts.values()))));
+ .withResults(results)));
// Write SARIF log to JSON file
ObjectMapper mapper = new ObjectMapper();
From e26dc060f22cd8bc33d302f677602801f1aa35ef Mon Sep 17 00:00:00 2001
From: Daniel Gao <1803483451@qq.com>
Date: Sun, 7 Dec 2025 15:51:45 +1100
Subject: [PATCH 6/7] remove artifact code
---
.../framework/report/SarifReportGenerator.java | 5 -----
1 file changed, 5 deletions(-)
diff --git a/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java b/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java
index 8b10ef653417..1f6027580133 100644
--- a/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java
+++ b/framework/src/main/java/org/checkerframework/framework/report/SarifReportGenerator.java
@@ -1,7 +1,5 @@
package org.checkerframework.framework.report;
-import com.contrastsecurity.sarif.Artifact;
-import com.contrastsecurity.sarif.ArtifactContent;
import com.contrastsecurity.sarif.ArtifactLocation;
import com.contrastsecurity.sarif.Location;
import com.contrastsecurity.sarif.Message;
@@ -25,10 +23,7 @@
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
-import java.util.Map;
import java.util.Properties;
import javax.annotation.processing.ProcessingEnvironment;
import javax.tools.Diagnostic;
From 7c7f9a643c0b1666cf785a90d482c9b08daf5a72 Mon Sep 17 00:00:00 2001
From: Michael Ernst
Date: Tue, 3 Mar 2026 07:54:47 -0800
Subject: [PATCH 7/7] Fix Javadoc
---
.../org/checkerframework/framework/source/AggregateChecker.java | 2 +-
.../org/checkerframework/framework/source/SourceChecker.java | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
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 80dbf318d81d..ad7ffd4cc260 100644
--- a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java
+++ b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java
@@ -2053,7 +2053,7 @@ protected final void setLintOption(String name, boolean val) {
*
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