Skip to content

Commit

Permalink
Merge pull request #119 from nojaf/report
Browse files Browse the repository at this point in the history
Add report flag to CLI tool
  • Loading branch information
nojaf authored Oct 16, 2023
2 parents 3436522 + 36ed716 commit 6108442
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 36 deletions.
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

0 comments on commit 6108442

Please sign in to comment.