Skip to content

Support remapping all severity levels #138

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
137 changes: 105 additions & 32 deletions src/FSharp.Analyzers.Cli/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,70 @@ 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
| [<Unique>] Treat_As_Info of string list
| [<Unique>] Treat_As_Hint of string list
| [<Unique>] Treat_As_Warning of string list
| [<Unique>] Treat_As_Error of string list
| [<Unique>] Ignore_Files of string list
| [<Unique>] Exclude_Analyzer of string list
| [<Unique>] Report of string
| [<Unique>] FSC_Args of string
| [<Unique>] Verbose

interface IArgParserTemplate with
member s.Usage =
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 _ ->
"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 =
{
TreatAsInfo: Set<string>
TreatAsHint: Set<string>
TreatAsWarning: Set<string>
TreatAsError: Set<string>
}

member x.IsValid() =
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 =
if mappings.TreatAsInfo |> Set.contains msg.Message.Code then
Info
else if mappings.TreatAsHint |> Set.contains msg.Message.Code then
Hint
else if mappings.TreatAsWarning |> Set.contains msg.Message.Code then
Warning
else if mappings.TreatAsError |> Set.contains msg.Message.Code then
Error
else
msg.Message.Severity

{ msg with
Message =
{ msg.Message with
Severity = targetSeverity
}
}

let mutable verbose = false

let fcs = Utils.createFCS None
Expand Down Expand Up @@ -77,6 +121,7 @@ let runProjectAux
(client: Client<CliAnalyzerAttribute, CliContext>)
(fsharpOptions: FSharpProjectOptions)
(ignoreFiles: Glob list)
(mappings: SeverityMappings)
=
async {
let! checkProjectResults = fcs.ParseAndCheckProject(fsharpOptions)
Expand Down Expand Up @@ -107,23 +152,35 @@ let runProjectAux
Some
[
for messages in messagesPerAnalyzer do
yield! messages
let mappedMessages = messages |> List.map (mapMessageToSeverity mappings)
yield! mappedMessages
]
}

let runProject (client: Client<CliAnalyzerAttribute, CliContext>) toolsPath proj (globs: Glob list) =
let runProject
(client: Client<CliAnalyzerAttribute, CliContext>)
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" |]

let isFSharpFile (file: string) =
Seq.exists (fun (ext: string) -> file.EndsWith ext) fsharpFiles

let runFscArgs (client: Client<CliAnalyzerAttribute, CliContext>) (fscArgs: string) (globs: Glob list) =
let runFscArgs
(client: Client<CliAnalyzerAttribute, CliContext>)
(fscArgs: string)
(globs: Glob list)
(mappings: SeverityMappings)
=
let fscArgs = fscArgs.Split(';', StringSplitOptions.RemoveEmptyEntries)

let sourceFiles =
Expand Down Expand Up @@ -155,9 +212,9 @@ let runFscArgs (client: Client<CliAnalyzerAttribute, CliContext>) (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 ""

Expand All @@ -171,7 +228,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
Expand All @@ -190,7 +246,7 @@ let printMessages failOnWarnings (msgs: AnalyzerMessage list) =
Console.ForegroundColor <- origForegroundColor
)

msgs
()

let writeReport (results: AnalyzerMessage list option) (report: string) =
try
Expand Down Expand Up @@ -278,7 +334,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 ->
Expand All @@ -288,7 +344,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
Expand All @@ -301,15 +356,34 @@ 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 =
{
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 "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 <treat-as-severity> arguments."

exit 1

let ignoreFiles = results.GetResult(<@ Ignore_Files @>, [])
printInfo "Ignore Files: [%s]" (ignoreFiles |> String.concat ", ")
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
Expand Down Expand Up @@ -357,7 +431,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 @>

Expand All @@ -366,17 +440,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`."

None
| Some _, Some _ ->
| _ :: _, Some _ ->
printError "`--project` and `--fsc-args` cannot be combined."
exit 1
| None, Some fscArgs -> runFscArgs client fscArgs ignoreFiles |> Async.RunSynchronously
| Some projects, None ->
| [], Some fscArgs -> runFscArgs client fscArgs ignoreFiles severityMapping |> Async.RunSynchronously
| projects, None ->
let runProj (proj: string) =
async {
let project =
Expand All @@ -385,8 +457,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
Expand All @@ -397,6 +469,7 @@ let main argv =
|> List.concat
|> Some

results |> Option.iter printMessages
report |> Option.iter (writeReport results)

calculateExitCode failOnWarnings results
calculateExitCode results