From 814ada130aaf247358901fee1db699073c49997e Mon Sep 17 00:00:00 2001 From: nojaf Date: Mon, 4 Aug 2025 17:21:00 +0200 Subject: [PATCH 1/6] Merge sarif files --- .github/workflows/main.yml | 2 +- build.fsx | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1aafc208f8..28a28247c2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,7 +28,7 @@ jobs: if: matrix.os == 'ubuntu-latest' && github.ref == 'refs/heads/main' run: "curl -H 'Accept: application/vnd.github.everest-preview+json' -H 'Authorization: token ${{secrets.FANTOMAS_TOOLS_TOKEN}}' --request POST --data '{\"event_type\": \"fantomas-commit-on-main\"}' https://api.github.com/repos/fsprojects/fantomas-tools/dispatches" - name: "Run analyzers" - run: dotnet msbuild /t:AnalyzeSolution + run: dotnet fsi build.fsx -- -p Analyze continue-on-error: true if: matrix.os == 'ubuntu-latest' - name: Upload SARIF file diff --git a/build.fsx b/build.fsx index 63c8458d3a..4a159bf9f9 100644 --- a/build.fsx +++ b/build.fsx @@ -3,6 +3,7 @@ #r "nuget: FSharp.Data, 6.3.0" #r "nuget: Ionide.KeepAChangelog, 0.1.8" #r "nuget: Humanizer.Core, 2.14.1" +#load "./sarif.fsx" open System open System.IO @@ -499,4 +500,11 @@ pipeline "PublishAlpha" { runIfOnlySpecified true } +pipeline "Analyze" { + workingDir __SOURCE_DIRECTORY__ + stage "Analyze" { run "dotnet msbuild /t:AnalyzeSolution" } + stage "Merge" { run Sarif.mergeSarifFiles } + runIfOnlySpecified true +} + tryPrintPipelineCommandHelp () From 2dd9fe95651fcbe0f4a30dc93659781df4d0caf9 Mon Sep 17 00:00:00 2001 From: nojaf Date: Mon, 4 Aug 2025 17:24:05 +0200 Subject: [PATCH 2/6] Yes, add the sarif file Precious --- sarif.fsx | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 sarif.fsx diff --git a/sarif.fsx b/sarif.fsx new file mode 100644 index 0000000000..5930598543 --- /dev/null +++ b/sarif.fsx @@ -0,0 +1,176 @@ +module Sarif + +open System +open System.IO +open System.Text.Json +open System.Text.Json.Serialization +open System.Threading.Tasks + +[] +type Text = + { [] + text: string } + +[] +type Region = + { [] + startLine: int + [] + startColumn: int + [] + endLine: int + [] + endColumn: int } + +[] +type ArtifactLocation = + { [] + uri: string } + +[] +type PhysicalLocation = + { [] + artifactLocation: ArtifactLocation + [] + region: Region } + +[] +type Location = + { [] + physicalLocation: PhysicalLocation } + +[] +type Message = + { [] + text: string } + +[] +type Result = + { [] + ruleId: string + [] + ruleIndex: int + [] + message: Message + [] + locations: Location list } + +[] +type RuleShortDescription = + { [] + text: string + [] + markdown: string } + +[] +type Rule = + { [] + id: string + [] + name: string + [] + shortDescription: RuleShortDescription + [] + helpUri: string } + +[] +type Driver = + { [] + name: string + [] + version: string + [] + informationUri: string + [] + rules: Rule list } + +[] +type Tool = + { [] + driver: Driver } + +[] +type Invocation = + { [] + startTimeUtc: DateTime + [] + endTimeUtc: DateTime + [] + executionSuccessful: bool } + +[] +type Run = + { [] + results: Result list + [] + tool: Tool + [] + invocations: Invocation list + [] + columnKind: string } + +[] +type SarifLog = + { + // This field needs JsonPropertyName because F# doesn't allow '$' in identifiers. + [] + schema: string + [] + version: string + [] + runs: Run list } + +let private options = JsonSerializerOptions() + +let private readSarif (json: System.IO.Stream) : System.Threading.Tasks.ValueTask = + JsonSerializer.DeserializeAsync(json, options) + +let private writeSarif (json: System.IO.Stream) (sarifLog: SarifLog) : Threading.Tasks.Task = + JsonSerializer.SerializeAsync(json, sarifLog, options) + +let mergeSarifFiles _ = + task { + let! sarifFiles = + Directory.GetFiles("analysisreports", "*.sarif") + |> Seq.map (fun path -> + task { + let sarifContent = File.OpenRead(path) + let! sarif = readSarif sarifContent + return path, sarif + }) + |> Task.WhenAll + + if Array.isEmpty sarifFiles then + printfn "No sarif files could be merged" + else + let firstSarif = snd (sarifFiles.[0]) + let firstRun = firstSarif.runs.[0] + + let results = + sarifFiles + |> Array.fold + (fun acc (_, sarif: SarifLog) -> + sarif.runs + |> List.collect (fun (r: Run) -> r.results) + |> List.toArray + |> Array.append acc) + [||] + |> List.ofArray + + let combined: SarifLog = + { + // I don't know why firstSarif.schema is null + schema = "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.6.json" + version = firstSarif.version + runs = + [ { tool = firstRun.tool + invocations = firstRun.invocations + columnKind = firstRun.columnKind + results = results } ] } + + sarifFiles |> Array.iter (fun (path, _) -> File.Delete(path)) + + let mergedStream = File.OpenWrite("analysisreports/merged.sarif") + do! writeSarif mergedStream combined + do! mergedStream.FlushAsync() + } From 0b3f8f0f674bf7b77aa4c68e94e6d2ab6e06da84 Mon Sep 17 00:00:00 2001 From: nojaf Date: Mon, 4 Aug 2025 17:56:48 +0200 Subject: [PATCH 3/6] Try FSharp.SystemTextJson --- sarif.fsx | 47 ++++++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/sarif.fsx b/sarif.fsx index 5930598543..4516916bcc 100644 --- a/sarif.fsx +++ b/sarif.fsx @@ -1,4 +1,4 @@ -module Sarif +#r "nuget: FSharp.SystemTextJson, 1.4.36" open System open System.IO @@ -82,7 +82,7 @@ type Driver = [] informationUri: string [] - rules: Rule list } + rules: Rule list option } [] type Tool = @@ -120,12 +120,14 @@ type SarifLog = [] runs: Run list } -let private options = JsonSerializerOptions() +let private options = + JsonFSharpOptions.Default() + .ToJsonSerializerOptions() -let private readSarif (json: System.IO.Stream) : System.Threading.Tasks.ValueTask = +let private readSarif (json: Stream) : System.Threading.Tasks.ValueTask = JsonSerializer.DeserializeAsync(json, options) -let private writeSarif (json: System.IO.Stream) (sarifLog: SarifLog) : Threading.Tasks.Task = +let private writeSarif (json: Stream) (sarifLog: SarifLog) : Task = JsonSerializer.SerializeAsync(json, sarifLog, options) let mergeSarifFiles _ = @@ -134,7 +136,7 @@ let mergeSarifFiles _ = Directory.GetFiles("analysisreports", "*.sarif") |> Seq.map (fun path -> task { - let sarifContent = File.OpenRead(path) + use sarifContent = File.OpenRead(path) let! sarif = readSarif sarifContent return path, sarif }) @@ -146,16 +148,17 @@ let mergeSarifFiles _ = let firstSarif = snd (sarifFiles.[0]) let firstRun = firstSarif.runs.[0] - let results = - sarifFiles - |> Array.fold - (fun acc (_, sarif: SarifLog) -> - sarif.runs - |> List.collect (fun (r: Run) -> r.results) - |> List.toArray - |> Array.append acc) - [||] - |> List.ofArray + let results = ResizeArray() + let rules = ResizeArray() + + for _, sarif in sarifFiles do + for run in sarif.runs do + results.AddRange(run.results) + + match run.tool.driver.rules with + | None -> () + | Some rulesList -> rules.AddRange(rulesList) + let combined: SarifLog = { @@ -163,14 +166,20 @@ let mergeSarifFiles _ = schema = "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.6.json" version = firstSarif.version runs = - [ { tool = firstRun.tool + [ { tool = + { firstRun.tool with + driver = + { firstRun.tool.driver with + rules = Some(List.ofSeq rules) } } invocations = firstRun.invocations columnKind = firstRun.columnKind - results = results } ] } + results = List.ofSeq results } ] } sarifFiles |> Array.iter (fun (path, _) -> File.Delete(path)) let mergedStream = File.OpenWrite("analysisreports/merged.sarif") do! writeSarif mergedStream combined do! mergedStream.FlushAsync() - } + mergedStream.Close() + printfn "Successfully merged %d SARIF files" sarifFiles.Length + } \ No newline at end of file From dee1fcf891f6f675151fc07a7920a7d8f873efd1 Mon Sep 17 00:00:00 2001 From: nojaf Date: Tue, 5 Aug 2025 10:07:16 +0200 Subject: [PATCH 4/6] Try with Thoth.Json --- .gitignore | 5 +- sarif.fsx | 285 +++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 202 insertions(+), 88 deletions(-) diff --git a/.gitignore b/.gitignore index 7b2775560e..86be744b4e 100644 --- a/.gitignore +++ b/.gitignore @@ -199,4 +199,7 @@ tests/.repositories/** # Analyzer files .analyzerpackages -*.sarif \ No newline at end of file +*.sarif + +# vscode history plugin +.history/ \ No newline at end of file diff --git a/sarif.fsx b/sarif.fsx index 4516916bcc..8c1247ee83 100644 --- a/sarif.fsx +++ b/sarif.fsx @@ -1,144 +1,254 @@ -#r "nuget: FSharp.SystemTextJson, 1.4.36" +#r "nuget: Thoth.Json.Newtonsoft, 0.3.2" open System open System.IO open System.Text.Json -open System.Text.Json.Serialization open System.Threading.Tasks -[] -type Text = - { [] - text: string } +type Text = { text: string } -[] type Region = - { [] - startLine: int - [] + { startLine: int startColumn: int - [] endLine: int - [] endColumn: int } -[] -type ArtifactLocation = - { [] - uri: string } +type ArtifactLocation = { uri: string } -[] type PhysicalLocation = - { [] - artifactLocation: ArtifactLocation - [] + { artifactLocation: ArtifactLocation region: Region } -[] -type Location = - { [] - physicalLocation: PhysicalLocation } +type Location = { physicalLocation: PhysicalLocation } -[] -type Message = - { [] - text: string } +type Message = { text: string } -[] type Result = - { [] - ruleId: string - [] + { ruleId: string ruleIndex: int - [] message: Message - [] locations: Location list } -[] -type RuleShortDescription = - { [] - text: string - [] - markdown: string } +type RuleShortDescription = { text: string; markdown: string } -[] type Rule = - { [] - id: string - [] + { id: string name: string - [] shortDescription: RuleShortDescription - [] helpUri: string } -[] type Driver = - { [] - name: string - [] + { name: string version: string - [] informationUri: string - [] rules: Rule list option } -[] -type Tool = - { [] - driver: Driver } +type Tool = { driver: Driver } -[] type Invocation = - { [] - startTimeUtc: DateTime - [] + { startTimeUtc: DateTime endTimeUtc: DateTime - [] executionSuccessful: bool } -[] type Run = - { [] - results: Result list - [] + { results: Result list tool: Tool - [] invocations: Invocation list - [] columnKind: string } -[] type SarifLog = - { - // This field needs JsonPropertyName because F# doesn't allow '$' in identifiers. - [] - schema: string - [] + { schema: string version: string - [] runs: Run list } -let private options = - JsonFSharpOptions.Default() - .ToJsonSerializerOptions() +module private Encoders = + open Thoth.Json.Core + + let textEncoder: Encoder = + fun (t: Text) -> Encode.object [ ("text", Encode.string t.text) ] + + let regionEncoder: Encoder = + fun (r: Region) -> + Encode.object + [ ("startLine", Encode.int r.startLine) + ("startColumn", Encode.int r.startColumn) + ("endLine", Encode.int r.endLine) + ("endColumn", Encode.int r.endColumn) ] + + let artifactLocationEncoder: Encoder = + fun (al: ArtifactLocation) -> Encode.object [ ("uri", Encode.string al.uri) ] + + let physicalLocationEncoder: Encoder = + fun (pl: PhysicalLocation) -> + Encode.object + [ ("artifactLocation", artifactLocationEncoder pl.artifactLocation) + ("region", regionEncoder pl.region) ] + + let locationEncoder: Encoder = + fun (l: Location) -> Encode.object [ ("physicalLocation", physicalLocationEncoder l.physicalLocation) ] + + let messageEncoder: Encoder = + fun (m: Message) -> Encode.object [ ("text", Encode.string m.text) ] + + let resultEncoder: Encoder = + fun (r: Result) -> + Encode.object + [ ("ruleId", Encode.string r.ruleId) + ("ruleIndex", Encode.int r.ruleIndex) + ("message", messageEncoder r.message) + ("locations", List.map locationEncoder r.locations |> Encode.list) ] + + let ruleShortDescriptionEncoder: Encoder = + fun (rsd: RuleShortDescription) -> + Encode.object [ ("text", Encode.string rsd.text); ("markdown", Encode.string rsd.markdown) ] + + let ruleEncoder: Encoder = + fun (r: Rule) -> + Encode.object + [ ("id", Encode.string r.id) + ("name", Encode.string r.name) + ("shortDescription", ruleShortDescriptionEncoder r.shortDescription) + ("helpUri", Encode.string r.helpUri) ] + + let driverEncoder: Encoder = + fun (d: Driver) -> + Encode.object + [ ("name", Encode.string d.name) + ("version", Encode.string d.version) + ("informationUri", Encode.string d.informationUri) + ("rules", + match d.rules with + | None -> Encode.list [] + | Some rules -> List.map ruleEncoder rules |> Encode.list) ] + + let toolEncoder: Encoder = + fun (t: Tool) -> Encode.object [ ("driver", driverEncoder t.driver) ] + + let invocationEncoder: Encoder = + fun (i: Invocation) -> + Encode.object + [ ("startTimeUtc", Encode.string (i.startTimeUtc.ToString("o"))) // ISO 8601 format + ("endTimeUtc", Encode.string (i.endTimeUtc.ToString("o"))) + ("executionSuccessful", Encode.bool i.executionSuccessful) ] + + let runEncoder: Encoder = + fun (r: Run) -> + Encode.object + [ ("results", List.map resultEncoder r.results |> Encode.list) + ("tool", toolEncoder r.tool) + ("invocations", List.map invocationEncoder r.invocations |> Encode.list) + ("columnKind", Encode.string r.columnKind) ] + + let sarifLogEncoder: Encoder = + fun (log: SarifLog) -> + Encode.object + [ ("$schema", Encode.string log.schema) + ("version", Encode.string log.version) + ("runs", List.map runEncoder log.runs |> Encode.list) ] + +module private Decoders = + open Thoth.Json.Core + + let textDecoder: Decoder = + Decode.object (fun get -> { text = get.Required.Field "text" Decode.string }) -let private readSarif (json: Stream) : System.Threading.Tasks.ValueTask = - JsonSerializer.DeserializeAsync(json, options) + let regionDecoder: Decoder = + Decode.object (fun get -> + { startLine = get.Required.Field "startLine" Decode.int + startColumn = get.Required.Field "startColumn" Decode.int + endLine = get.Required.Field "endLine" Decode.int + endColumn = get.Required.Field "endColumn" Decode.int }) -let private writeSarif (json: Stream) (sarifLog: SarifLog) : Task = - JsonSerializer.SerializeAsync(json, sarifLog, options) + let artifactLocationDecoder: Decoder = + Decode.object (fun get -> { uri = get.Required.Field "uri" Decode.string }) + + let physicalLocationDecoder: Decoder = + Decode.object (fun get -> + { artifactLocation = get.Required.Field "artifactLocation" artifactLocationDecoder + region = get.Required.Field "region" regionDecoder }) + + let locationDecoder: Decoder = + Decode.object (fun get -> { physicalLocation = get.Required.Field "physicalLocation" physicalLocationDecoder }) + + let messageDecoder: Decoder = + Decode.object (fun get -> { text = get.Required.Field "text" Decode.string }) + + let resultDecoder: Decoder = + Decode.object (fun get -> + { ruleId = get.Required.Field "ruleId" Decode.string + ruleIndex = get.Required.Field "ruleIndex" Decode.int + message = get.Required.Field "message" messageDecoder + locations = get.Required.Field "locations" (Decode.list locationDecoder) }) + + let ruleShortDescriptionDecoder: Decoder = + Decode.object (fun get -> + { text = get.Required.Field "text" Decode.string + markdown = get.Required.Field "markdown" Decode.string }) + + let ruleDecoder: Decoder = + Decode.object (fun get -> + { id = get.Required.Field "id" Decode.string + name = get.Required.Field "name" Decode.string + shortDescription = get.Required.Field "shortDescription" ruleShortDescriptionDecoder + helpUri = get.Required.Field "helpUri" Decode.string }) + + let driverDecoder: Decoder = + Decode.object (fun get -> + { name = get.Required.Field "name" Decode.string + version = get.Required.Field "version" Decode.string + informationUri = get.Required.Field "informationUri" Decode.string + rules = get.Optional.Field "rules" (Decode.list ruleDecoder) }) + + let toolDecoder: Decoder = + Decode.object (fun get -> { driver = get.Required.Field "driver" driverDecoder }) + + let invocationDecoder: Decoder = + Decode.object (fun get -> + { startTimeUtc = get.Required.Field "startTimeUtc" Decode.datetimeUtc + endTimeUtc = get.Required.Field "endTimeUtc" Decode.datetimeUtc + executionSuccessful = get.Required.Field "executionSuccessful" Decode.bool }) + + let runDecoder: Decoder = + Decode.object (fun get -> + { results = get.Required.Field "results" (Decode.list resultDecoder) + tool = get.Required.Field "tool" toolDecoder + invocations = get.Required.Field "invocations" (Decode.list invocationDecoder) + columnKind = get.Required.Field "columnKind" Decode.string }) + + let sarifLogDecoder: Decoder = + Decode.object (fun get -> + { schema = get.Required.Field "$schema" Decode.string + version = get.Required.Field "version" Decode.string + runs = get.Required.Field "runs" (Decode.list runDecoder) }) + +let private readSarif (json: string) : Result = + match Thoth.Json.Newtonsoft.Decode.fromString Decoders.sarifLogDecoder json with + | Ok sarifLog -> Ok sarifLog + | Error err -> Error($"Failed to decode, got %A{err}") + +let private writeSarif (sarifLog: SarifLog) : string = + Encoders.sarifLogEncoder sarifLog |> Thoth.Json.Newtonsoft.Encode.toString 4 let mergeSarifFiles _ = task { + let mergedPath = + Path.Combine(__SOURCE_DIRECTORY__, "analysisreports", "merged.sarif") + + if Path.Exists(mergedPath) then + File.Delete(mergedPath) + let! sarifFiles = Directory.GetFiles("analysisreports", "*.sarif") |> Seq.map (fun path -> task { - use sarifContent = File.OpenRead(path) - let! sarif = readSarif sarifContent - return path, sarif + let! sarifContent = File.ReadAllTextAsync(path) + let sarifResult = readSarif sarifContent + + match sarifResult with + | Error e -> + eprintfn $"%A{e}" + return exit 1 + | Ok sarif -> return path, sarif }) |> Task.WhenAll @@ -178,8 +288,9 @@ let mergeSarifFiles _ = sarifFiles |> Array.iter (fun (path, _) -> File.Delete(path)) let mergedStream = File.OpenWrite("analysisreports/merged.sarif") - do! writeSarif mergedStream combined + let combinedJson = writeSarif combined + do! mergedStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(combinedJson)) do! mergedStream.FlushAsync() mergedStream.Close() - printfn "Successfully merged %d SARIF files" sarifFiles.Length - } \ No newline at end of file + printfn $"Successfully merged %d{sarifFiles.Length} SARIF files" + } From 1ea94d404a26a0eeb5d3be2e5c5d932e2530c6ef Mon Sep 17 00:00:00 2001 From: nojaf Date: Tue, 5 Aug 2025 10:07:54 +0200 Subject: [PATCH 5/6] Clean up --- sarif.fsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sarif.fsx b/sarif.fsx index 8c1247ee83..42d073b8a0 100644 --- a/sarif.fsx +++ b/sarif.fsx @@ -2,7 +2,6 @@ open System open System.IO -open System.Text.Json open System.Threading.Tasks type Text = { text: string } @@ -255,7 +254,7 @@ let mergeSarifFiles _ = if Array.isEmpty sarifFiles then printfn "No sarif files could be merged" else - let firstSarif = snd (sarifFiles.[0]) + let firstSarif = snd sarifFiles.[0] let firstRun = firstSarif.runs.[0] let results = ResizeArray() From c0f4e4b1ca9c0da38e07fd76229c87221e141e11 Mon Sep 17 00:00:00 2001 From: nojaf Date: Wed, 6 Aug 2025 14:31:12 +0200 Subject: [PATCH 6/6] Guess that is solved now --- sarif.fsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sarif.fsx b/sarif.fsx index 42d073b8a0..89889136fa 100644 --- a/sarif.fsx +++ b/sarif.fsx @@ -271,8 +271,7 @@ let mergeSarifFiles _ = let combined: SarifLog = { - // I don't know why firstSarif.schema is null - schema = "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.6.json" + schema = firstSarif.schema version = firstSarif.version runs = [ { tool =