Skip to content
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

Add report flag to CLI tool #119

Merged
merged 8 commits into from
Oct 16, 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
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ 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).

## [0.16.0] - 2023-10-16

### Added
* [Analyzer report ](https://github.com/ionide/FSharp.Analyzers.SDK/issues/110) (thanks @nojaf!)

## [0.15.0] - 2023-10-10

### Added
* [Support multiple project parameters in the Cli tool](https://github.com/ionide/FSharp.Analyzers.SDK/pull/116) (thanks @dawedawe!)
* [Exclude analyzers](https://github.com/ionide/FSharp.Analyzers.SDK/issues/112) (thanks @nojaf)
* [Exclude analyzers](https://github.com/ionide/FSharp.Analyzers.SDK/issues/112) (thanks @nojaf!)

## [0.14.1] - 2023-09-26

Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@
<PackageVersion Include="NUnit3TestAdapter" Version="4.4.2" />
<PackageVersion Include="NUnit.Analyzers" Version="3.6.1" />
<PackageVersion Include="coverlet.collector" Version="3.2.0" />
<PackageVersion Include="Sarif.Sdk" Version="4.3.4" />
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions src/FSharp.Analyzers.Cli/FSharp.Analyzers.Cli.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<PackageReference Include="Microsoft.Build.Locator" />
<PackageReference Include="Microsoft.Build.Tasks.Core" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.Build.Utilities.Core" ExcludeAssets="runtime" />
<PackageReference Include="Sarif.Sdk" />
</ItemGroup>

<ItemGroup>
Expand Down
103 changes: 97 additions & 6 deletions src/FSharp.Analyzers.Cli/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ open FSharp.Compiler.Text
open Argu
open FSharp.Analyzers.SDK
open GlobExpressions
open Microsoft.CodeAnalysis.Sarif
open Microsoft.CodeAnalysis.Sarif.Writers
open Ionide.ProjInfo

type Arguments =
Expand All @@ -13,6 +15,7 @@ type Arguments =
| Fail_On_Warnings of string list
| Ignore_Files of string list
| Exclude_Analyzer of string list
| Report of string
| Verbose

interface IArgParserTemplate with
Expand All @@ -24,6 +27,7 @@ type Arguments =
"List of analyzer codes that should trigger tool failures in the presence of warnings."
| 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."

let mutable verbose = false
Expand Down Expand Up @@ -107,15 +111,17 @@ let runProject (client: Client<CliAnalyzerAttribute, CliContext>) toolsPath proj
]
}

let printMessages failOnWarnings (msgs: Message list) =
let printMessages failOnWarnings (msgs: AnalyzerMessage list) =
if verbose then
printfn ""

if verbose && List.isEmpty msgs then
printfn "No messages found from the analyzer(s)"

msgs
|> Seq.iter (fun m ->
|> Seq.iter (fun analyzerMessage ->
let m = analyzerMessage.Message

let color =
match m.Severity with
| Error -> ConsoleColor.Red
Expand All @@ -140,15 +146,97 @@ let printMessages failOnWarnings (msgs: Message list) =

msgs

let calculateExitCode failOnWarnings (msgs: Message list option) : int =
let writeReport (results: AnalyzerMessage list option) (report: string) =
try
let driver = ToolComponent()
driver.Name <- "Ionide.Analyzers.Cli"
driver.InformationUri <- Uri("https://ionide.io/FSharp.Analyzers.SDK/")
driver.Version <- string (System.Reflection.Assembly.GetExecutingAssembly().GetName().Version)
let tool = Tool()
tool.Driver <- driver
let run = Run()
run.Tool <- tool

use sarifLogger =
new SarifLogger(
report,
logFilePersistenceOptions =
(FilePersistenceOptions.PrettyPrint ||| FilePersistenceOptions.ForceOverwrite),
run = run,
levels = BaseLogger.ErrorWarningNote,
kinds = BaseLogger.Fail,
closeWriterOnDispose = true
)

sarifLogger.AnalysisStarted()

for analyzerResult in (Option.defaultValue List.empty results) do
let reportDescriptor = ReportingDescriptor()
reportDescriptor.Id <- analyzerResult.Message.Code
reportDescriptor.Name <- analyzerResult.Message.Message

analyzerResult.ShortDescription
|> Option.iter (fun shortDescription ->
reportDescriptor.ShortDescription <-
MultiformatMessageString(shortDescription, shortDescription, dict [])
)

analyzerResult.HelpUri
|> Option.iter (fun helpUri -> reportDescriptor.HelpUri <- Uri(helpUri))

let result = Result()
result.RuleId <- reportDescriptor.Id

result.Level <-
match analyzerResult.Message.Severity with
| Info -> FailureLevel.Note
| Hint -> FailureLevel.None
| Warning -> FailureLevel.Warning
| Error -> FailureLevel.Error

let msg = Message()
msg.Text <- analyzerResult.Message.Message
result.Message <- msg

let physicalLocation = PhysicalLocation()

physicalLocation.ArtifactLocation <-
let al = ArtifactLocation()
al.Uri <- Uri(analyzerResult.Message.Range.FileName)
al

physicalLocation.Region <-
let r = Region()
r.StartLine <- analyzerResult.Message.Range.StartLine
r.StartColumn <- analyzerResult.Message.Range.StartColumn
r.EndLine <- analyzerResult.Message.Range.EndLine
r.EndColumn <- analyzerResult.Message.Range.EndColumn
r

let location: Location = Location()
location.PhysicalLocation <- physicalLocation
result.Locations <- [| location |]

sarifLogger.Log(reportDescriptor, result, System.Nullable())

sarifLogger.AnalysisStopped(RuntimeConditions.None)

sarifLogger.Dispose()
with ex ->
let details = if not verbose then "" else $" %s{ex.Message}"
printfn $"Could not write sarif to %s{report}%s{details}"

let calculateExitCode failOnWarnings (msgs: AnalyzerMessage list option) : int =
match msgs with
| None -> -1
| Some msgs ->
let check =
msgs
|> List.exists (fun n ->
n.Severity = Error
|| (n.Severity = Warning && failOnWarnings |> List.contains n.Code)
|> List.exists (fun analyzerMessage ->
let message = analyzerMessage.Message

message.Severity = Error
|| (message.Severity = Warning && failOnWarnings |> List.contains message.Code)
)

if check then -2 else 0
Expand Down Expand Up @@ -197,6 +285,7 @@ let main argv =
printInfo "Registered %d analyzers from %d dlls" analyzers dlls

let projOpts = results.TryGetResult <@ Project @>
let report = results.TryGetResult <@ Report @>

let results =
if analyzers = 0 then
Expand Down Expand Up @@ -231,4 +320,6 @@ let main argv =
|> List.concat
|> Some

report |> Option.iter (writeReport results)

calculateExitCode failOnWarnings results
78 changes: 60 additions & 18 deletions src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,25 @@ type AnalysisResult =

module Client =

type RegisteredAnalyzer<'TContext when 'TContext :> Context> =
{
AssemblyPath: string
Name: string
Analyzer: Analyzer<'TContext>
ShortDescription: string option
HelpUri: string option
}

let isAnalyzer<'TAttribute when 'TAttribute :> AnalyzerAttribute> (mi: MemberInfo) =
mi.GetCustomAttributes true
|> Seq.tryFind (fun n -> n.GetType().Name = typeof<'TAttribute>.Name)
|> Option.map unbox<'TAttribute>

let analyzerFromMember<'TAnalyzerAttribute, 'TContext when 'TAnalyzerAttribute :> AnalyzerAttribute>
let analyzerFromMember<'TAnalyzerAttribute, 'TContext
when 'TAnalyzerAttribute :> AnalyzerAttribute and 'TContext :> Context>
(path: string)
(mi: MemberInfo)
: (string * Analyzer<'TContext>) option
: RegisteredAnalyzer<'TContext> option
=
let inline unboxAnalyzer v =
if isNull v then failwith "Analyzer is null" else unbox v
Expand Down Expand Up @@ -75,13 +86,30 @@ module Client =
match isAnalyzer<'TAnalyzerAttribute> mi with
| Some analyzerAttribute ->
match getAnalyzerFromMemberInfo mi with
| Some analyzer -> Some(analyzerAttribute.Name, analyzer)
| Some analyzer ->
let name =
if String.IsNullOrWhiteSpace analyzerAttribute.Name then
mi.Name
else
analyzerAttribute.Name

Some
{
AssemblyPath = path
Name = name
Analyzer = analyzer
ShortDescription = analyzerAttribute.ShortDescription
HelpUri = analyzerAttribute.HelpUri
}

| None -> None
| None -> None

let analyzersFromType<'TAnalyzerAttribute, 'TContext when 'TAnalyzerAttribute :> AnalyzerAttribute>
let analyzersFromType<'TAnalyzerAttribute, 'TContext
when 'TAnalyzerAttribute :> AnalyzerAttribute and 'TContext :> Context>
(path: string)
(t: Type)
: (string * Analyzer<'TContext>) list
: RegisteredAnalyzer<'TContext> list
=
let asMembers x = Seq.map (fun m -> m :> MemberInfo) x
let bindingFlags = BindingFlags.Public ||| BindingFlags.Static
Expand All @@ -95,7 +123,7 @@ module Client =
|> Seq.collect id

members
|> Seq.choose analyzerFromMember<'TAnalyzerAttribute, 'TContext>
|> Seq.choose (analyzerFromMember<'TAnalyzerAttribute, 'TContext> path)
|> Seq.toList

[<Interface>]
Expand All @@ -107,7 +135,7 @@ type Client<'TAttribute, 'TContext when 'TAttribute :> AnalyzerAttribute and 'TC
(logger: Logger, excludedAnalyzers: string Set)
=
let registeredAnalyzers =
ConcurrentDictionary<string, (string * Analyzer<'TContext>) list>()
ConcurrentDictionary<string, Client.RegisteredAnalyzer<'TContext> list>()

new() =
Client(
Expand Down Expand Up @@ -169,12 +197,12 @@ type Client<'TAttribute, 'TContext when 'TAttribute :> AnalyzerAttribute and 'TC
|> Array.map (fun (path, assembly) ->
let analyzers =
assembly.GetExportedTypes()
|> Seq.collect Client.analyzersFromType<'TAttribute, 'TContext>
|> Seq.filter (fun (analyzerName, _) ->
let shouldExclude = excludedAnalyzers.Contains(analyzerName)
|> Seq.collect (Client.analyzersFromType<'TAttribute, 'TContext> path)
|> Seq.filter (fun registeredAnalyzer ->
let shouldExclude = excludedAnalyzers.Contains(registeredAnalyzer.Name)

if shouldExclude then
logger.Verbose $"Excluding %s{analyzerName} from %s{assembly.FullName}"
logger.Verbose $"Excluding %s{registeredAnalyzer.Name} from %s{assembly.FullName}"

not shouldExclude
)
Expand All @@ -191,15 +219,29 @@ type Client<'TAttribute, 'TContext when 'TAttribute :> AnalyzerAttribute and 'TC
else
0, 0

member x.RunAnalyzers(ctx: 'TContext) : Async<Message list> =
member x.RunAnalyzers(ctx: 'TContext) : Async<AnalyzerMessage list> =
async {
let analyzers = registeredAnalyzers.Values |> Seq.collect id

let! messagesPerAnalyzer =
analyzers
|> Seq.map (fun (_analyzerName, analyzer) ->
|> Seq.map (fun registeredAnalyzer ->
try
analyzer ctx
async {
let! messages = registeredAnalyzer.Analyzer ctx

return
messages
|> List.map (fun message ->
{
Message = message
Name = registeredAnalyzer.Name
AssemblyPath = registeredAnalyzer.AssemblyPath
ShortDescription = registeredAnalyzer.ShortDescription
HelpUri = registeredAnalyzer.HelpUri
}
)
}
with error ->
async.Return []
)
Expand All @@ -218,20 +260,20 @@ type Client<'TAttribute, 'TContext when 'TAttribute :> AnalyzerAttribute and 'TC

let! results =
analyzers
|> Seq.map (fun (analyzerName, analyzer) ->
|> Seq.map (fun registeredAnalyzer ->
async {
try
let! result = analyzer ctx
let! result = registeredAnalyzer.Analyzer ctx

return
{
AnalyzerName = analyzerName
AnalyzerName = registeredAnalyzer.Name
Output = Result.Ok result
}
with error ->
return
{
AnalyzerName = analyzerName
AnalyzerName = registeredAnalyzer.Name
Output = Result.Error error
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type Client<'TAttribute, 'TContext when 'TAttribute :> AnalyzerAttribute and 'TC
member LoadAnalyzers: dir: string -> int * int
/// <summary>Runs all registered analyzers for given context (file).</summary>
/// <returns>list of messages. Ignores errors from the analyzers</returns>
member RunAnalyzers: ctx: 'TContext -> Async<Message list>
member RunAnalyzers: ctx: 'TContext -> Async<AnalyzerMessage list>
/// <summary>Runs all registered analyzers for given context (file).</summary>
/// <returns>list of results per analyzer which can either be messages or an exception.</returns>
member RunAnalyzersSafely: ctx: 'TContext -> Async<AnalysisResult list>
Loading