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