diff --git a/CHANGELOG.md b/CHANGELOG.md index d0972a2..f54fc99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ 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). +## [Unreleased] + +### Added +* [Exclude analyzers](https://github.com/ionide/FSharp.Analyzers.SDK/issues/112) (thanks @nojaf) + ## [0.14.1] - 2023-09-26 ### Changed diff --git a/docs/content/Programmatic access.fsx b/docs/content/Programmatic access.fsx index 11eaf5b..c8efac7 100644 --- a/docs/content/Programmatic access.fsx +++ b/docs/content/Programmatic access.fsx @@ -20,7 +20,7 @@ The `Client` needs to know what type of analyzer you intend to load: *console* o open FSharp.Analyzers.SDK let client = Client() -let countLoaded = client.LoadAnalyzers ignore @"C:\MyAnalyzers" +let countLoaded = client.LoadAnalyzers @"C:\MyAnalyzers" let ctx = Unchecked.defaultof // Construct your context... client.RunAnalyzers(ctx) diff --git a/src/FSharp.Analyzers.Cli/Program.fs b/src/FSharp.Analyzers.Cli/Program.fs index 72cd09c..556fbb0 100644 --- a/src/FSharp.Analyzers.Cli/Program.fs +++ b/src/FSharp.Analyzers.Cli/Program.fs @@ -12,10 +12,19 @@ type Arguments = | Analyzers_Path of string | Fail_On_Warnings of string list | Ignore_Files of string list + | Exclude_Analyzer of string list | Verbose interface IArgParserTemplate with - member s.Usage = "" + member s.Usage = + match s with + | Project _ -> "Path to your .fsproj file." + | Analyzers_Path _ -> "Path to a folder where your analyzers are located." + | Fail_On_Warnings _ -> + "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." + | Verbose -> "Verbose logging." let mutable verbose = false @@ -169,9 +178,21 @@ let main argv = printInfo "Loading analyzers from %s" analyzersPath - let client = Client() + let excludeAnalyzers = results.GetResult(<@ Exclude_Analyzer @>, []) - let dlls, analyzers = client.LoadAnalyzers (printError "%s") analyzersPath + let logger = + { new Logger with + member _.Error msg = printError "%s" msg + + member _.Verbose msg = + if verbose then + printInfo "%s" msg + } + + let client = + Client(logger, Set.ofList excludeAnalyzers) + + let dlls, analyzers = client.LoadAnalyzers analyzersPath printInfo "Registered %d analyzers from %d dlls" analyzers dlls diff --git a/src/FSharp.Analyzers.SDK.Testing/FSharp.Analyzers.SDK.Testing.fs b/src/FSharp.Analyzers.SDK.Testing/FSharp.Analyzers.SDK.Testing.fs index 83362c8..99aa6a0 100644 --- a/src/FSharp.Analyzers.SDK.Testing/FSharp.Analyzers.SDK.Testing.fs +++ b/src/FSharp.Analyzers.SDK.Testing/FSharp.Analyzers.SDK.Testing.fs @@ -218,12 +218,11 @@ let getContext (opts: FSharpProjectOptions) source = Map.tryFind fileName files |> async.Return let fcs = Utils.createFCS (Some documentSource) - let printError (s: string) = Console.WriteLine(s) let pathToAnalyzerDlls = Path.GetFullPath(".") let foundDlls, registeredAnalyzers = let client = Client() - client.LoadAnalyzers printError pathToAnalyzerDlls + client.LoadAnalyzers pathToAnalyzerDlls if foundDlls = 0 then failwith $"no Dlls found in {pathToAnalyzerDlls}" diff --git a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fs b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fs index 4f96a53..09592af 100644 --- a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fs +++ b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fs @@ -31,8 +31,10 @@ module Client = let hasExpectReturnType (t: Type) = // t might be a System.RunTimeType as could have no FullName if not (isNull t.FullName) then - t.FullName.StartsWith - "Microsoft.FSharp.Control.FSharpAsync`1[[Microsoft.FSharp.Collections.FSharpList`1[[FSharp.Analyzers.SDK.Message" + t.FullName.StartsWith( + "Microsoft.FSharp.Control.FSharpAsync`1[[Microsoft.FSharp.Collections.FSharpList`1[[FSharp.Analyzers.SDK.Message", + StringComparison.InvariantCulture + ) elif t.Name = "FSharpAsync`1" && t.GenericTypeArguments.Length = 1 then let listType = t.GenericTypeArguments.[0] @@ -96,19 +98,39 @@ module Client = |> Seq.choose analyzerFromMember<'TAnalyzerAttribute, 'TContext> |> Seq.toList -type Client<'TAttribute, 'TContext when 'TAttribute :> AnalyzerAttribute and 'TContext :> Context>() = +[] +type Logger = + abstract member Error: string -> unit + abstract member Verbose: string -> unit + +type Client<'TAttribute, 'TContext when 'TAttribute :> AnalyzerAttribute and 'TContext :> Context> + (logger: Logger, excludedAnalyzers: string Set) + = let registeredAnalyzers = ConcurrentDictionary) list>() - member x.LoadAnalyzers (printError: string -> unit) (dir: string) : int * int = + new() = + Client( + { new Logger with + member this.Error _ = () + member this.Verbose _ = () + }, + Set.empty + ) + + member x.LoadAnalyzers(dir: string) : int * int = if Directory.Exists dir then let analyzerAssemblies = let regex = Regex(@".*test.*\.dll$") Directory.GetFiles(dir, "*Analyzer*.dll", SearchOption.AllDirectories) |> Array.filter (fun a -> - let s = Path.GetFileName(a).ToLowerInvariant() - not (s.EndsWith("fsharp.analyzers.sdk.dll") || regex.IsMatch(s)) + let s = Path.GetFileName(a) + + not ( + s.EndsWith("fsharp.analyzers.sdk.dll", StringComparison.InvariantCultureIgnoreCase) + || regex.IsMatch(s) + ) ) |> Array.choose (fun analyzerDll -> try @@ -139,7 +161,7 @@ type Client<'TAttribute, 'TContext when 'TAttribute :> AnalyzerAttribute and 'TC if version = Utils.currentFSharpAnalyzersSDKVersion then true else - printError + logger.Error $"Trying to load %s{name} which was built using SDK version %A{version}. Expect %A{Utils.currentFSharpAnalyzersSDKVersion} instead. Assembly will be skipped." false @@ -148,13 +170,20 @@ type Client<'TAttribute, 'TContext when 'TAttribute :> AnalyzerAttribute and 'TC let analyzers = assembly.GetExportedTypes() |> Seq.collect Client.analyzersFromType<'TAttribute, 'TContext> + |> Seq.filter (fun (analyzerName, _) -> + let shouldExclude = excludedAnalyzers.Contains(analyzerName) + + if shouldExclude then + logger.Verbose $"Excluding %s{analyzerName} from %s{assembly.FullName}" + + not shouldExclude + ) + |> Seq.toList path, analyzers ) for path, analyzers in analyzers do - let analyzers = Seq.toList analyzers - registeredAnalyzers.AddOrUpdate(path, analyzers, (fun _ _ -> analyzers)) |> ignore diff --git a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fsi b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fsi index da047ef..3cd0d1d 100644 --- a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fsi +++ b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fsi @@ -6,14 +6,20 @@ type AnalysisResult = Output: Result } +[] +type Logger = + abstract member Error: string -> unit + abstract member Verbose: string -> unit + type Client<'TAttribute, 'TContext when 'TAttribute :> AnalyzerAttribute and 'TContext :> Context> = + new: logger: Logger * excludedAnalyzers: string Set -> Client<'TAttribute, 'TContext> new: unit -> Client<'TAttribute, 'TContext> /// /// Loads into private state any analyzers defined in any assembly /// matching `*Analyzer*.dll` in given directory (and any subdirectories) /// /// number of found dlls matching `*Analyzer*.dll` and number of registered analyzers - member LoadAnalyzers: printError: (string -> unit) -> dir: string -> int * int + 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