From cb4ceed0ef11128e9c315f895c0d4f1300367fc9 Mon Sep 17 00:00:00 2001 From: dawe Date: Mon, 30 Oct 2023 18:17:15 +0100 Subject: [PATCH 1/7] - mark all CLI args as unique - print messages when using FSC args, too - support remapping of all severity levels --- src/FSharp.Analyzers.Cli/Program.fs | 138 +++++++++++++++++++++++----- 1 file changed, 114 insertions(+), 24 deletions(-) diff --git a/src/FSharp.Analyzers.Cli/Program.fs b/src/FSharp.Analyzers.Cli/Program.fs index e33b74a..b58638d 100644 --- a/src/FSharp.Analyzers.Cli/Program.fs +++ b/src/FSharp.Analyzers.Cli/Program.fs @@ -11,14 +11,18 @@ open Microsoft.CodeAnalysis.Sarif.Writers open Ionide.ProjInfo type Arguments = - | Project of string list - | Analyzers_Path of string list - | Fail_On_Warnings of string list - | Ignore_Files of string list - | Exclude_Analyzer of string list - | Report of string - | FSC_Args of string - | Verbose + | [] Project of string list + | [] Analyzers_Path of string list + | [] Fail_On_Warnings of string list + | [] Treat_As_Info of string list + | [] Treat_As_Hint of string list + | [] Treat_As_Warning of string list + | [] Treat_As_Error of string list + | [] Ignore_Files of string list + | [] Exclude_Analyzer of string list + | [] Report of string + | [] FSC_Args of string + | [] Verbose interface IArgParserTemplate with member s.Usage = @@ -27,12 +31,68 @@ type Arguments = | Analyzers_Path _ -> "Path to a folder where your analyzers are located." | Fail_On_Warnings _ -> "List of analyzer codes that should trigger tool failures in the presence of warnings." + | Treat_As_Info _ -> + "List of analyzer codes that should be treated as severity Info by the tool. Regardless of the original severity." + | Treat_As_Hint _ -> + "List of analyzer codes that should be treated as severity Hint by the tool. Regardless of the original severity." + | Treat_As_Warning _ -> + "List of analyzer codes that should be treated as severity Warning by the tool. Regardless of the original severity." + | Treat_As_Error _ -> + "List of analyzer codes that should be treated as severity Error by the tool. Regardless of the original severity." | Ignore_Files _ -> "Source files that shouldn't be processed." | Exclude_Analyzer _ -> "The names of analyzers that should not be executed." | Report _ -> "Write the result messages to a (sarif) report file." | Verbose -> "Verbose logging." | FSC_Args _ -> "Pass in the raw fsc compiler arguments. Cannot be combined with the `--project` flag." +type SeverityMappings = + { + FailOnWarnings: string list + TreatAsInfo: string list + TreatAsHint: string list + TreatAsWarning: string list + TreatAsError: string list + } + + member x.IsValid() = + let allCodes = + [ + x.FailOnWarnings + x.TreatAsInfo + x.TreatAsHint + x.TreatAsWarning + x.TreatAsError + ] + |> List.concat + + let distinctLength = allCodes |> List.distinct |> List.length + allCodes.Length = distinctLength + +let mapMessageToSeverity (mappings: SeverityMappings) (msg: FSharp.Analyzers.SDK.AnalyzerMessage) = + let targetSeverity = + if mappings.TreatAsInfo |> List.contains msg.Message.Code then + Info + else if mappings.TreatAsHint |> List.contains msg.Message.Code then + Hint + else if mappings.TreatAsWarning |> List.contains msg.Message.Code then + Warning + else if mappings.TreatAsError |> List.contains msg.Message.Code then + Error + else if + mappings.FailOnWarnings |> List.contains msg.Message.Code + && msg.Message.Severity = Warning + then + Error + else + msg.Message.Severity + + { msg with + Message = + { msg.Message with + Severity = targetSeverity + } + } + let mutable verbose = false let fcs = Utils.createFCS None @@ -77,6 +137,7 @@ let runProjectAux (client: Client) (fsharpOptions: FSharpProjectOptions) (ignoreFiles: Glob list) + (mappings: SeverityMappings) = async { let! checkProjectResults = fcs.ParseAndCheckProject(fsharpOptions) @@ -107,15 +168,22 @@ let runProjectAux Some [ for messages in messagesPerAnalyzer do - yield! messages + let mappedMessages = messages |> List.map (mapMessageToSeverity mappings) + yield! mappedMessages ] } -let runProject (client: Client) toolsPath proj (globs: Glob list) = +let runProject + (client: Client) + toolsPath + proj + (globs: Glob list) + (mappings: SeverityMappings) + = async { let path = Path.Combine(Environment.CurrentDirectory, proj) |> Path.GetFullPath let! option = loadProject toolsPath path - return! runProjectAux client option globs + return! runProjectAux client option globs mappings } let fsharpFiles = set [| ".fs"; ".fsi"; ".fsx" |] @@ -123,7 +191,12 @@ let fsharpFiles = set [| ".fs"; ".fsi"; ".fsx" |] let isFSharpFile (file: string) = Seq.exists (fun (ext: string) -> file.EndsWith ext) fsharpFiles -let runFscArgs (client: Client) (fscArgs: string) (globs: Glob list) = +let runFscArgs + (client: Client) + (fscArgs: string) + (globs: Glob list) + (mappings: SeverityMappings) + = let fscArgs = fscArgs.Split(';', StringSplitOptions.RemoveEmptyEntries) let sourceFiles = @@ -155,9 +228,9 @@ let runFscArgs (client: Client) (fscArgs: stri Stamp = None } - runProjectAux client projectOptions globs + runProjectAux client projectOptions globs mappings -let printMessages failOnWarnings (msgs: AnalyzerMessage list) = +let printMessages (msgs: AnalyzerMessage list) = if verbose then printfn "" @@ -171,7 +244,6 @@ let printMessages failOnWarnings (msgs: AnalyzerMessage list) = let color = match m.Severity with | Error -> ConsoleColor.Red - | Warning when failOnWarnings |> List.contains m.Code -> ConsoleColor.Red | Warning -> ConsoleColor.DarkYellow | Info -> ConsoleColor.Blue | Hint -> ConsoleColor.Cyan @@ -190,7 +262,7 @@ let printMessages failOnWarnings (msgs: AnalyzerMessage list) = Console.ForegroundColor <- origForegroundColor ) - msgs + () let writeReport (results: AnalyzerMessage list option) (report: string) = try @@ -278,7 +350,7 @@ let writeReport (results: AnalyzerMessage list option) (report: string) = let details = if not verbose then "" else $" %A{ex}" printfn $"Could not write sarif to %s{report}%s{details}" -let calculateExitCode failOnWarnings (msgs: AnalyzerMessage list option) : int = +let calculateExitCode (msgs: AnalyzerMessage list option) : int = match msgs with | None -> -1 | Some msgs -> @@ -288,7 +360,6 @@ let calculateExitCode failOnWarnings (msgs: AnalyzerMessage list option) : int = let message = analyzerMessage.Message message.Severity = Error - || (message.Severity = Warning && failOnWarnings |> List.contains message.Code) ) if check then -2 else 0 @@ -301,8 +372,26 @@ let main argv = verbose <- results.Contains <@ Verbose @> printInfo "Running in verbose mode" - let failOnWarnings = results.GetResult(<@ Fail_On_Warnings @>, []) - printInfo "Fail On Warnings: [%s]" (failOnWarnings |> String.concat ", ") + let severityMapping = + { + FailOnWarnings = results.GetResult(<@ Fail_On_Warnings @>, []) + TreatAsHint = results.GetResult(<@ Treat_As_Hint @>, []) + TreatAsInfo = results.GetResult(<@ Treat_As_Info @>, []) + TreatAsWarning = results.GetResult(<@ Treat_As_Warning @>, []) + TreatAsError = results.GetResult(<@ Treat_As_Error @>, []) + } + + printInfo "Fail On Warnings: [%s]" (severityMapping.FailOnWarnings |> String.concat ", ") + printInfo "Treat as Hints: [%s]" (severityMapping.TreatAsHint |> String.concat ", ") + printInfo "Treat as Info: [%s]" (severityMapping.TreatAsInfo |> String.concat ", ") + printInfo "Treat as Warning: [%s]" (severityMapping.TreatAsWarning |> String.concat ", ") + printInfo "Treat as Error: [%s]" (severityMapping.TreatAsError |> String.concat ", ") + + if not (severityMapping.IsValid()) then + printError + "An analyzer code may only be listed once in the and arguments." + + exit 1 let ignoreFiles = results.GetResult(<@ Ignore_Files @>, []) printInfo "Ignore Files: [%s]" (ignoreFiles |> String.concat ", ") @@ -375,7 +464,7 @@ let main argv = | Some _, Some _ -> printError "`--project` and `--fsc-args` cannot be combined." exit 1 - | None, Some fscArgs -> runFscArgs client fscArgs ignoreFiles |> Async.RunSynchronously + | None, Some fscArgs -> runFscArgs client fscArgs ignoreFiles severityMapping |> Async.RunSynchronously | Some projects, None -> let runProj (proj: string) = async { @@ -385,8 +474,8 @@ let main argv = else Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, proj)) - let! results = runProject client toolsPath project ignoreFiles - return results |> Option.map (printMessages failOnWarnings) + let! results = runProject client toolsPath project ignoreFiles severityMapping + return results } projects @@ -397,6 +486,7 @@ let main argv = |> List.concat |> Some + results |> Option.iter printMessages report |> Option.iter (writeReport results) - calculateExitCode failOnWarnings results + calculateExitCode results From 5b767b55be89e5c281db124d18dc220c7ba6f3cb Mon Sep 17 00:00:00 2001 From: dawe Date: Mon, 30 Oct 2023 18:18:56 +0100 Subject: [PATCH 2/7] add changelog entry --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25d21dc..186e2c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added +* [Allow remapping of all severity levels](https://github.com/ionide/FSharp.Analyzers.SDK/pull/138) (thanks @dawedawe!) + ## [0.17.1] - 2023-10-30 ### Fixed From ca492a0f3c4a07f940c406c7787ffff426a36192 Mon Sep 17 00:00:00 2001 From: dawe Date: Mon, 30 Oct 2023 18:41:52 +0100 Subject: [PATCH 3/7] support passing of multiple project args on the CLI --- src/FSharp.Analyzers.Cli/Program.fs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/FSharp.Analyzers.Cli/Program.fs b/src/FSharp.Analyzers.Cli/Program.fs index b58638d..08b36d7 100644 --- a/src/FSharp.Analyzers.Cli/Program.fs +++ b/src/FSharp.Analyzers.Cli/Program.fs @@ -11,7 +11,7 @@ open Microsoft.CodeAnalysis.Sarif.Writers open Ionide.ProjInfo type Arguments = - | [] Project of string list + | Project of string list | [] Analyzers_Path of string list | [] Fail_On_Warnings of string list | [] Treat_As_Info of string list @@ -446,7 +446,7 @@ let main argv = printInfo "Registered %d analyzers from %d dlls" analyzers dlls - let projOpts = results.TryGetResult <@ Project @> + let projOpts = results.GetResults <@ Project @> |> List.concat let fscArgs = results.TryGetResult <@ FSC_Args @> let report = results.TryGetResult <@ Report @> @@ -455,17 +455,15 @@ let main argv = Some [] else match projOpts, fscArgs with - | None, None - | Some [], None -> - printError - "No project given. Use `--project PATH_TO_FSPROJ`. Pass path relative to current directory.%s" + | [], None -> + printError "No project given. Use `--project PATH_TO_FSPROJ`. Pass path relative to current directory." None - | Some _, Some _ -> + | _ :: _, Some _ -> printError "`--project` and `--fsc-args` cannot be combined." exit 1 - | None, Some fscArgs -> runFscArgs client fscArgs ignoreFiles severityMapping |> Async.RunSynchronously - | Some projects, None -> + | [], Some fscArgs -> runFscArgs client fscArgs ignoreFiles severityMapping |> Async.RunSynchronously + | projects, None -> let runProj (proj: string) = async { let project = From 2a35552e6b9cd29d6ac856d633468a8434fc3d0d Mon Sep 17 00:00:00 2001 From: dawe Date: Mon, 30 Oct 2023 18:47:08 +0100 Subject: [PATCH 4/7] support passing of multiple analyzer-path flags on the CLI --- src/FSharp.Analyzers.Cli/Program.fs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/FSharp.Analyzers.Cli/Program.fs b/src/FSharp.Analyzers.Cli/Program.fs index 08b36d7..5e4c3b2 100644 --- a/src/FSharp.Analyzers.Cli/Program.fs +++ b/src/FSharp.Analyzers.Cli/Program.fs @@ -12,7 +12,7 @@ open Ionide.ProjInfo type Arguments = | Project of string list - | [] Analyzers_Path of string list + | Analyzers_Path of string list | [] Fail_On_Warnings of string list | [] Treat_As_Info of string list | [] Treat_As_Hint of string list @@ -398,7 +398,11 @@ let main argv = let ignoreFiles = ignoreFiles |> List.map Glob let analyzersPaths = - results.GetResult(<@ Analyzers_Path @>, [ "packages/Analyzers" ]) + results.GetResults(<@ Analyzers_Path @>) + |> List.concat + |> function + | [] -> [ "packages/Analyzers" ] + | paths -> paths |> List.map (fun path -> if Path.IsPathRooted path then path From 94e366358fe40810947ead48610af9040a0d9176 Mon Sep 17 00:00:00 2001 From: dawe Date: Tue, 31 Oct 2023 09:49:51 +0100 Subject: [PATCH 5/7] use Set instead of string list --- src/FSharp.Analyzers.Cli/Program.fs | 36 ++++++++++++++--------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/FSharp.Analyzers.Cli/Program.fs b/src/FSharp.Analyzers.Cli/Program.fs index 5e4c3b2..c93491b 100644 --- a/src/FSharp.Analyzers.Cli/Program.fs +++ b/src/FSharp.Analyzers.Cli/Program.fs @@ -47,11 +47,11 @@ type Arguments = type SeverityMappings = { - FailOnWarnings: string list - TreatAsInfo: string list - TreatAsHint: string list - TreatAsWarning: string list - TreatAsError: string list + FailOnWarnings: Set + TreatAsInfo: Set + TreatAsHint: Set + TreatAsWarning: Set + TreatAsError: Set } member x.IsValid() = @@ -63,23 +63,23 @@ type SeverityMappings = x.TreatAsWarning x.TreatAsError ] - |> List.concat - let distinctLength = allCodes |> List.distinct |> List.length - allCodes.Length = distinctLength + let distinctLength = allCodes |> Set.unionMany |> Set.count + let summedLength = allCodes |> List.sumBy Set.count + summedLength = distinctLength let mapMessageToSeverity (mappings: SeverityMappings) (msg: FSharp.Analyzers.SDK.AnalyzerMessage) = let targetSeverity = - if mappings.TreatAsInfo |> List.contains msg.Message.Code then + if mappings.TreatAsInfo |> Set.contains msg.Message.Code then Info - else if mappings.TreatAsHint |> List.contains msg.Message.Code then + else if mappings.TreatAsHint |> Set.contains msg.Message.Code then Hint - else if mappings.TreatAsWarning |> List.contains msg.Message.Code then + else if mappings.TreatAsWarning |> Set.contains msg.Message.Code then Warning - else if mappings.TreatAsError |> List.contains msg.Message.Code then + else if mappings.TreatAsError |> Set.contains msg.Message.Code then Error else if - mappings.FailOnWarnings |> List.contains msg.Message.Code + mappings.FailOnWarnings |> Set.contains msg.Message.Code && msg.Message.Severity = Warning then Error @@ -374,11 +374,11 @@ let main argv = let severityMapping = { - FailOnWarnings = results.GetResult(<@ Fail_On_Warnings @>, []) - TreatAsHint = results.GetResult(<@ Treat_As_Hint @>, []) - TreatAsInfo = results.GetResult(<@ Treat_As_Info @>, []) - TreatAsWarning = results.GetResult(<@ Treat_As_Warning @>, []) - TreatAsError = results.GetResult(<@ Treat_As_Error @>, []) + FailOnWarnings = results.GetResult(<@ Fail_On_Warnings @>, []) |> Set.ofList + TreatAsHint = results.GetResult(<@ Treat_As_Hint @>, []) |> Set.ofList + TreatAsInfo = results.GetResult(<@ Treat_As_Info @>, []) |> Set.ofList + TreatAsWarning = results.GetResult(<@ Treat_As_Warning @>, []) |> Set.ofList + TreatAsError = results.GetResult(<@ Treat_As_Error @>, []) |> Set.ofList } printInfo "Fail On Warnings: [%s]" (severityMapping.FailOnWarnings |> String.concat ", ") From b54755a15bce153e56a6a5106dd01bcff553a485 Mon Sep 17 00:00:00 2001 From: dawe Date: Tue, 31 Oct 2023 09:51:53 +0100 Subject: [PATCH 6/7] fix error message for missing project --- src/FSharp.Analyzers.Cli/Program.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FSharp.Analyzers.Cli/Program.fs b/src/FSharp.Analyzers.Cli/Program.fs index c93491b..c41cd8c 100644 --- a/src/FSharp.Analyzers.Cli/Program.fs +++ b/src/FSharp.Analyzers.Cli/Program.fs @@ -460,7 +460,7 @@ let main argv = else match projOpts, fscArgs with | [], None -> - printError "No project given. Use `--project PATH_TO_FSPROJ`. Pass path relative to current directory." + printError "No project given. Use `--project PATH_TO_FSPROJ`." None | _ :: _, Some _ -> From db78af13040b7c5c8a26a253398711453e034c79 Mon Sep 17 00:00:00 2001 From: dawe Date: Tue, 31 Oct 2023 10:31:12 +0100 Subject: [PATCH 7/7] remove the fail-on-warnings flag, too close to the new treat-as-xxx support --- src/FSharp.Analyzers.Cli/Program.fs | 31 ++++++----------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/src/FSharp.Analyzers.Cli/Program.fs b/src/FSharp.Analyzers.Cli/Program.fs index c41cd8c..86d4b55 100644 --- a/src/FSharp.Analyzers.Cli/Program.fs +++ b/src/FSharp.Analyzers.Cli/Program.fs @@ -13,7 +13,6 @@ open Ionide.ProjInfo type Arguments = | Project of string list | Analyzers_Path of string list - | [] Fail_On_Warnings of string list | [] Treat_As_Info of string list | [] Treat_As_Hint of string list | [] Treat_As_Warning of string list @@ -29,8 +28,6 @@ type Arguments = match s with | Project _ -> "Path to your .fsproj file." | Analyzers_Path _ -> "Path to a folder where your analyzers are located." - | Fail_On_Warnings _ -> - "List of analyzer codes that should trigger tool failures in the presence of warnings." | Treat_As_Info _ -> "List of analyzer codes that should be treated as severity Info by the tool. Regardless of the original severity." | Treat_As_Hint _ -> @@ -47,7 +44,6 @@ type Arguments = type SeverityMappings = { - FailOnWarnings: Set TreatAsInfo: Set TreatAsHint: Set TreatAsWarning: Set @@ -55,18 +51,11 @@ type SeverityMappings = } member x.IsValid() = - let allCodes = - [ - x.FailOnWarnings - x.TreatAsInfo - x.TreatAsHint - x.TreatAsWarning - x.TreatAsError - ] - - let distinctLength = allCodes |> Set.unionMany |> Set.count - let summedLength = allCodes |> List.sumBy Set.count - summedLength = distinctLength + let allCodes = [ x.TreatAsInfo; x.TreatAsHint; x.TreatAsWarning; x.TreatAsError ] + + let unionCount = allCodes |> Set.unionMany |> Set.count + let summedCount = allCodes |> List.sumBy Set.count + summedCount = unionCount let mapMessageToSeverity (mappings: SeverityMappings) (msg: FSharp.Analyzers.SDK.AnalyzerMessage) = let targetSeverity = @@ -78,11 +67,6 @@ let mapMessageToSeverity (mappings: SeverityMappings) (msg: FSharp.Analyzers.SDK Warning else if mappings.TreatAsError |> Set.contains msg.Message.Code then Error - else if - mappings.FailOnWarnings |> Set.contains msg.Message.Code - && msg.Message.Severity = Warning - then - Error else msg.Message.Severity @@ -374,22 +358,19 @@ let main argv = let severityMapping = { - FailOnWarnings = results.GetResult(<@ Fail_On_Warnings @>, []) |> Set.ofList TreatAsHint = results.GetResult(<@ Treat_As_Hint @>, []) |> Set.ofList TreatAsInfo = results.GetResult(<@ Treat_As_Info @>, []) |> Set.ofList TreatAsWarning = results.GetResult(<@ Treat_As_Warning @>, []) |> Set.ofList TreatAsError = results.GetResult(<@ Treat_As_Error @>, []) |> Set.ofList } - printInfo "Fail On Warnings: [%s]" (severityMapping.FailOnWarnings |> String.concat ", ") printInfo "Treat as Hints: [%s]" (severityMapping.TreatAsHint |> String.concat ", ") printInfo "Treat as Info: [%s]" (severityMapping.TreatAsInfo |> String.concat ", ") printInfo "Treat as Warning: [%s]" (severityMapping.TreatAsWarning |> String.concat ", ") printInfo "Treat as Error: [%s]" (severityMapping.TreatAsError |> String.concat ", ") if not (severityMapping.IsValid()) then - printError - "An analyzer code may only be listed once in the and arguments." + printError "An analyzer code may only be listed once in the arguments." exit 1