buildifier: add SARIF output (--format=sarif) (#1529) Adds SARIF 2.1.0 as a third diagnostics format next to `text` and `json`. SARIF is understood by many CI, IDE and code-quality tools. Fixes #1528. ## Usage ``` buildifier --mode=check --lint=warn --format=sarif -r . > buildifier.sarif ``` As with `json`, `--format=sarif` only works with `--mode=check`, and `-v` indents the output. ## Mapping - Each lint warning becomes a result at level `warning`. Its `ruleId` is the warning category, `helpUri` is the WARNINGS.md link, and the region is the start and end line and column. - A file that needs reformatting becomes a file-level result (no region) with rule `format`, level `warning`. - A file that can't be parsed becomes a file-level result with rule `parse-error`, level `error`. - Rules are deduplicated per category. - With no findings, the output is still a valid log with empty `rules` and `results`. ## Design notes - The serializer is a small in-tree struct set, so it adds no dependency. - Findings carry no severity today (only `actionable`, which `warn.go` always sets to `true`). So every lint finding is a `warning`. - `autoFixable` is not mapped. It would need SARIF `fixes` with replacement text, which can come in a follow-up. - A `format` finding has no region, because buildifier doesn't track where the formatting differs. - `artifactLocation.uri` is the path as passed on the command line, so run from the repo root with relative paths for repo-relative URIs. ## Commits 1. Serializer (`utils/sarif.go`), unit tests, and the `Format` hook-up. 2. Accept `--format=sarif` in `--mode=check` and update the help text. 3. README section. 4. Integration tests with goldens, covering many files, a single file, stdin and an invalid file. ## Additional testing - Sample outputs validate against the official OASIS SARIF 2.1.0 schema. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
diff --git a/buildifier/README.md b/buildifier/README.md index 875058e..b3a3a7c 100644 --- a/buildifier/README.md +++ b/buildifier/README.md
@@ -235,6 +235,23 @@ failures or wrong input parameters, this means the output can be parsed as JSON, and its `success` field should be used to determine whether the diagnostics result is positive. +## File diagnostics in SARIF + +Buildifier can also emit its findings as a [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html) +report, triggered by `--format=sarif` (only works in combination with `--mode=check`). If used in +combination with `-v`, the output will be indented. + +* Every lint warning becomes a result whose `ruleId` is the warning category, with a region + (line and column) and level `warning`. +* A file that is not correctly formatted yields a file-level result (no region) with rule `format`. +* A file that cannot be parsed yields a result with level `error` and rule `parse-error`. + +Example: + +```bash +buildifier --mode=check --lint=warn --format=sarif -r . > buildifier.sarif +``` + ## Symlink Safety By default, Buildifier will not write to symlinks pointing outside of the Bazel workspace to prevent unintended path traversal.
diff --git a/buildifier/config/config.go b/buildifier/config/config.go index 7b90929..4efa191 100644 --- a/buildifier/config/config.go +++ b/buildifier/config/config.go
@@ -97,7 +97,7 @@ // repo (for REPO.bazel files), vendor (for VENDOR.bazel files), // or auto (default, based on the filename) InputType string `json:"type,omitempty"` - // Format sets the diagnostics format: text or json (default text) + // Format sets the diagnostics format: text, json or sarif (default text) Format string `json:"format,omitempty"` // Mode determines the formatting mode: check, diff, or fix (default fix) Mode string `json:"mode,omitempty"` @@ -179,7 +179,7 @@ flags.BoolVar(&c.Recursive, "r", c.Recursive, "find starlark files recursively") flags.BoolVar(&c.MultiDiff, "multi_diff", c.MultiDiff, "the command specified by the -diff_command flag can diff multiple files in the style of tkdiff (default false)") flags.StringVar(&c.Mode, "mode", c.Mode, "formatting mode: check, diff, or fix (default fix)") - flags.StringVar(&c.Format, "format", c.Format, "diagnostics format: text or json (default text)") + flags.StringVar(&c.Format, "format", c.Format, "diagnostics format: text, json or sarif (default text)") flags.StringVar(&c.DiffCommand, "diff_command", c.DiffCommand, "command to run when the formatting mode is diff (default uses the BUILDIFIER_DIFF, BUILDIFIER_MULTIDIFF, and DISPLAY environment variables to create the diff command)") flags.StringVar(&c.Lint, "lint", c.Lint, "lint mode: off, warn, or fix (default off)") flags.StringVar(&c.Warnings, "warnings", c.Warnings, "comma-separated warnings used in the lint mode or \"all\"")
diff --git a/buildifier/config/config_test.go b/buildifier/config/config_test.go index a9b0877..c736433 100644 --- a/buildifier/config/config_test.go +++ b/buildifier/config/config_test.go
@@ -160,7 +160,7 @@ // d: alias for -mode=diff ("false") // diff_command: command to run when the formatting mode is diff (default uses the BUILDIFIER_DIFF, BUILDIFIER_MULTIDIFF, and DISPLAY environment variables to create the diff command) ("") // disable_symlink_safety: per default Buildifier will not write to symlinks pointing outside of the Bazel workspace. Setting this to true will disable this behavior ("false") - // format: diagnostics format: text or json (default text) ("") + // format: diagnostics format: text, json or sarif (default text) ("") // help: print usage information ("false") // lint: lint mode: off, warn, or fix (default off) ("") // mode: formatting mode: check, diff, or fix (default fix) ("") @@ -256,8 +256,10 @@ "lint fix error": {options: "--lint=fix --mode=check", wantErr: fmt.Errorf("--lint=fix is only compatible with --mode=fix")}, "format mode error": {options: "--mode=fix --format=text", wantErr: fmt.Errorf("cannot specify --format without --mode=check")}, "format text": {options: "--mode=check --format=text"}, + "format sarif": {options: "--mode=check --format=sarif"}, + "format sarif error": {options: "--mode=fix --format=sarif", wantErr: fmt.Errorf("cannot specify --format without --mode=check")}, "format json": {options: "--mode=check --format=json"}, - "format error": {options: "--mode=check --format=foo", wantErr: fmt.Errorf("unrecognized format foo; valid types are text, json")}, + "format error": {options: "--mode=check --format=foo", wantErr: fmt.Errorf("unrecognized format foo; valid types are text, json, sarif")}, "type build": {options: "--type=build"}, "type bzl": {options: "--type=bzl"}, "type workspace": {options: "--type=workspace"},
diff --git a/buildifier/config/validation.go b/buildifier/config/validation.go index 509be21..eb3bdb7 100644 --- a/buildifier/config/validation.go +++ b/buildifier/config/validation.go
@@ -40,13 +40,13 @@ case "": return nil - case "text", "json": + case "text", "json", "sarif": if *mode != "check" { return fmt.Errorf("cannot specify --format without --mode=check") } default: - return fmt.Errorf("unrecognized format %s; valid types are text, json", *format) + return fmt.Errorf("unrecognized format %s; valid types are text, json, sarif", *format) } return nil }
diff --git a/buildifier/integration_test.sh b/buildifier/integration_test.sh index d4cc289..e7ae2e7 100755 --- a/buildifier/integration_test.sh +++ b/buildifier/integration_test.sh
@@ -507,7 +507,7 @@ test_lint "cfg" "--warnings=attr-cfg" "test_dir/fixed_golden_cfg.bzl" "$error_cfg" 0 test_lint "custom" "--warnings=-bzl-visibility,-integer-division,+unsorted-dict-items" "test_dir/fixed_golden_dict_cfg.bzl" "$error_docstring"$'\n'"$error_dict"$'\n'"$error_cfg" 1 -# Test --format=json +# Test --format=json and --format=sarif mkdir test_dir/json cp test_dir/to_fix.bzl test_dir/json @@ -658,6 +658,259 @@ } EOF +cat > golden/sarif_report_golden <<'EOF' +{ + "$schema": "https://json.schemastore.org/sarif-2.1.0.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "buildifier", + "informationUri": "https://github.com/bazel-contrib/buildtools/tree/main/buildifier", + "rules": [ + { + "id": "bzl-visibility", + "helpUri": "https://github.com/bazel-contrib/buildtools/blob/main/WARNINGS.md#bzl-visibility" + }, + { + "id": "integer-division", + "helpUri": "https://github.com/bazel-contrib/buildtools/blob/main/WARNINGS.md#integer-division" + }, + { + "id": "attr-cfg", + "helpUri": "https://github.com/bazel-contrib/buildtools/blob/main/WARNINGS.md#attr-cfg" + }, + { + "id": "format", + "shortDescription": { + "text": "The file is not formatted according to buildifier's rules." + } + } + ] + } + }, + "columnKind": "unicodeCodePoints", + "results": [ + { + "ruleId": "bzl-visibility", + "ruleIndex": 0, + "level": "warning", + "message": { + "text": "Module \"//foo/bar/internal/baz:module.bzl\" can only be loaded from files located inside \"//foo/bar\", not from \"//json/to_fix.bzl\"." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "to_fix.bzl" + }, + "region": { + "startLine": 1, + "startColumn": 6, + "endLine": 1, + "endColumn": 41 + } + } + } + ] + }, + { + "ruleId": "integer-division", + "ruleIndex": 1, + "level": "warning", + "message": { + "text": "The \"/\" operator for integer division is deprecated in favor of \"//\"." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "to_fix.bzl" + }, + "region": { + "startLine": 4, + "startColumn": 5, + "endLine": 4, + "endColumn": 10 + } + } + } + ] + }, + { + "ruleId": "attr-cfg", + "ruleIndex": 2, + "level": "warning", + "message": { + "text": "cfg = \"data\" for attr definitions has no effect and should be removed." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "to_fix.bzl" + }, + "region": { + "startLine": 6, + "startColumn": 15, + "endLine": 6, + "endColumn": 27 + } + } + } + ] + }, + { + "ruleId": "format", + "ruleIndex": 3, + "level": "warning", + "message": { + "text": "The file needs to be reformatted." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "to_fix_2.bzl" + } + } + } + ] + }, + { + "ruleId": "format", + "ruleIndex": 3, + "level": "warning", + "message": { + "text": "The file needs to be reformatted." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "to_fix_3.bzl" + } + } + } + ] + } + ] + } + ] +} +EOF + +cat > golden/sarif_report_small_golden <<'EOF' +{ + "$schema": "https://json.schemastore.org/sarif-2.1.0.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "buildifier", + "informationUri": "https://github.com/bazel-contrib/buildtools/tree/main/buildifier", + "rules": [] + } + }, + "columnKind": "unicodeCodePoints", + "results": [] + } + ] +} +EOF + +cat > golden/sarif_report_stdin_golden <<'EOF' +{ + "$schema": "https://json.schemastore.org/sarif-2.1.0.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "buildifier", + "informationUri": "https://github.com/bazel-contrib/buildtools/tree/main/buildifier", + "rules": [ + { + "id": "format", + "shortDescription": { + "text": "The file is not formatted according to buildifier's rules." + } + } + ] + } + }, + "columnKind": "unicodeCodePoints", + "results": [ + { + "ruleId": "format", + "ruleIndex": 0, + "level": "warning", + "message": { + "text": "The file needs to be reformatted." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "%3Cstdin%3E" + } + } + } + ] + } + ] + } + ] +} +EOF + +cat > golden/sarif_report_invalid_file_golden <<'EOF' +{ + "$schema": "https://json.schemastore.org/sarif-2.1.0.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "buildifier", + "informationUri": "https://github.com/bazel-contrib/buildtools/tree/main/buildifier", + "rules": [ + { + "id": "parse-error", + "shortDescription": { + "text": "The file is not a valid Starlark file." + } + } + ] + } + }, + "columnKind": "unicodeCodePoints", + "results": [ + { + "ruleId": "parse-error", + "ruleIndex": 0, + "level": "error", + "message": { + "text": "The file could not be parsed." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "foo.bar" + } + } + } + ] + } + ] + } + ] +} +EOF + cd test_dir/json $buildifier --mode=check --format=json --lint=warn --warnings=-module-docstring -v to_fix.bzl to_fix_2.bzl to_fix_3.bzl to_fix_4.bzl > json_report @@ -672,6 +925,18 @@ $buildifier --mode=check --format=json --lint=warn --warnings=-module-docstring -v to_fix_4.bzl foo.bar > json_report diff -u json_report ../../golden/json_report_invalid_file_golden || die "$1: wrong console output for --mode=check --format=json --lint=warn with an invalid file" +$buildifier --mode=check --format=sarif --lint=warn --warnings=-module-docstring -v to_fix.bzl to_fix_2.bzl to_fix_3.bzl to_fix_4.bzl > sarif_report +diff -u sarif_report ../../golden/sarif_report_golden || die "$1: wrong console output for --mode=check --format=sarif --lint=warn with many files" + +$buildifier --mode=check --format=sarif --lint=warn --warnings=-module-docstring -v to_fix_4.bzl > sarif_report +diff -u sarif_report ../../golden/sarif_report_small_golden || die "$1: wrong console output for --mode=check --format=sarif --lint=warn with a single file" + +$buildifier --mode=check --format=sarif --lint=warn --warnings=-module-docstring -v < to_fix_2.bzl > sarif_report +diff -u sarif_report ../../golden/sarif_report_stdin_golden || die "$1: wrong console output for --mode=check --format=sarif --lint=warn with stdin" + +$buildifier --mode=check --format=sarif --lint=warn --warnings=-module-docstring -v to_fix_4.bzl foo.bar > sarif_report +diff -u sarif_report ../../golden/sarif_report_invalid_file_golden || die "$1: wrong console output for --mode=check --format=sarif --lint=warn with an invalid file" + cd ../.. # Test the multifile functionality
diff --git a/buildifier/utils/BUILD.bazel b/buildifier/utils/BUILD.bazel index ec6aab9..c406ec6 100644 --- a/buildifier/utils/BUILD.bazel +++ b/buildifier/utils/BUILD.bazel
@@ -4,6 +4,7 @@ name = "utils", srcs = [ "diagnostics.go", + "sarif.go", "tempfile.go", "utils.go", ], @@ -17,7 +18,10 @@ go_test( name = "utils_test", - srcs = ["utils_test.go"], + srcs = [ + "sarif_test.go", + "utils_test.go", + ], embed = [":utils"], )
diff --git a/buildifier/utils/diagnostics.go b/buildifier/utils/diagnostics.go index 8de8534..412bd45 100644 --- a/buildifier/utils/diagnostics.go +++ b/buildifier/utils/diagnostics.go
@@ -31,7 +31,7 @@ Files []*FileDiagnostics `json:"files"` // diagnostics per file } -// Format formats a Diagnostics object either as plain text or as json +// Format formats a Diagnostics object either as plain text, json or sarif func (d *Diagnostics) Format(format string, verbose bool) string { switch format { case "text", "": @@ -62,6 +62,8 @@ result, _ = json.Marshal(*d) } return string(result) + "\n" + case "sarif": + return d.formatSarif(verbose) } return "" }
diff --git a/buildifier/utils/sarif.go b/buildifier/utils/sarif.go new file mode 100644 index 0000000..d880341 --- /dev/null +++ b/buildifier/utils/sarif.go
@@ -0,0 +1,208 @@ +/* +Copyright 2026 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package utils + +import ( + "encoding/json" + "net/url" + "path/filepath" + "strings" +) + +const ( + sarifSchema = "https://json.schemastore.org/sarif-2.1.0.json" + sarifVersion = "2.1.0" + sarifToolURI = "https://github.com/bazel-contrib/buildtools/tree/main/buildifier" + sarifLevelWarning = "warning" + sarifLevelError = "error" + // Columns are counted in runes, see build.Position.LineRune. + sarifColumnKind = "unicodeCodePoints" + sarifFormatRule = "format" + sarifParseRule = "parse-error" +) + +// Minimal subset of SARIF 2.1.0 needed to report buildifier diagnostics. + +type sarifLog struct { + Schema string `json:"$schema"` + Version string `json:"version"` + Runs []sarifRun `json:"runs"` +} + +type sarifRun struct { + Tool sarifTool `json:"tool"` + ColumnKind string `json:"columnKind"` + Results []sarifResult `json:"results"` +} + +type sarifTool struct { + Driver sarifDriver `json:"driver"` +} + +type sarifDriver struct { + Name string `json:"name"` + InformationURI string `json:"informationUri"` + Rules []sarifRule `json:"rules"` +} + +type sarifRule struct { + ID string `json:"id"` + ShortDescription *sarifMessage `json:"shortDescription,omitempty"` + HelpURI string `json:"helpUri,omitempty"` +} + +type sarifResult struct { + RuleID string `json:"ruleId"` + RuleIndex int `json:"ruleIndex"` + Level string `json:"level"` + Message sarifMessage `json:"message"` + Locations []sarifLocation `json:"locations"` +} + +type sarifMessage struct { + Text string `json:"text"` +} + +type sarifLocation struct { + PhysicalLocation sarifPhysicalLocation `json:"physicalLocation"` +} + +type sarifPhysicalLocation struct { + ArtifactLocation sarifArtifactLocation `json:"artifactLocation"` + Region *sarifRegion `json:"region,omitempty"` +} + +type sarifArtifactLocation struct { + URI string `json:"uri"` +} + +type sarifRegion struct { + StartLine int `json:"startLine"` + StartColumn int `json:"startColumn,omitempty"` + EndLine int `json:"endLine,omitempty"` + EndColumn int `json:"endColumn,omitempty"` +} + +// sarifBuilder collects rules and results, deduplicating rules by id. +type sarifBuilder struct { + rules []sarifRule + indexes map[string]int + results []sarifResult +} + +func (b *sarifBuilder) ruleIndex(id, description, helpURI string) int { + if i, ok := b.indexes[id]; ok { + return i + } + rule := sarifRule{ID: id, HelpURI: helpURI} + if description != "" { + rule.ShortDescription = &sarifMessage{Text: description} + } + b.rules = append(b.rules, rule) + b.indexes[id] = len(b.rules) - 1 + return b.indexes[id] +} + +func (b *sarifBuilder) add(ruleID, description, helpURI, level, message, filename string, region *sarifRegion) { + b.results = append(b.results, sarifResult{ + RuleID: ruleID, + RuleIndex: b.ruleIndex(ruleID, description, helpURI), + Level: level, + Message: sarifMessage{Text: escapeSarifBraces(message)}, + Locations: []sarifLocation{{ + PhysicalLocation: sarifPhysicalLocation{ + ArtifactLocation: sarifArtifactLocation{URI: sarifURI(filename)}, + Region: region, + }, + }}, + }) +} + +// escapeSarifBraces escapes the placeholder delimiters in message strings (SARIF 3.11.5). +var escapeSarifBraces = strings.NewReplacer("{", "{{", "}", "}}").Replace + +// sarifURI converts a file name into a URI reference as required by SARIF 3.4.3: +// special characters are percent-encoded, absolute paths become file URIs and +// relative paths are relative-path references. +func sarifURI(filename string) string { + path := filepath.ToSlash(filename) + if filepath.IsAbs(filename) { + if !strings.HasPrefix(path, "/") { + path = "/" + path // Windows drive letter, e.g. C:/dir + } + return (&url.URL{Scheme: "file", Path: path}).String() + } + return (&url.URL{Path: path}).String() +} + +// formatSarif renders the diagnostics as a SARIF 2.1.0 log. +func (d *Diagnostics) formatSarif(verbose bool) string { + b := &sarifBuilder{indexes: map[string]int{}} + + for _, f := range d.Files { + for _, w := range f.Warnings { + // Without a start line the result is reported at file level. + var region *sarifRegion + if w.Start.Line > 0 { + region = &sarifRegion{ + StartLine: w.Start.Line, + StartColumn: w.Start.Column, + EndLine: w.End.Line, + EndColumn: w.End.Column, + } + } + b.add(w.Category, "", w.URL, sarifLevelWarning, w.Message, f.Filename, region) + } + switch { + case !f.Valid: + b.add(sarifParseRule, "The file is not a valid Starlark file.", "", sarifLevelError, + "The file could not be parsed.", f.Filename, nil) + case !f.Formatted: + b.add(sarifFormatRule, "The file is not formatted according to buildifier's rules.", "", sarifLevelWarning, + "The file needs to be reformatted.", f.Filename, nil) + } + } + + if b.rules == nil { + b.rules = []sarifRule{} + } + if b.results == nil { + b.results = []sarifResult{} + } + + log := sarifLog{ + Schema: sarifSchema, + Version: sarifVersion, + Runs: []sarifRun{{ + Tool: sarifTool{Driver: sarifDriver{ + Name: "buildifier", + InformationURI: sarifToolURI, + Rules: b.rules, + }}, + ColumnKind: sarifColumnKind, + Results: b.results, + }}, + } + + var out []byte + if verbose { + out, _ = json.MarshalIndent(log, "", " ") + } else { + out, _ = json.Marshal(log) + } + return string(out) + "\n" +}
diff --git a/buildifier/utils/sarif_test.go b/buildifier/utils/sarif_test.go new file mode 100644 index 0000000..16d2bc0 --- /dev/null +++ b/buildifier/utils/sarif_test.go
@@ -0,0 +1,115 @@ +/* +Copyright 2026 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package utils + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/bazel-contrib/buildtools/v10/build" + "github.com/bazel-contrib/buildtools/v10/warn" +) + +func TestFormatSarif(t *testing.T) { + finding := &warn.Finding{ + Start: build.Position{Line: 3, LineRune: 5}, + End: build.Position{Line: 3, LineRune: 10}, + Category: "integer-division", + Message: "msg", + URL: "https://example.com#integer-division", + } + good := NewFileDiagnostics("a/good.bzl", nil) + warned := NewFileDiagnostics("a/warn.bzl", []*warn.Finding{finding, finding}) + unformatted := NewFileDiagnostics("a/fmt.bzl", nil) + unformatted.Formatted = false + invalid := InvalidFileDiagnostics("a/bad.bzl") + + out := NewDiagnostics(good, warned, unformatted, invalid).Format("sarif", false) + var log sarifLog + if err := json.Unmarshal([]byte(out), &log); err != nil { + t.Fatalf("invalid json: %v", err) + } + if log.Version != "2.1.0" || len(log.Runs) != 1 { + t.Fatalf("unexpected log header: %+v", log) + } + run := log.Runs[0] + if len(run.Results) != 4 { + t.Fatalf("got %d results, want 4", len(run.Results)) + } + if len(run.Tool.Driver.Rules) != 3 { + t.Errorf("got %d rules, want 3 (deduplicated)", len(run.Tool.Driver.Rules)) + } + r := run.Results[0] + reg := r.Locations[0].PhysicalLocation.Region + if r.RuleID != "integer-division" || r.Level != "warning" || reg == nil || + reg.StartLine != 3 || reg.StartColumn != 5 || reg.EndColumn != 10 { + t.Errorf("unexpected warning result: %+v", r) + } + if run.Results[2].RuleID != "format" || run.Results[2].Locations[0].PhysicalLocation.Region != nil { + t.Errorf("unexpected format result: %+v", run.Results[2]) + } + if run.Results[3].RuleID != "parse-error" || run.Results[3].Level != "error" { + t.Errorf("unexpected parse result: %+v", run.Results[3]) + } + for i, res := range run.Results { + if run.Tool.Driver.Rules[res.RuleIndex].ID != res.RuleID { + t.Errorf("result %d has inconsistent ruleIndex", i) + } + } +} + +func TestFormatSarifEmpty(t *testing.T) { + out := NewDiagnostics(NewFileDiagnostics("a.bzl", nil)).Format("sarif", true) + if !strings.Contains(out, `"results": []`) || !strings.Contains(out, `"rules": []`) { + t.Errorf("expected empty arrays, got:\n%s", out) + } +} + +func TestSarifURI(t *testing.T) { + for in, want := range map[string]string{ + "a/b.bzl": "a/b.bzl", + "a b/c.bzl": "a%20b/c.bzl", + "<stdin>": "%3Cstdin%3E", + "/tmp/x.bzl": "file:///tmp/x.bzl", + "a:b/c.bzl": "./a:b/c.bzl", + } { + if got := sarifURI(in); got != want { + t.Errorf("sarifURI(%q) = %q, want %q", in, got, want) + } + } +} + +func TestFormatSarifEscapesBracesAndSetsColumnKind(t *testing.T) { + finding := &warn.Finding{ + Start: build.Position{Line: 1, LineRune: 1}, + End: build.Position{Line: 1, LineRune: 2}, + Category: "c", + Message: `use {"a": 1}`, + } + out := NewDiagnostics(NewFileDiagnostics("a.bzl", []*warn.Finding{finding})).Format("sarif", false) + var log sarifLog + if err := json.Unmarshal([]byte(out), &log); err != nil { + t.Fatal(err) + } + if got, want := log.Runs[0].Results[0].Message.Text, `use {{"a": 1}}`; got != want { + t.Errorf("message = %q, want %q", got, want) + } + if log.Runs[0].ColumnKind != "unicodeCodePoints" { + t.Errorf("columnKind = %q", log.Runs[0].ColumnKind) + } +}