Skip to content

Commit 6108442

Browse files
authored
Merge pull request ionide#119 from nojaf/report
Add report flag to CLI tool
2 parents 3436522 + 36ed716 commit 6108442

8 files changed

+239
-36
lines changed

CHANGELOG.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
66
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.16.0] - 2023-10-16
9+
10+
### Added
11+
* [Analyzer report ](https://github.com/ionide/FSharp.Analyzers.SDK/issues/110) (thanks @nojaf!)
12+
813
## [0.15.0] - 2023-10-10
914

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

1419
## [0.14.1] - 2023-09-26
1520

Directory.Packages.props

+1
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@
2525
<PackageVersion Include="NUnit3TestAdapter" Version="4.4.2" />
2626
<PackageVersion Include="NUnit.Analyzers" Version="3.6.1" />
2727
<PackageVersion Include="coverlet.collector" Version="3.2.0" />
28+
<PackageVersion Include="Sarif.Sdk" Version="4.3.4" />
2829
</ItemGroup>
2930
</Project>

src/FSharp.Analyzers.Cli/FSharp.Analyzers.Cli.fsproj

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<PackageReference Include="Microsoft.Build.Locator" />
2626
<PackageReference Include="Microsoft.Build.Tasks.Core" ExcludeAssets="runtime" />
2727
<PackageReference Include="Microsoft.Build.Utilities.Core" ExcludeAssets="runtime" />
28+
<PackageReference Include="Sarif.Sdk" />
2829
</ItemGroup>
2930

3031
<ItemGroup>

src/FSharp.Analyzers.Cli/Program.fs

+97-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ open FSharp.Compiler.Text
55
open Argu
66
open FSharp.Analyzers.SDK
77
open GlobExpressions
8+
open Microsoft.CodeAnalysis.Sarif
9+
open Microsoft.CodeAnalysis.Sarif.Writers
810
open Ionide.ProjInfo
911

1012
type Arguments =
@@ -13,6 +15,7 @@ type Arguments =
1315
| Fail_On_Warnings of string list
1416
| Ignore_Files of string list
1517
| Exclude_Analyzer of string list
18+
| Report of string
1619
| Verbose
1720

1821
interface IArgParserTemplate with
@@ -24,6 +27,7 @@ type Arguments =
2427
"List of analyzer codes that should trigger tool failures in the presence of warnings."
2528
| Ignore_Files _ -> "Source files that shouldn't be processed."
2629
| Exclude_Analyzer _ -> "The names of analyzers that should not be executed."
30+
| Report _ -> "Write the result messages to a (sarif) report file."
2731
| Verbose -> "Verbose logging."
2832

2933
let mutable verbose = false
@@ -107,15 +111,17 @@ let runProject (client: Client<CliAnalyzerAttribute, CliContext>) toolsPath proj
107111
]
108112
}
109113

110-
let printMessages failOnWarnings (msgs: Message list) =
114+
let printMessages failOnWarnings (msgs: AnalyzerMessage list) =
111115
if verbose then
112116
printfn ""
113117

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

