feat(cmd): add SARIF output format - #820
Conversation
|
🔍 OpenCodeReview found 3 issue(s) in this PR.
[bug · high] 📄
|
| ArtifactLocation: sarifArtifactLocation{URI: c.Path}, | ||
| }, | ||
| } | ||
| if c.StartLine > 0 && c.EndLine > 0 { |
There was a problem hiding this comment.
[maintainability · low]
Defensive programming: The region is only emitted when both c.StartLine > 0 && c.EndLine > 0, but there's no validation that StartLine <= EndLine. While the upstream resolver typically produces valid ranges from diff hunks, if a comment bypasses the resolver or the LLM produces an inverted range, the SARIF output would violate the spec requirement that endLine >= startLine, potentially causing SARIF consumers to reject or misinterpret the result.
Consider adding a check: if c.StartLine > 0 && c.EndLine >= c.StartLine
| // Fixes are only emitted when both SuggestionCode and ExistingCode are | ||
| // non-empty. When line numbers are 0, deletedRegion is omitted (the fix | ||
| // becomes a pure insertion), per AC-8. | ||
| if c.SuggestionCode != "" && c.ExistingCode != "" { |
There was a problem hiding this comment.
[bug · medium]
Bug: The fixes block uses c.Path for the artifact location URI without checking whether it is empty. The location block above correctly guards on c.Path != "", but this fixes block does not. If a comment has non-empty SuggestionCode and ExistingCode but an empty Path, the emitted fix will contain "uri": "" in artifactLocation, which is structurally invalid for SARIF consumers and inconsistent with the location logic.
Consider guarding the fixes block with the same c.Path != "" check, or at minimum also requiring c.Path != "" in the condition.
Suggestion:
| if c.SuggestionCode != "" && c.ExistingCode != "" { | |
| if c.SuggestionCode != "" && c.ExistingCode != "" && c.Path != "" { |
c359938 to
905e6bc
Compare
lizhengfeng101
left a comment
There was a problem hiding this comment.
Thanks for taking this on — SARIF support is a genuinely useful addition, and I like that you kept it strictly at the output layer and mirrored the existing outputJSON / outputText shape. The category set lines up exactly with the constants in internal/tool/code_comment.go, and since normalizeCodeCommentCategory already collapses anything unknown to other, every ruleId is guaranteed to resolve to a declared rule. Nice detail.
That said, I validated the output shape against the very schema the PR declares in $schema (json.schemastore.org/sarif-2.1.0.json), and I think the file we currently emit won't be accepted by GitHub. Three things I'd call blocking:
1. --format sarif doesn't silence progress output, so the document gets corrupted
newQuietHandle in shared.go:272 still only quiets stdout for json or audience=agent, and all the [ocr] … progress lines go through stdout.Writer() (see internal/agent/agent.go:272,277,313). So with the default --audience human:
$ ocr review --format sarif > results.sarif
$ head -3 results.sarif
[ocr] 12 file(s) changed, reviewing 12 in /repo
[ocr] estimated cost: ...
{
That isn't even parseable JSON. The machineReadable abstraction you added is doing the right thing inside emitRunResult, it just needs to reach the place that actually decides silencing. I'd pull it out so both sites share it:
func isMachineReadable(outputFormat string) bool {
return outputFormat == "json" || outputFormat == "sarif"
}
func newQuietHandle(outputFormat, audience string) *quietHandle {
h := &quietHandle{}
if isMachineReadable(outputFormat) || audience == "agent" {
h.fn = stdout.Quiet()
}
return h
}The reason the suite stays green is that every test calls outputSARIF / emitRunResult directly and never goes through newQuietHandle (review_cmd.go:215). Worth a small TestNewQuietHandle_Sarif to lock it down.
2. result.location should be locations (an array)
In sarif.go:57 the field is declared as Location *sarifLocation with a json:"location,omitempty" tag. Checking the schema: definitions.result.properties has no location at all — only locations — and definitions.result sets additionalProperties: false. So this fails schema validation outright, and even where it's tolerated the alert ends up with no location and never lands on a line, which is the whole point of the feature.
type sarifResult struct {
RuleID string `json:"ruleId"`
Level string `json:"level"`
Message sarifMessage `json:"message"`
Locations []sarifLocation `json:"locations,omitempty"`
Fixes []sarifFix `json:"fixes,omitempty"`
}The result["location"] assertions in TestOutputSARIF_FullFieldMapping, _ZeroLineNumbers and _SchemaCompliance currently pin the wrong shape, so they'll need updating too.
3. replacement.deletedRegion is required, so the AC-8 "omit it" behaviour produces an invalid file
The schema has definitions.replacement.required == ["deletedRegion"]. In sarif.go:206-213, when line numbers are 0 or inverted we emit a replacement with only insertedContent. The important part: this doesn't degrade one finding — it invalidates the whole document, so every other alert in the run disappears with it.
Since there's no meaningful deletion range to construct when the lines are unknown anyway, I'd just not emit fixes in that case, which keeps the same rule you already apply to region:
hasRegion := c.StartLine > 0 && c.EndLine >= c.StartLine
if c.SuggestionCode != "" && c.ExistingCode != "" && c.Path != "" && hasRegion {
// deletedRegion is always present here
}TestOutputSARIF_ZeroLineNumbers and TestSarifResultFromComment_InvertedLineNumbers would flip to asserting fixes is absent.
Two things that speak to the stated goal rather than to validity:
partialFingerprints is what makes the alert lifecycle actually work. Without it GitHub fingerprints on ruleId + location + message, and our message.text is LLM prose that gets reworded on every run — so the same finding will keep getting closed and reopened, which undercuts the open → fixed → dismissed story the PR is built on. Something stable like sha256(Path + "|" + Category + "|" + normalize(ExistingCode)) under partialFingerprints: {"ocrFinding/v1": …} would fix it. I'd do this in this PR rather than as a follow-up.
Partial or failed runs will silently close real alerts. review_cmd.go:245 emits results whenever a manifest exists, including partial ones (budget exhausted, some files failed). A run that only covered 2 of 50 files produces a SARIF document indistinguishable from a clean full run, so GitHub marks everything it didn't see as fixed. Adding runs[].invocations[].executionSuccessful, derived from the manifest's terminal state, would prevent that. Same place you could surface ag.Warnings() as toolExecutionNotifications — right now warnings and ProjectSummary() are dropped entirely in the sarif path, and "file skipped, over token limit" is exactly the kind of thing a consumer needs to know.
Smaller stuff:
- The no-files branch in
shared.gois redundant for sarif: because!machineReadablealready skips the restore and the trace summary, falling through to the bottomoutputSARIFcall gives byte-identical output. You can drop the nestedifand leave the early return asoutputFormat == "json", then use!isMachineReadable(...)for the other three checks. sarifSeverityLevelfalls back towarning, whilenormalizeCodeCommentSeverityininternal/tool/code_comment.gofalls back tolow(→notehere). Empty severity should only reach you via session resume, but I'd either matchnoteor leave a comment explaining the deliberate bump.- Docs go stale immediately in two spots even though they're listed as follow-up:
pages/src/i18n/en.ts:168,224(plus zh/ja) still say "Output format: text or json", andaction.yml:279hard-codes--format json— which means the headline use case (uploading from Actions) can't be done through the official action yet. Worth calling out explicitly in the description. outputPreview(output.go:483) only knowsjson, so--preview --format sarifsilently prints the human view. Low priority, but mapping sarif→json there (or erroring) beats silence.
On the tests — the AC traceability is great, a few mechanical notes:
json.Unmarshalresults are discarded in_EmptyCategory,_ZeroLineNumbers,_NoFixesand_MultipleComments, so malformed output (issue 1) surfaces as a nil-map panic instead of a clear failure.TestEmitRunResult_Sarif*hand-rollos.Pipeinstead of reusingcaptureStdout(output_helpers_test.go:383); the hand-rolled version deadlocks if output exceeds the ~64KB pipe buffer.t.Run(tc.severity, …)gives the empty-string case an unnamed subtest; anamefield reads better.- The schema URL and
"OpenCodeReview"are re-typed as literals whensarifSchema/sarifToolNamealready exist. - The gap worth closing: everything currently asserts "field exists and has the right type", which is precisely why issues 2 and 3 pass. A single assertion that
locationsexists and that eachreplacementcarries adeletedRegionwould have caught both — and validating the document against a vendoredsarif-2.1.0.json(e.g. withsanthosh-tekuri/jsonschema) would catch the whole class. Even simpler as a one-off sanity check: rungithub/codeql-action/upload-sarifagainst a real file once and paste the result — all three blockers would show up immediately.
Happy to re-review as soon as the schema issues are sorted. The structure is right; it's the wire format that needs a pass.
905e6bc to
82f5edf
Compare
|
fixed |
|
Hey @Syt3s, nice work on this! The SARIF integration is clean and well-tested. I have a few things I'd like you to take a look at before we merge: 1.
|
82f5edf to
1555ed0
Compare
|
Hi @lizhengfeng101, thanks for the thorough review! |
|
Thanks for this — the SARIF modelling is clean and the comments explain the non-obvious constraints well (why Two things I'd like to see addressed before merge, both in 1.
|
1555ed0 to
21f16a7
Compare
|
@lizhengfeng101Thanks for the careful review — really appreciate you catching these two issues in sarifInvocationFromRun. Both are now fixed: executionSuccessful mapping — only StateFailed now maps to false. StateSkipped and StatePartial are true (publishable outcomes), and non-complete states carry the manifest message as a toolExecutionNotification so consumers can distinguish them from a clean run. Added a StateSkipped test case to pin this down. manifest == nil + subtask errors — subtask diagnostics are now only filtered when a manifest exists to freeze them into coverage.failed. Without a manifest they're the sole record of the failure and are kept as notifications. Updated TestSarifInvocation_WarningsAsNotifications to cover both paths. I also addressed the minor points: delegate validation stays at text|json, --preview --format sarif now errors explicitly, and duplicate fingerprints get an occurrence index appended. |
commit 140871d Author: Syt3s <lkxyout@gmail.com> Date: Wed Aug 12 18:03:34 2026 +0800 feat(cmd): add SARIF output format (alibaba#820) commit 552dc95 Author: Syt3s <lkxyout@gmail.com> Date: Wed Aug 12 16:44:35 2026 +0800 feat(cmd): add no-review cmd (alibaba#835) * feat(cmd): add no-review cmd * docs(flags): improve --no-filter help text for clarity --------- Co-authored-by: kite <lizhengfeng.lzf@alibaba-inc.com> commit 980f21d Author: kite <254839944+lizhengfeng101@users.noreply.github.com> Date: Wed Aug 12 15:58:23 2026 +0800 chore: remove leftover Chinese from the Go core, CI examples and pages comments (alibaba#861) * docs(comments): translate Chinese comments to English Rewrite the remaining Chinese code comments outside the VSCode extension in English, so the Go core and the pages site read consistently. - allowed_ext.go: translate the default_exclude_patterns.json package doc. Quote the wildcards ("*", "**", "{a,b,c}") so gofmt stops reflowing the leading "*" as a markdown bullet, which had swallowed the first entry's wildcard and broken the list alignment in godoc. - HighlightsSection.tsx: translate three comments in parseStatValue and CountUpValue. * docs(examples): use "Chinese" instead of "中文" in OCR_LANGUAGE examples The language config value is fed to the LLM, which understands "Chinese" just as well, and "Chinese" is what the rest of the project already uses (skills/open-code-review/SKILL.md, config_cmd_test.go, ApplyLanguage). Keeps the GitLab CI example's inline docs fully English. * fix(agent): drop the unreachable Chinese branch from planBlockPattern task_template.json ships a single English template ("### Review Plan (Optional)") and is embedded via go:embed with no override path, so the "审查计划" alternative could never match anything. It came from the pre-open-source template and survived the alibaba#33 fix as dead defensive code. Drops the two test cases that only exercised that alternative.
…t broke it (#38) `ocr delegate` gained a shared `-f, --format text|json` in v1.9.3 as a side effect of the SARIF work (alibaba/open-code-review#820). The skill still said in bold that the flag does not exist — true against v1.8.10, false against the installed v1.9.5. The CLI itself is already at the latest release, so nothing was upgraded on the binary side. Steps 1 and 2 instruct JSON again, but the text-parsing guide stays in the file as a named fallback branch rather than a silent retry: pinning this flag on v1.8.10 is what made every run through the skill exit on its first command and review nothing. The report has to say which path ran. All four local hardenings are kept, so this stays a fork of upstream's open-code-review-delegate rather than a re-vendor: the fallback, the (path, status) checklist identity, the coverage-rate denominator, and the Markdown-only-repo gotcha exist here and in no upstream release. `coverage_rate` is not a field OCR emits, so Step 6 still computes reviewed / reviewable_count. pr-review's probe becomes `ocr --version` rather than `which ocr`, since presence no longer determines which path the OCR track takes, and its degradation clause now excludes a rejected `--format` — the skill falls back on its own, so degrading there would discard a working review. Verified against the live CLI: preview and rule both emit schema_version "1", and `--format sarif` is rejected by delegate mode as documented. python3 scripts/validators/validate_all.py passes 3/3 (uv is absent on this box, so `make validate` could not be used). Co-authored-by: sonhyrd <sondh0127@gmail.com>
Description
本 PR 为
ocr review和ocr scan新增 SARIF v2.1.0(OASIS 标准)作为第三种输出格式,使审查结果可通过 GitHub Code Scanning 进行持久化告警追踪。背景
OpenCodeReview 当前支持
text和json两种输出格式。在 CI/CD 流水线中,JSON 输出被post-review-comments.js消费,以 PR 行内评论的形式发布审查发现。然而 PR 评论是对话式的——push 后即过时,没有持久化告警生命周期(open → fixed → dismissed),没有 Security tab 仪表盘,也无法作为分支保护门槛。OCR 内置了 SQL 注入、XSS、线程安全、NPE 检测等安全规则,但这些安全发现无法与 CodeQL、Semgrep、Bandit 等 SAST 工具一起呈现在 GitHub Security tab 中,因为这些工具输出 SARIF 而 OCR 不支持。
改动内容
新增
--format sarif选项,将LlmComment字段映射为 SARIF v2.1.0result对象:Pathlocations[].physicalLocation.artifactLocation.uriStartLine/EndLinelocations[].physicalLocation.region.startLine/endLineContentmessage.textCategoryruleId+tool.driver.rules[].idSeveritylevel(critical/high →error,medium →warning,low/空/未知 →note)SuggestionCodefixes[].artifactChanges[].replacements[].insertedContent.textExistingCodefixes[].artifactChanges[].replacements[].deletedRegion这是一个纯输出层变更——不涉及审查 Agent、LLM 循环、Diff 解析器或工具系统。实现完全遵循现有
outputJSON和outputText函数的相同模式。用法
改动文件
cmd/opencodereview/sarif.gooutputSARIF()函数 + severity/category 映射cmd/opencodereview/sarif_test.gocmd/opencodereview/shared.goemitRunResult新增sarif输出分支cmd/opencodereview/shared_flags.go--formatflag 描述和补全列表新增sarif未修改任何
internal/包。未引入新的第三方 Go 依赖(go.mod未变)。Type of Change
How Has This Been Tested?
单元测试
TestOutputSARIF_BasicStructureTestOutputSARIF_EmptyCommentsresults: [](非 null),rules 仍存在TestEmitRunResult_SarifNoFilesTestOutputSARIF_FullFieldMappingTestSarifSeverityLevelTestOutputSARIF_EmptyCategoryruleId: "other"TestOutputSARIF_ZeroLineNumbersTestOutputSARIF_NoFixesTestSarifRulesTestAddOutputFlags_IncludesSarif--formatflag 描述包含 sarifTestEmitRunResult_SarifemitRunResult以 sarif 格式输出合法 SARIF JSON 到 stdoutTestOutputSARIF_SchemaComplianceTestOutputSARIF_JSONFormattingTestOutputSARIF_MultipleCommentsTestNewQuietHandle_SarifTestSarifResultFromComment_EmptyPathTestSarifResultFromComment_InvertedLineNumbersTestSarifResultFromComment_FixesWithEmptyPathTestSarifFingerprints_StableTestSarifFingerprints_EmptyExistingCodeFallbackTestSarifInvocation_ExecutionSuccessfulTestSarifInvocation_WarningsAsNotificationsTestSarifInvocation_NonCompleteAddsManifestNotificationTestSarifResults_DuplicateFingerprintsTestOutputPreview_SarifRejects--preview --format sarif返回错误Checklist
go fmt,go vet)Related Issues
Fixes #819