diff --git a/CHANGELOG.md b/CHANGELOG.md
index ad6c457..0b54e8a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 0cf9f42..e604290 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -25,5 +25,6 @@
+
\ No newline at end of file
diff --git a/src/FSharp.Analyzers.Cli/FSharp.Analyzers.Cli.fsproj b/src/FSharp.Analyzers.Cli/FSharp.Analyzers.Cli.fsproj
index 3cb9862..9e314c6 100644
--- a/src/FSharp.Analyzers.Cli/FSharp.Analyzers.Cli.fsproj
+++ b/src/FSharp.Analyzers.Cli/FSharp.Analyzers.Cli.fsproj
@@ -25,6 +25,7 @@
+
diff --git a/src/FSharp.Analyzers.Cli/Program.fs b/src/FSharp.Analyzers.Cli/Program.fs
index 18b8b98..9cf97ed 100644
--- a/src/FSharp.Analyzers.Cli/Program.fs
+++ b/src/FSharp.Analyzers.Cli/Program.fs
@@ -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 =
@@ -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
@@ -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
@@ -107,7 +111,7 @@ let runProject (client: Client) toolsPath proj
]
}
-let printMessages failOnWarnings (msgs: Message list) =
+let printMessages failOnWarnings (msgs: AnalyzerMessage list) =
if verbose then
printfn ""
@@ -115,7 +119,9 @@ let printMessages failOnWarnings (msgs: Message list) =
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
@@ -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
@@ -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
@@ -231,4 +320,6 @@ let main argv =
|> List.concat
|> Some
+ report |> Option.iter (writeReport results)
+
calculateExitCode failOnWarnings results
diff --git a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fs b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fs
index 09592af..05792ef 100644
--- a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fs
+++ b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fs
@@ -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
@@ -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
@@ -95,7 +123,7 @@ module Client =
|> Seq.collect id
members
- |> Seq.choose analyzerFromMember<'TAnalyzerAttribute, 'TContext>
+ |> Seq.choose (analyzerFromMember<'TAnalyzerAttribute, 'TContext> path)
|> Seq.toList
[]
@@ -107,7 +135,7 @@ type Client<'TAttribute, 'TContext when 'TAttribute :> AnalyzerAttribute and 'TC
(logger: Logger, excludedAnalyzers: string Set)
=
let registeredAnalyzers =
- ConcurrentDictionary) list>()
+ ConcurrentDictionary list>()
new() =
Client(
@@ -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
)
@@ -191,15 +219,29 @@ type Client<'TAttribute, 'TContext when 'TAttribute :> AnalyzerAttribute and 'TC
else
0, 0
- member x.RunAnalyzers(ctx: 'TContext) : Async =
+ member x.RunAnalyzers(ctx: 'TContext) : Async =
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 []
)
@@ -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
}
}
diff --git a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fsi b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fsi
index 3cd0d1d..d70dcb2 100644
--- a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fsi
+++ b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fsi
@@ -22,7 +22,7 @@ type Client<'TAttribute, 'TContext when 'TAttribute :> AnalyzerAttribute and 'TC
member LoadAnalyzers: dir: string -> int * int
/// Runs all registered analyzers for given context (file).
/// list of messages. Ignores errors from the analyzers
- member RunAnalyzers: ctx: 'TContext -> Async
+ member RunAnalyzers: ctx: 'TContext -> Async
/// Runs all registered analyzers for given context (file).
/// list of results per analyzer which can either be messages or an exception.
member RunAnalyzersSafely: ctx: 'TContext -> Async
diff --git a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fs b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fs
index 9689562..0d1da36 100644
--- a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fs
+++ b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fs
@@ -50,19 +50,43 @@ module EntityCache =
[]
[]
-type AnalyzerAttribute([ obj)>] name: string) =
+type AnalyzerAttribute(name: string, shortDescription: string, helpUri: string) =
inherit Attribute()
member val Name: string = name
+ member val ShortDescription: string option =
+ if String.IsNullOrWhiteSpace shortDescription then
+ None
+ else
+ Some shortDescription
+
+ member val HelpUri: string option =
+ if String.IsNullOrWhiteSpace helpUri then
+ None
+ else
+ Some helpUri
+
[]
-type CliAnalyzerAttribute([] name: string) =
- inherit AnalyzerAttribute(name)
+type CliAnalyzerAttribute
+ (
+ [] name: string,
+ [ obj)>] shortDescription: string,
+ [ obj)>] helpUri: string
+ )
+ =
+ inherit AnalyzerAttribute(name, shortDescription, helpUri)
member _.Name = name
[]
-type EditorAnalyzerAttribute([] name: string) =
- inherit AnalyzerAttribute(name)
+type EditorAnalyzerAttribute
+ (
+ [] name: string,
+ [ obj)>] shortDescription: string,
+ [ obj)>] helpUri: string
+ )
+ =
+ inherit AnalyzerAttribute(name, shortDescription, helpUri)
member _.Name = name
@@ -143,8 +167,16 @@ type Message =
type Analyzer<'TContext> = 'TContext -> Async
-module Utils =
+type AnalyzerMessage =
+ {
+ Message: Message
+ Name: string
+ AssemblyPath: string
+ ShortDescription: string option
+ HelpUri: string option
+ }
+module Utils =
let currentFSharpAnalyzersSDKVersion =
Assembly.GetExecutingAssembly().GetName().Version
@@ -185,7 +217,7 @@ module Utils =
let typeCheckFile
(fcs: FSharpChecker)
- (printError: (string -> unit))
+ (printError: string -> unit)
(options: FSharpProjectOptions)
(fileName: string)
(source: SourceOfSource)
diff --git a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fsi b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fsi
index fb69758..95c0dea 100644
--- a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fsi
+++ b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fsi
@@ -10,19 +10,36 @@ open FSharp.Compiler.Text
[]
[]
type AnalyzerAttribute =
- new: [ obj)>] name: string -> AnalyzerAttribute
+ new:
+ [ obj)>] name: string *
+ [ obj)>] shortDescription: string *
+ [ obj)>] helpUri: string ->
+ AnalyzerAttribute
+
inherit Attribute
member Name: string
+ member ShortDescription: string option
+ member HelpUri: string option
/// Marks an analyzer for scanning during the console application run.
type CliAnalyzerAttribute =
- new: [ obj)>] name: string -> CliAnalyzerAttribute
+ new:
+ [ obj)>] name: string *
+ [ obj)>] shortDescription: string *
+ [ obj)>] helpUri: string ->
+ CliAnalyzerAttribute
+
inherit AnalyzerAttribute
member Name: string
/// Marks an analyzer for scanning during IDE integration.
type EditorAnalyzerAttribute =
- new: [ obj)>] name: string -> EditorAnalyzerAttribute
+ new:
+ [ obj)>] name: string *
+ [ obj)>] shortDescription: string *
+ [ obj)>] helpUri: string ->
+ EditorAnalyzerAttribute
+
inherit AnalyzerAttribute
member Name: string
@@ -121,6 +138,20 @@ type Message =
type Analyzer<'TContext> = 'TContext -> Async
+type AnalyzerMessage =
+ {
+ /// A message produced by the analyzer.
+ Message: Message
+ /// Either the Name property used from the AnalyzerAttribute of the name or the function or member.
+ Name: string
+ /// Assembly the analyzer was found in.
+ AssemblyPath: string
+ /// Short description for the analyzer. Used in the sarif output.
+ ShortDescription: string option
+ /// A link to the documentation of this analyzer. Used in the sarif output.
+ HelpUri: string option
+ }
+
module Utils =
[]