117121
msgs
118-
|> Seq.iter (fun m ->
122+
|> Seq.iter (fun analyzerMessage ->
123+
let m = analyzerMessage.Message
124+
119125
let color =
120126
match m.Severity with
121127
| Error -> ConsoleColor.Red
@@ -140,15 +146,97 @@ let printMessages failOnWarnings (msgs: Message list) =
140146

141147
msgs
142148

143-
let calculateExitCode failOnWarnings (msgs: Message list option) : int =
149+
let writeReport (results: AnalyzerMessage list option) (report: string) =
150+
try
151+
let driver = ToolComponent()
152+
driver.Name <- "Ionide.Analyzers.Cli"
153+
driver.InformationUri <- Uri("https://ionide.io/FSharp.Analyzers.SDK/")
154+
driver.Version <- string (System.Reflection.Assembly.GetExecutingAssembly().GetName().Version)
155+
let tool = Tool()
156+
tool.Driver <- driver
157+
let run = Run()
158+
run.Tool <- tool
159+
160+
use sarifLogger =
161+
new SarifLogger(
162+
report,
163+
logFilePersistenceOptions =
164+
(FilePersistenceOptions.PrettyPrint ||| FilePersistenceOptions.ForceOverwrite),
165+
run = run,
166+
levels = BaseLogger.ErrorWarningNote,
167+
kinds = BaseLogger.Fail,
168+
closeWriterOnDispose = true
169+
)
170+
171+
sarifLogger.AnalysisStarted()
172+
173+
for analyzerResult in (Option.defaultValue List.empty results) do
174+
let reportDescriptor = ReportingDescriptor()
175+
reportDescriptor.Id <- analyzerResult.Message.Code
176+
reportDescriptor.Name <- analyzerResult.Message.Message
177+
178+
analyzerResult.ShortDescription
179+
|> Option.iter (fun shortDescription ->
180+
reportDescriptor.ShortDescription <-
181+
MultiformatMessageString(shortDescription, shortDescription, dict [])
182+
)
183+
184+
analyzerResult.HelpUri
185+
|> Option.iter (fun helpUri -> reportDescriptor.HelpUri <- Uri(helpUri))
186+
187+
let result = Result()
188+
result.RuleId <- reportDescriptor.Id
189+
190+
result.Level <-
191+
match analyzerResult.Message.Severity with
192+
| Info -> FailureLevel.Note
193+
| Hint -> FailureLevel.None
194+
| Warning -> FailureLevel.Warning
195+
| Error -> FailureLevel.Error
196+
197+
let msg = Message()
198+
msg.Text <- analyzerResult.Message.Message
199+
result.Message <- msg
200+
201+
let physicalLocation = PhysicalLocation()
202+
203+
physicalLocation.ArtifactLocation <-
204+
let al = ArtifactLocation()
205+
al.Uri <- Uri(analyzerResult.Message.Range.FileName)
206+
al
207+
208+
physicalLocation.Region <-
209+
let r = Region()
210+
r.StartLine <- analyzerResult.Message.Range.StartLine
211+
r.StartColumn <- analyzerResult.Message.Range.StartColumn
212+
r.EndLine <- analyzerResult.Message.Range.EndLine
213+
r.EndColumn <- analyzerResult.Message.Range.EndColumn
214+
r
215+
216+
let location: Location = Location()
217+
location.PhysicalLocation <- physicalLocation
218+
result.Locations <- [| location |]
219+
220+
sarifLogger.Log(reportDescriptor, result, System.Nullable())
221+
222+
sarifLogger.AnalysisStopped(RuntimeConditions.None)
223+
224+
sarifLogger.Dispose()
225+
with ex ->
226+
let details = if not verbose then "" else $" %s{ex.Message}"
227+
printfn $"Could not write sarif to %s{report}%s{details}"
228+
229+
let calculateExitCode failOnWarnings (msgs: AnalyzerMessage list option) : int =
144230
match msgs with
145231
| None -> -1
146232
| Some msgs ->
147233
let check =
148234
msgs
149-
|> List.exists (fun n ->
150-
n.Severity = Error
151-
|| (n.Severity = Warning && failOnWarnings |> List.contains n.Code)
235+
|> List.exists (fun analyzerMessage ->
236+
let message = analyzerMessage.Message
237+
238+
message.Severity = Error
239+
|| (message.Severity = Warning && failOnWarnings |> List.contains message.Code)
152240
)
153241

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

199287
let projOpts = results.TryGetResult <@ Project @>
288+
let report = results.TryGetResult <@ Report @>
200289

201290
let results =
202291
if analyzers = 0 then
@@ -231,4 +320,6 @@ let main argv =
231320
|> List.concat
232321
|> Some
233322

323+
report |> Option.iter (writeReport results)
324+
234325
calculateExitCode failOnWarnings results

src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fs

+60-18
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,25 @@ type AnalysisResult =
1616

1717
module Client =
1818

19+
type RegisteredAnalyzer<'TContext when 'TContext :> Context> =
20+
{
21+
AssemblyPath: string
22+
Name: string
23+
Analyzer: Analyzer<'TContext>
24+
ShortDescription: string option
25+
HelpUri: string option
26+
}
27+
1928
let isAnalyzer<'TAttribute when 'TAttribute :> AnalyzerAttribute> (mi: MemberInfo) =
2029
mi.GetCustomAttributes true
2130
|> Seq.tryFind (fun n -> n.GetType().Name = typeof<'TAttribute>.Name)
2231
|> Option.map unbox<'TAttribute>
2332

24-
let analyzerFromMember<'TAnalyzerAttribute, 'TContext when 'TAnalyzerAttribute :> AnalyzerAttribute>
33+
let analyzerFromMember<'TAnalyzerAttribute, 'TContext
34+
when 'TAnalyzerAttribute :> AnalyzerAttribute and 'TContext :> Context>
35+
(path: string)
2536
(mi: MemberInfo)
26-
: (string * Analyzer<'TContext>) option
37+
: RegisteredAnalyzer<'TContext> option
2738
=
2839
let inline unboxAnalyzer v =
2940
if isNull v then failwith "Analyzer is null" else unbox v
@@ -75,13 +86,30 @@ module Client =
7586
match isAnalyzer<'TAnalyzerAttribute> mi with
7687
| Some analyzerAttribute ->
7788
match getAnalyzerFromMemberInfo mi with
78-
| Some analyzer -> Some(analyzerAttribute.Name, analyzer)
89+
| Some analyzer ->
90+
let name =
91+
if String.IsNullOrWhiteSpace analyzerAttribute.Name then
92+
mi.Name
93+
else
94+
analyzerAttribute.Name
95+
96+
Some
97+
{
98+
AssemblyPath = path
99+
Name = name
100+
Analyzer = analyzer
101+
ShortDescription = analyzerAttribute.ShortDescription
102+
HelpUri = analyzerAttribute.HelpUri
103+
}
104+
79105
| None -> None
80106
| None -> None
81107

82-
let analyzersFromType<'TAnalyzerAttribute, 'TContext when 'TAnalyzerAttribute :> AnalyzerAttribute>
108+
let analyzersFromType<'TAnalyzerAttribute, 'TContext
109+
when 'TAnalyzerAttribute :> AnalyzerAttribute and 'TContext :> Context>
110+
(path: string)
83111
(t: Type)
84-
: (string * Analyzer<'TContext>) list
112+
: RegisteredAnalyzer<'TContext> list
85113
=
86114
let asMembers x = Seq.map (fun m -> m :> MemberInfo) x
87115
let bindingFlags = BindingFlags.Public ||| BindingFlags.Static
@@ -95,7 +123,7 @@ module Client =
95123
|> Seq.collect id
96124

97125
members
98-
|> Seq.choose analyzerFromMember<'TAnalyzerAttribute, 'TContext>
126+
|> Seq.choose (analyzerFromMember<'TAnalyzerAttribute, 'TContext> path)
99127
|> Seq.toList
100128

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

112140
new() =
113141
Client(
@@ -169,12 +197,12 @@ type Client<'TAttribute, 'TContext when 'TAttribute :> AnalyzerAttribute and 'TC
169197
|> Array.map (fun (path, assembly) ->
170198
let analyzers =
171199
assembly.GetExportedTypes()
172-
|> Seq.collect Client.analyzersFromType<'TAttribute, 'TContext>
173-
|> Seq.filter (fun (analyzerName, _) ->
174-
let shouldExclude = excludedAnalyzers.Contains(analyzerName)
200+
|> Seq.collect (Client.analyzersFromType<'TAttribute, 'TContext> path)
201+
|> Seq.filter (fun registeredAnalyzer ->
202+
let shouldExclude = excludedAnalyzers.Contains(registeredAnalyzer.Name)
175203

176204
if shouldExclude then
177-
logger.Verbose $"Excluding %s{analyzerName} from %s{assembly.FullName}"
205+
logger.Verbose $"Excluding %s{registeredAnalyzer.Name} from %s{assembly.FullName}"
178206

179207
not shouldExclude
180208
)
@@ -191,15 +219,29 @@ type Client<'TAttribute, 'TContext when 'TAttribute :> AnalyzerAttribute and 'TC
191219
else
192220
0, 0
193221

194-
member x.RunAnalyzers(ctx: 'TContext) : Async<Message list> =
222+
member x.RunAnalyzers(ctx: 'TContext) : Async<AnalyzerMessage list> =
195223
async {
196224
let analyzers = registeredAnalyzers.Values |> Seq.collect id
197225

198226
let! messagesPerAnalyzer =
199227
analyzers
200-
|> Seq.map (fun (_analyzerName, analyzer) ->
228+
|> Seq.map (fun registeredAnalyzer ->
201229
try
202-
analyzer ctx
230+
async {
231+
let! messages = registeredAnalyzer.Analyzer ctx
232+
233+
return
234+
messages
235+
|> List.map (fun message ->
236+
{
237+
Message = message
238+
Name = registeredAnalyzer.Name
239+
AssemblyPath = registeredAnalyzer.AssemblyPath
240+
ShortDescription = registeredAnalyzer.ShortDescription
241+
HelpUri = registeredAnalyzer.HelpUri
242+
}
243+
)
244+
}
203245
with error ->
204246
async.Return []
205247
)
@@ -218,20 +260,20 @@ type Client<'TAttribute, 'TContext when 'TAttribute :> AnalyzerAttribute and 'TC
218260

219261
let! results =
220262
analyzers
221-
|> Seq.map (fun (analyzerName, analyzer) ->
263+
|> Seq.map (fun registeredAnalyzer ->
222264
async {
223265
try
224-
let! result = analyzer ctx
266+
let! result = registeredAnalyzer.Analyzer ctx
225267

226268
return
227269
{
228-
AnalyzerName = analyzerName
270+
AnalyzerName = registeredAnalyzer.Name
229271
Output = Result.Ok result
230272
}
231273
with error ->
232274
return
233275
{
234-
AnalyzerName = analyzerName
276+
AnalyzerName = registeredAnalyzer.Name
235277
Output = Result.Error error
236278
}
237279
}

src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fsi

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ type Client<'TAttribute, 'TContext when 'TAttribute :> AnalyzerAttribute and 'TC
2222
member LoadAnalyzers: dir: string -> int * int
2323
/// <summary>Runs all registered analyzers for given context (file).</summary>
2424
/// <returns>list of messages. Ignores errors from the analyzers</returns>
25-
member RunAnalyzers: ctx: 'TContext -> Async<Message list>
25+
member RunAnalyzers: ctx: 'TContext -> Async<AnalyzerMessage list>
2626
/// <summary>Runs all registered analyzers for given context (file).</summary>
2727
/// <returns>list of results per analyzer which can either be messages or an exception.</returns>
2828
member RunAnalyzersSafely: ctx: 'TContext -> Async<AnalysisResult list>

0 commit comments

Comments
 (0)