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

Named analyzers and better error handling #16

Merged
merged 3 commits into from
Mar 8, 2020
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
20 changes: 14 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,34 @@ module BadCodeAnalyzer
open FSharp.Analyzers.SDK

[<Analyzer>]
let badCodeAnalyzer : Analyzer =
fun (context: Context) =
let badCodeAnalyzer : Analyzer =
fun (context: Context) ->
// inspect context to determine the error/warning messages
[ ]
```
Notice how we expose the function `BadCodeAnalyzer.badCodeAnalyzer` with an attribute `[<Analyzer>]` that allows the SDK to detect the function. The input `Context` is a record that contains information about a single F# file such as the typed AST, the AST, the file content, the file name and more. The SDK runs this function against all files of a project during editing. The output messages that come out of the function are eventually used by Ionide to highlight the inspected code as a warning or error depending on the `Severity` level of each message.
Notice how we expose the function `BadCodeAnalyzer.badCodeAnalyzer` with an attribute `[<Analyzer>]` that allows the SDK to detect the function. The input `Context` is a record that contains information about a single F# file such as the typed AST, the AST, the file content, the file name and more. The SDK runs this function against all files of a project during editing. The output messages that come out of the function are eventually used by Ionide to highlight the inspected code as a warning or error depending on the `Severity` level of each message.

Analyzers can also be named which allows for better logging if something went wrong while using the SDK from Ionide:
```fs
[<Analyzer "BadCodeAnalyzer">]
let badCodeAnalyzer : Analyzer =
fun (context: Context) ->
// inspect context to determine the error/warning messages
[ ]
```
### Analyzer Requirements

Analyzers are .NET core class libraries and they are distributed as such. However, since the SDK relies on dynamically loading the analyzers during runtime, there are some requirements to get them to work properly:
- The analyzer class library has to target the `netcoreapp2.0` framework
- The analyzer has to reference the latest `FSharp.Analyzers.SDK` (at least the version used by FsAutoComplete which is subsequently used by Ionide)

### Packaging and Distribution

Since analyzers are just .NET core libraries, you can distribute them to the nuget registry just like you would with a normal .NET package. Simply run `dotnet pack --configuration Release` against the analyzer project to get a nuget package and publish it with

```
dotnet nuget push {NugetPackageFullPath} -s nuget.org -k {NugetApiKey}
```
```

However, the story is different and slightly more complicated when your analyzer package has third-party dependencies also coming from nuget. Since the SDK dynamically loads the package assemblies (`.dll` files), the assemblies of the dependencies has be there *next* to the main assembly of the analyzer. Using `dotnet pack` will **not** include these dependencies into the output Nuget package. More specifically, the `./lib/netcoreapp2.0` directory of the nuget package must have all the required assemblies, also those from third-party packages. In order to package the analyzer properly with all the assemblies, you need to take the output you get from running:
```
Expand All @@ -64,7 +72,7 @@ Target.create "PackAnalyzer" (fun _ ->
sprintf "/p:PackageReleaseNotes=\"%s\"" (String.concat "\n" releaseNotes.Notes)
sprintf "--output %s" (__SOURCE_DIRECTORY__ </> "dist")
]

// create initial nuget package
let exitCode = Shell.Exec("dotnet", String.concat " " args, analyzerProject)
if exitCode <> 0 then
Expand Down
5 changes: 5 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
### 0.4.0 - 08.03.2020

* Allow for optional named analyzers via the attribute `[<Analyzer("AnalyzerName")>]`
* Add ability to get exact errors from running each individual analyzer

### 0.3.1 - 28.02.2020

* Update FCS version to 34.1.0
Expand Down
2 changes: 1 addition & 1 deletion paket.dependencies
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ source https://api.nuget.org/v3/index.json
source ./lib

nuget FSharp.Core ~> 4.5
nuget FSharp.Compiler.Service 34.1.0
nuget FSharp.Compiler.Service 34.1.1
nuget Argu
nuget Glob

Expand Down
12 changes: 6 additions & 6 deletions paket.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ NUGET
Argu (6.0)
FSharp.Core (>= 4.3.2) - restriction: >= netstandard2.0
System.Configuration.ConfigurationManager (>= 4.4) - restriction: >= netstandard2.0
FSharp.Compiler.Service (34.1)
FSharp.Compiler.Service (34.1.1)
FSharp.Core (>= 4.6.2) - restriction: || (>= net461) (>= netstandard2.0)
System.Buffers (>= 4.5) - restriction: || (>= net461) (>= netstandard2.0)
System.Collections.Immutable (>= 1.5) - restriction: || (>= net461) (>= netstandard2.0)
Expand Down Expand Up @@ -632,24 +632,24 @@ NUGET
FSharp.Core (>= 4.6.2) - restriction: || (>= net461) (>= netstandard2.0)
Microsoft.NETFramework.ReferenceAssemblies (>= 1.0) - restriction: || (>= net461) (>= netstandard2.0)
remote: paket-files/github.com/fsharp/FsAutoComplete/bin/pkgs
ProjectSystem (0.40.0)
ProjectSystem (0.40.1)
Dotnet.ProjInfo (>= 0.38) - restriction: || (>= net461) (>= netstandard2.0)
Dotnet.ProjInfo.Workspace.FCS (>= 0.38) - restriction: || (>= net461) (>= netstandard2.0)
FSharp.Compiler.Service (>= 34.1) - restriction: || (>= net461) (>= netstandard2.0)
FSharp.Compiler.Service (>= 34.1.1) - restriction: || (>= net461) (>= netstandard2.0)
Microsoft.NETFramework.ReferenceAssemblies (>= 1.0) - restriction: || (>= net461) (>= netstandard2.0)
Newtonsoft.Json (>= 12.0.3) - restriction: || (>= net461) (>= netstandard2.0)
Sln (>= 0.3) - restriction: || (>= net461) (>= netstandard2.0)
GIT
remote: https://github.com/fsharp/FsAutoComplete.git
(d849d7200382f5da2c0179c5a555783644cb7c0b)
(8fb00089e5af75bbdf1280e6cbfe2d0e25caea5c)
build: build.cmd
path: /bin/pkgs/
os: win
(d849d7200382f5da2c0179c5a555783644cb7c0b)
(8fb00089e5af75bbdf1280e6cbfe2d0e25caea5c)
build: build.sh
path: /bin/pkgs/
os: osx
(d849d7200382f5da2c0179c5a555783644cb7c0b)
(8fb00089e5af75bbdf1280e6cbfe2d0e25caea5c)
build: build.sh
path: /bin/pkgs/
os: linux
Expand Down
3 changes: 1 addition & 2 deletions samples/OptionAnalyzer/Library.fs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ let notUsed() =
let option : Option<int> = None
option.Value

[<Analyzer>]
[<Analyzer "OptionAnalyzer">]
let optionValueAnalyzer : Analyzer =
fun ctx ->
let state = ResizeArray<range>()
Expand All @@ -133,6 +133,5 @@ let optionValueAnalyzer : Analyzer =
Severity = Warning
Range = r
Fixes = []}

)
|> Seq.toList
47 changes: 32 additions & 15 deletions src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,21 @@ open System.Runtime.Loader
open McMaster.NETCore.Plugins
open System.Collections.Concurrent

type AnalysisResult = {
AnalyzerName : string
Output : Result<Message list, exn>
}

module Client =

let internal attributeName = "AnalyzerAttribute"

let internal isAnalyzer (mi: MemberInfo) =
mi.GetCustomAttributes true
|> Seq.exists (fun n -> n.GetType().Name = attributeName)
|> Seq.tryFind (fun n -> n.GetType().Name = attributeName)
|> Option.map unbox<AnalyzerAttribute>

let internal analyzerFromMember (mi: MemberInfo) : Analyzer option =
let internal analyzerFromMember (mi: MemberInfo) : (string * Analyzer) option =
let inline unboxAnalyzer v =
if isNull v then
failwith "Analyzer is null"
Expand All @@ -30,23 +36,22 @@ module Client =
then Some(m.Invoke(null, null) |> unboxAnalyzer)
elif m.ReturnType.FullName.StartsWith "Microsoft.FSharp.Collections.FSharpList`1[[FSharp.Analyzers.SDK.Message" then
try
let x : Analyzer = fun ctx ->
try
m.Invoke(null, [|ctx|]) |> unbox
with
| ex ->
printfn "Error while executing Analyzer from %s.%s" m.DeclaringType.Name m.Name
printfn "%A" ex
[]
Some x
let analyzer : Analyzer = fun ctx -> m.Invoke(null, [|ctx|]) |> unbox
Some analyzer
with
| ex -> None
else None
| :? PropertyInfo as m ->
if m.PropertyType = typeof<Analyzer> then Some(m.GetValue(null, null) |> unboxAnalyzer)
else None
| _ -> None
if isAnalyzer mi then getAnalyzerFromMemberInfo mi else None

match isAnalyzer mi with
| Some analyzerAttribute ->
match getAnalyzerFromMemberInfo mi with
| Some analyzer -> Some (analyzerAttribute.Name, analyzer)
| None -> None
| None -> None

let internal analyzersFromType (t: Type) =
let asMembers x = Seq.map (fun m -> m :> MemberInfo) x
Expand All @@ -61,7 +66,7 @@ module Client =
|> Seq.choose analyzerFromMember
|> Seq.toList

let registeredAnalyzers: ConcurrentDictionary<string, Analyzer list> = ConcurrentDictionary()
let registeredAnalyzers: ConcurrentDictionary<string, (string * Analyzer) list> = ConcurrentDictionary()

///Loads into private state any analyzers defined in any assembly
///matching `*Analyzer*.dll` in given directory (and any subdirectories)
Expand Down Expand Up @@ -97,9 +102,21 @@ module Client =
0,0

///Runs all registered analyzers for given context (file).
///Returns list of messages
///Returns list of messages. Ignores errors from the analyzers
let runAnalyzers (ctx: Context) : Message list =
let analyzers = registeredAnalyzers.Values |> Seq.collect id
analyzers
|> Seq.collect (fun analyzer -> analyzer ctx)
|> Seq.collect (fun (analyzerName, analyzer) -> try analyzer ctx with error -> [ ])
|> Seq.toList

/// Runs all registered analyzers for given context (file).
/// Returns list of results per analyzer which can ei
let runAnalyzersSafely (ctx: Context) : AnalysisResult list =
let analyzers = registeredAnalyzers.Values |> Seq.collect id
analyzers
|> Seq.map (fun (analyzerName, analyzer) ->
{
AnalyzerName = analyzerName
Output = try Ok (analyzer ctx) with error -> Result.Error error
})
|> Seq.toList
6 changes: 5 additions & 1 deletion src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ open System
open FSharp.Compiler
open FSharp.Compiler.Ast
open FSharp.Compiler.SourceCodeServices
open System.Runtime.InteropServices

/// Marks an analyzer for scanning
[<AttributeUsage(AttributeTargets.Method ||| AttributeTargets.Property ||| AttributeTargets.Field)>]
type AnalyzerAttribute() = inherit Attribute()
type AnalyzerAttribute([<Optional; DefaultParameterValue "Analyzer">] name: string) =
inherit Attribute()

member _.Name = name

type Context =
{ FileName: string
Expand Down