From f8ef624dcc600fb829b22d6068bb6cc1de02e18c Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Sat, 26 Sep 2026 09:43:24 +0100 Subject: [PATCH 1/2] Support TinyGo SARIF parsing and writing --- .github/workflows/ci.yml | 9 ++++ README.md | 2 + sarif.go | 69 +----------------------- sarif_test.go | 103 ----------------------------------- validate.go | 77 +++++++++++++++++++++++++++ validate_test.go | 112 +++++++++++++++++++++++++++++++++++++++ validate_tinygo.go | 11 ++++ validate_tinygo_test.go | 51 ++++++++++++++++++ 8 files changed, 263 insertions(+), 171 deletions(-) create mode 100644 validate.go create mode 100644 validate_test.go create mode 100644 validate_tinygo.go create mode 100644 validate_tinygo_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 818cdf5..eeeea44 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,15 @@ jobs: - name: Test run: go test -v -race ./... + - name: Test TinyGo validation fallback + run: go test -tags=tinygo ./... + + - name: Build WebAssembly + run: GOOS=js GOARCH=wasm go build ./... + + - name: Build WASI + run: GOOS=wasip1 GOARCH=wasm go build ./... + lint: runs-on: ubuntu-latest steps: diff --git a/README.md b/README.md index 4a18619..f0a14ce 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,8 @@ for _, run := range log.Runs { `Validate` checks a `*sarif.Log` against the bundled SARIF 2.1.0 JSON schema using `github.com/santhosh-tekuri/jsonschema/v6`. +Under TinyGo, parsing, writing, and generated defaults remain available. Schema validation is unsupported: `Validate` returns `errors.ErrUnsupported`, `Valid` returns false, and `Schema` is unavailable. + ```go if sarif.Valid(log) { // log is valid SARIF 2.1.0 diff --git a/sarif.go b/sarif.go index f9310ef..e8c42e3 100644 --- a/sarif.go +++ b/sarif.go @@ -1,27 +1,11 @@ package sarif import ( - "bytes" - "embed" "encoding/json" "fmt" "io" "os" "reflect" - "sync" - - "github.com/santhosh-tekuri/jsonschema/v6" -) - -//go:embed schema/sarif-schema-2.1.0.json -var schemaFS embed.FS - -const schemaPath = "schema/sarif-schema-2.1.0.json" - -var ( - compiledSchema *jsonschema.Schema - compiledSchemaErr error - compiledSchemaOnce sync.Once ) // Load reads a SARIF log from path. @@ -62,30 +46,8 @@ func Dump(log *Log, w io.Writer, pretty bool) error { return nil } -// Validate validates a SARIF log against the bundled SARIF 2.1.0 schema. -func Validate(log *Log) error { - schema, err := Schema() - if err != nil { - return err - } - - data, err := json.Marshal(log) - if err != nil { - return fmt.Errorf("validate sarif: %w", err) - } - - value, err := jsonschema.UnmarshalJSON(bytes.NewReader(data)) - if err != nil { - return fmt.Errorf("validate sarif: %w", err) - } - - if err := schema.Validate(value); err != nil { - return fmt.Errorf("validate sarif: %w", err) - } - return nil -} - // Valid reports whether log validates against the bundled SARIF 2.1.0 schema. +// It returns false under TinyGo, where validation is unsupported. func Valid(log *Log) bool { return Validate(log) == nil } @@ -114,32 +76,3 @@ func includeNonDefault(value any, defaultValue any) bool { } return !reflect.DeepEqual(value, defaultValue) } - -// Schema returns the compiled bundled SARIF 2.1.0 JSON schema. -func Schema() (*jsonschema.Schema, error) { - compiledSchemaOnce.Do(func() { - data, err := schemaFS.ReadFile(schemaPath) - if err != nil { - compiledSchemaErr = fmt.Errorf("compile sarif schema: %w", err) - return - } - - compiler := jsonschema.NewCompiler() - compiler.DefaultDraft(jsonschema.Draft7) - doc, err := jsonschema.UnmarshalJSON(bytes.NewReader(data)) - if err != nil { - compiledSchemaErr = fmt.Errorf("compile sarif schema: %w", err) - return - } - if err := compiler.AddResource(schemaPath, doc); err != nil { - compiledSchemaErr = fmt.Errorf("compile sarif schema: %w", err) - return - } - - compiledSchema, compiledSchemaErr = compiler.Compile(schemaPath) - if compiledSchemaErr != nil { - compiledSchemaErr = fmt.Errorf("compile sarif schema: %w", compiledSchemaErr) - } - }) - return compiledSchema, compiledSchemaErr -} diff --git a/sarif_test.go b/sarif_test.go index df5d1e4..fd79de7 100644 --- a/sarif_test.go +++ b/sarif_test.go @@ -9,17 +9,6 @@ import ( "testing" ) -func TestMinimalLogValidates(t *testing.T) { - log := &Log{ - Version: "2.1.0", - Runs: []Run{}, - } - - if err := Validate(log); err != nil { - t.Fatalf("Validate() error = %v", err) - } -} - func TestParseDumpRoundTrip(t *testing.T) { log, err := Parse([]byte(`{"version":"2.1.0","runs":[]}`)) if err != nil { @@ -150,95 +139,3 @@ func TestLoad(t *testing.T) { t.Fatalf("Version = %q, want 2.1.0", log.Version) } } - -func TestResultLogValidates(t *testing.T) { - artifactLocation := NewArtifactLocation() - artifactLocation.URI = "src/main.go" - region := NewRegion() - region.StartLine = 10 - region.StartColumn = 5 - location := NewLocation() - location.PhysicalLocation = PhysicalLocation{ - ArtifactLocation: artifactLocation, - Region: region, - } - result := NewResult() - result.RuleID = "no-unused-vars" - result.RuleIndex = 0 - result.Level = "warning" - result.Message = Message{Text: "Variable 'x' is unused"} - result.Locations = []Location{location} - defaultConfiguration := NewReportingConfiguration() - defaultConfiguration.Level = "error" - - log := &Log{ - Version: "2.1.0", - Runs: []Run{ - { - Tool: Tool{ - Driver: ToolComponent{ - Name: "test-linter", - Version: "1.0.0", - Rules: []ReportingDescriptor{ - { - ID: "no-unused-vars", - Name: "NoUnusedVars", - ShortDescription: MultiformatMessageString{ - Text: "Disallow unused variables", - }, - DefaultConfiguration: defaultConfiguration, - }, - }, - }, - }, - Results: []Result{result}, - }, - }, - } - - if err := Validate(log); err != nil { - t.Fatalf("Validate() error = %v", err) - } - - data, err := Marshal(log, false) - if err != nil { - t.Fatalf("Marshal() error = %v", err) - } - if !strings.Contains(string(data), `"ruleIndex":0`) { - t.Fatalf("Marshal() omitted meaningful zero ruleIndex: %s", data) - } - if strings.Contains(string(data), `null`) { - t.Fatalf("Marshal() emitted null default fields: %s", data) - } - for _, unexpected := range []string{`"enabled":`, `"rank":`, `"index":`} { - if strings.Contains(string(data), unexpected) { - t.Fatalf("Marshal() emitted unset schema-defaulted field %s: %s", unexpected, data) - } - } - var encoded struct { - Runs []struct { - Results []struct { - Locations []struct { - ID *int `json:"id"` - } `json:"locations"` - } `json:"results"` - } `json:"runs"` - } - if err := json.Unmarshal(data, &encoded); err != nil { - t.Fatalf("Unmarshal() error = %v", err) - } - if encoded.Runs[0].Results[0].Locations[0].ID != nil { - t.Fatalf("Marshal() emitted unset location id: %s", data) - } -} - -func TestValidateRejectsInvalidLog(t *testing.T) { - log := &Log{ - Version: "2.0.0", - Runs: []Run{}, - } - - if err := Validate(log); err == nil { - t.Fatal("Validate() error = nil, want invalid version error") - } -} diff --git a/validate.go b/validate.go new file mode 100644 index 0000000..2b1f4a3 --- /dev/null +++ b/validate.go @@ -0,0 +1,77 @@ +//go:build !tinygo + +package sarif + +import ( + "bytes" + "embed" + "encoding/json" + "fmt" + "sync" + + "github.com/santhosh-tekuri/jsonschema/v6" +) + +//go:embed schema/sarif-schema-2.1.0.json +var schemaFS embed.FS + +const schemaPath = "schema/sarif-schema-2.1.0.json" + +var ( + compiledSchema *jsonschema.Schema + compiledSchemaErr error + compiledSchemaOnce sync.Once +) + +// Validate validates a SARIF log against the bundled SARIF 2.1.0 schema. +func Validate(log *Log) error { + schema, err := Schema() + if err != nil { + return err + } + + data, err := json.Marshal(log) + if err != nil { + return fmt.Errorf("validate sarif: %w", err) + } + + value, err := jsonschema.UnmarshalJSON(bytes.NewReader(data)) + if err != nil { + return fmt.Errorf("validate sarif: %w", err) + } + + if err := schema.Validate(value); err != nil { + return fmt.Errorf("validate sarif: %w", err) + } + return nil +} + +// Schema returns the compiled bundled SARIF 2.1.0 JSON schema. +// It is unavailable under TinyGo. +func Schema() (*jsonschema.Schema, error) { + compiledSchemaOnce.Do(func() { + data, err := schemaFS.ReadFile(schemaPath) + if err != nil { + compiledSchemaErr = fmt.Errorf("compile sarif schema: %w", err) + return + } + + compiler := jsonschema.NewCompiler() + compiler.DefaultDraft(jsonschema.Draft7) + doc, err := jsonschema.UnmarshalJSON(bytes.NewReader(data)) + if err != nil { + compiledSchemaErr = fmt.Errorf("compile sarif schema: %w", err) + return + } + if err := compiler.AddResource(schemaPath, doc); err != nil { + compiledSchemaErr = fmt.Errorf("compile sarif schema: %w", err) + return + } + + compiledSchema, compiledSchemaErr = compiler.Compile(schemaPath) + if compiledSchemaErr != nil { + compiledSchemaErr = fmt.Errorf("compile sarif schema: %w", compiledSchemaErr) + } + }) + return compiledSchema, compiledSchemaErr +} diff --git a/validate_test.go b/validate_test.go new file mode 100644 index 0000000..333d3f9 --- /dev/null +++ b/validate_test.go @@ -0,0 +1,112 @@ +//go:build !tinygo + +package sarif + +import ( + "encoding/json" + "strings" + "testing" +) + +func TestMinimalLogValidates(t *testing.T) { + log := &Log{ + Version: "2.1.0", + Runs: []Run{}, + } + + if err := Validate(log); err != nil { + t.Fatalf("Validate() error = %v", err) + } +} + +func TestResultLogValidates(t *testing.T) { + artifactLocation := NewArtifactLocation() + artifactLocation.URI = "src/main.go" + region := NewRegion() + region.StartLine = 10 + region.StartColumn = 5 + location := NewLocation() + location.PhysicalLocation = PhysicalLocation{ + ArtifactLocation: artifactLocation, + Region: region, + } + result := NewResult() + result.RuleID = "no-unused-vars" + result.RuleIndex = 0 + result.Level = "warning" + result.Message = Message{Text: "Variable 'x' is unused"} + result.Locations = []Location{location} + defaultConfiguration := NewReportingConfiguration() + defaultConfiguration.Level = "error" + + log := &Log{ + Version: "2.1.0", + Runs: []Run{ + { + Tool: Tool{ + Driver: ToolComponent{ + Name: "test-linter", + Version: "1.0.0", + Rules: []ReportingDescriptor{ + { + ID: "no-unused-vars", + Name: "NoUnusedVars", + ShortDescription: MultiformatMessageString{ + Text: "Disallow unused variables", + }, + DefaultConfiguration: defaultConfiguration, + }, + }, + }, + }, + Results: []Result{result}, + }, + }, + } + + if err := Validate(log); err != nil { + t.Fatalf("Validate() error = %v", err) + } + + data, err := Marshal(log, false) + if err != nil { + t.Fatalf("Marshal() error = %v", err) + } + if !strings.Contains(string(data), `"ruleIndex":0`) { + t.Fatalf("Marshal() omitted meaningful zero ruleIndex: %s", data) + } + if strings.Contains(string(data), `null`) { + t.Fatalf("Marshal() emitted null default fields: %s", data) + } + for _, unexpected := range []string{`"enabled":`, `"rank":`, `"index":`} { + if strings.Contains(string(data), unexpected) { + t.Fatalf("Marshal() emitted unset schema-defaulted field %s: %s", unexpected, data) + } + } + var encoded struct { + Runs []struct { + Results []struct { + Locations []struct { + ID *int `json:"id"` + } `json:"locations"` + } `json:"results"` + } `json:"runs"` + } + if err := json.Unmarshal(data, &encoded); err != nil { + t.Fatalf("Unmarshal() error = %v", err) + } + if encoded.Runs[0].Results[0].Locations[0].ID != nil { + t.Fatalf("Marshal() emitted unset location id: %s", data) + } +} + +func TestValidateRejectsInvalidLog(t *testing.T) { + log := &Log{ + Version: "2.0.0", + Runs: []Run{}, + } + + if err := Validate(log); err == nil { + t.Fatal("Validate() error = nil, want invalid version error") + } +} diff --git a/validate_tinygo.go b/validate_tinygo.go new file mode 100644 index 0000000..3ecc53c --- /dev/null +++ b/validate_tinygo.go @@ -0,0 +1,11 @@ +//go:build tinygo + +package sarif + +import "errors" + +// Validate returns errors.ErrUnsupported because schema validation is +// unavailable under TinyGo. +func Validate(log *Log) error { + return errors.ErrUnsupported +} diff --git a/validate_tinygo_test.go b/validate_tinygo_test.go new file mode 100644 index 0000000..48e192e --- /dev/null +++ b/validate_tinygo_test.go @@ -0,0 +1,51 @@ +//go:build tinygo + +package sarif_test + +import ( + "bytes" + "errors" + "testing" + + "github.com/git-pkgs/sarif" +) + +func TestTinyGoValidationUnsupported(t *testing.T) { + input := []byte(`{"version":"2.1.0","runs":[{"tool":{"driver":{"name":"test-linter"}},"results":[{"ruleId":"unused-variable","message":{"text":"Variable x is unused"}}]}]}`) + log, err := sarif.Parse(input) + if err != nil { + t.Fatal(err) + } + if err := sarif.Validate(log); !errors.Is(err, errors.ErrUnsupported) { + t.Fatalf("Validate error = %v, want errors.ErrUnsupported", err) + } + if sarif.Valid(log) { + t.Fatal("Valid returned true without schema validation") + } + + for _, pretty := range []bool{false, true} { + var out bytes.Buffer + if err := sarif.Dump(log, &out, pretty); err != nil { + t.Fatal(err) + } + decoded, err := sarif.Parse(out.Bytes()) + if err != nil { + t.Fatal(err) + } + if decoded.Version != log.Version || len(decoded.Runs) != 1 || len(decoded.Runs[0].Results) != 1 { + t.Fatalf("round-trip changed the log: %s", out.Bytes()) + } + run := decoded.Runs[0] + result := run.Results[0] + if run.Tool.Driver.Name != "test-linter" || result.RuleID != "unused-variable" || result.Message.Text != "Variable x is unused" { + t.Fatalf("round-trip changed the finding: %s", out.Bytes()) + } + if result.Level != "warning" || result.Kind != "fail" || result.RuleIndex != -1 { + t.Fatalf("round-trip changed the defaults: %+v", result) + } + } + + if err := sarif.Validate(nil); !errors.Is(err, errors.ErrUnsupported) { + t.Fatalf("Validate(nil) error = %v, want errors.ErrUnsupported", err) + } +} From adca047775bdd1551fde96520a773bc85d0da1b2 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Sat, 26 Sep 2026 21:00:45 +0100 Subject: [PATCH 2/2] Run SARIF round-trip tests with TinyGo in CI --- .github/workflows/ci.yml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eeeea44..cad61a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,44 @@ jobs: - name: Build WASI run: GOOS=wasip1 GOARCH=wasm go build ./... + tinygo: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + + - name: Set up Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + package-manager-cache: false + + - name: Install TinyGo and Wasmtime + working-directory: ${{ runner.temp }} + run: | + curl --fail --location --silent --show-error --output tinygo.tar.gz https://github.com/tinygo-org/tinygo/releases/download/v0.42.0/tinygo0.42.0.linux-amd64.tar.gz + echo 'b87688fa2e19cee7d813cad7fd7dadb71dff3198e47125aba66ba4af5e490438 tinygo.tar.gz' | sha256sum --check + tar -xzf tinygo.tar.gz + echo "$RUNNER_TEMP/tinygo/bin" >> "$GITHUB_PATH" + curl --fail --location --silent --show-error --output wasmtime.tar.xz https://github.com/bytecodealliance/wasmtime/releases/download/v44.0.1/wasmtime-v44.0.1-x86_64-linux.tar.xz + echo 'afd58715f105e3a7f454169daed22168c5736ec5f225fb04c4ac62c54c9508a3 wasmtime.tar.xz' | sha256sum --check + tar -xJf wasmtime.tar.xz + echo "$RUNNER_TEMP/wasmtime-v44.0.1-x86_64-linux" >> "$GITHUB_PATH" + + - name: Test TinyGo WebAssembly SARIF round trip and validation fallback + run: tinygo test -target=wasm -run '^TestTinyGoValidationUnsupported$' -v . + + - name: Test TinyGo WASI SARIF round trip and validation fallback + run: tinygo test -target=wasip1 -run '^TestTinyGoValidationUnsupported$' -v . + lint: runs-on: ubuntu-latest steps: