diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0cb5752..d2aca02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,10 +3,10 @@ name: CI on: push: branches: - - master + - main pull_request: branches: - - master + - main permissions: contents: read @@ -28,7 +28,7 @@ jobs: run: dotnet fsi build.fsx - name: Upload documentation - if: github.ref == 'refs/heads/master' + if: github.ref == 'refs/heads/main' uses: actions/upload-pages-artifact@v1 with: path: ./output @@ -36,7 +36,7 @@ jobs: deploy: runs-on: ubuntu-latest needs: ci - if: github.ref == 'refs/heads/master' + if: github.ref == 'refs/heads/main' steps: - name: Deploy to GitHub Pages id: deployment diff --git a/CHANGELOG.md b/CHANGELOG.md index f5271e7..af5556b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Changed * [Add path to ASTCollecting](https://github.com/ionide/FSharp.Analyzers.SDK/pull/171) (thanks @nojaf!) +* [Use Microsoft.Extensions.Logging instead of printf based logging infrastructure](https://github.com/ionide/FSharp.Analyzers.SDK/pull/175) (thanks @dawedawe!) ## [0.21.0] - 2023-11-22 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index adeeb6a..873ed3d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,7 +27,7 @@ Guidelines for bug reports: reported. 2. **Check if the issue has been fixed** — try to reproduce it using the - `master` branch in the repository. + `main` branch in the repository. 3. **Isolate and report the problem** — ideally create a reduced test case. @@ -97,16 +97,16 @@ in order to craft an excellent pull request: 2. If you cloned a while ago, get the latest changes from upstream, and update your fork: ```bash - git checkout master - git pull upstream master + git checkout main + git pull upstream main git push ``` -3. Create a new topic branch (off of `master`) to contain your feature, change, +3. Create a new topic branch (off of `main`) to contain your feature, change, or fix. - **IMPORTANT**: Making changes in `master` is discouraged. You should always - keep your local `master` in sync with upstream `master` and make your + **IMPORTANT**: Making changes in `main` is discouraged. You should always + keep your local `main` in sync with upstream `main` and make your changes in topic branches. ```bash @@ -135,17 +135,17 @@ in order to craft an excellent pull request: with a clear title and description. 8. If you haven't updated your pull request for a while, you should consider - rebasing on master and resolving any conflicts. + rebasing on main and resolving any conflicts. - **IMPORTANT**: _Never ever_ merge upstream `master` into your branches. You - should always `git rebase` on `master` to bring your changes up to date when + **IMPORTANT**: _Never ever_ merge upstream `main` into your branches. You + should always `git rebase` on `main` to bring your changes up to date when necessary. ```bash - git checkout master - git pull upstream master + git checkout main + git pull upstream main git checkout - git rebase master + git rebase main ``` @@ -159,4 +159,4 @@ in order to craft an excellent pull request: 3. Make a new version tag (for example, `v0.45.0`) 1. `git tag v0.45.0` 4. Push changes to the repo. - 1. `git push --atomic [insert-remote-branch] master v0.45.0` + 1. `git push --atomic [insert-remote-branch] main v0.45.0` diff --git a/Directory.Build.props b/Directory.Build.props index c7bc60e..f5e9e35 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -14,8 +14,8 @@ - https://github.com/ionide/FSharp.Analyzers.SDK/blob/master/LICENSE.md - https://github.com/ionide/FSharp.Analyzers.SDK/blob/master/CHANGELOG.md + https://github.com/ionide/FSharp.Analyzers.SDK/blob/main/LICENSE.md + https://github.com/ionide/FSharp.Analyzers.SDK/blob/main/CHANGELOG.md https://github.com/ionide/FSharp.Analyzers.SDK diff --git a/Directory.Packages.props b/Directory.Packages.props index 2aeb53c..5aa4b90 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,6 +19,9 @@ + + + diff --git a/README.md b/README.md index ae9634f..3459b99 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ F# analyzers are live, real-time, project based plugins that enables to diagnose 2. Run the console application: ```shell -dotnet run --project src\FSharp.Analyzers.Cli\FSharp.Analyzers.Cli.fsproj -- --project ./samples/OptionAnalyzer/OptionAnalyzer.fsproj --analyzers-path ./samples/OptionAnalyzer/bin/Release --verbose +dotnet run --project src\FSharp.Analyzers.Cli\FSharp.Analyzers.Cli.fsproj -- --project ./samples/OptionAnalyzer/OptionAnalyzer.fsproj --analyzers-path ./samples/OptionAnalyzer/bin/Release --verbosity d ``` You can also set up a run configuration of FSharp.Analyzers.Cli in your favorite IDE using similar arguments. This also allows you to debug FSharp.Analyzers.Cli. @@ -36,7 +36,7 @@ There might be a little voice inside that tells you you're not ready; that you n I assure you, that's not the case. -This project has some clear Contribution Guidelines and expectations that you can [read here](https://github.com/Krzysztof-Cieslak/FSharp.Analyzers.SDK/blob/master/CONTRIBUTING.md). +This project has some clear Contribution Guidelines and expectations that you can [read here](https://github.com/Krzysztof-Cieslak/FSharp.Analyzers.SDK/blob/main/CONTRIBUTING.md). The contribution guidelines outline the process that you'll need to follow to get a patch merged. By making expectations and process explicit, I hope it will make it easier for you to contribute. @@ -50,4 +50,4 @@ Thank you for contributing! The project is hosted on [GitHub](https://github.com/Krzysztof-Cieslak/FSharp.Analyzers.SDK) where you can [report issues](https://github.com/Krzysztof-Cieslak/FSharp.Analyzers.SDK/issues), fork the project and submit pull requests. -The library is available under [MIT license](https://github.com/Krzysztof-Cieslak/FSharp.Analyzers.SDK/blob/master/LICENSE.md), which allows modification and redistribution for both commercial and non-commercial purposes. +The library is available under [MIT license](https://github.com/Krzysztof-Cieslak/FSharp.Analyzers.SDK/blob/main/LICENSE.md), which allows modification and redistribution for both commercial and non-commercial purposes. diff --git a/build.fsx b/build.fsx index e8d3a57..17f13d4 100644 --- a/build.fsx +++ b/build.fsx @@ -28,7 +28,7 @@ pipeline "Build" { } stage "sample" { run - "dotnet run --project src/FSharp.Analyzers.Cli/FSharp.Analyzers.Cli.fsproj -- --project ./samples/OptionAnalyzer/OptionAnalyzer.fsproj --analyzers-path ./samples/OptionAnalyzer/bin/Release --verbose" + "dotnet run --project src/FSharp.Analyzers.Cli/FSharp.Analyzers.Cli.fsproj -- --project ./samples/OptionAnalyzer/OptionAnalyzer.fsproj --analyzers-path ./samples/OptionAnalyzer/bin/Release --verbosity d" } stage "docs" { run "dotnet fsdocs build --properties Configuration=Release --eval --clean --strict" } runIfOnlySpecified false diff --git a/docs/content/Getting Started Using.md b/docs/content/Getting Started Using.md index 06c8671..c309e05 100644 --- a/docs/content/Getting Started Using.md +++ b/docs/content/Getting Started Using.md @@ -33,7 +33,7 @@ At the time of writing, the [G-Research analyzers](https://github.com/g-research With the package downloaded, we can run the CLI tool: ```shell -dotnet fsharp-analyzers --project ./YourProject.fsproj --analyzers-path C:\Users\yourusername\.nuget\packages\g-research.fsharp.analyzers\0.4.0\analyzers\dotnet\fs\ --verbose +dotnet fsharp-analyzers --project ./YourProject.fsproj --analyzers-path C:\Users\yourusername\.nuget\packages\g-research.fsharp.analyzers\0.4.0\analyzers\dotnet\fs\ --verbosity d ``` ### Using an MSBuild target @@ -57,7 +57,7 @@ Before we can run `dotnet msbuild /t:AnalyzeFSharpProject`, we need to specify o ```xml - --analyzers-path "$(PkgG-Research_FSharp_Analyzers)/analyzers/dotnet/fs" --report "$(MSBuildProjectName)-$(TargetFramework).sarif" --treat-as-warning IONIDE-004 --verbose + --analyzers-path "$(PkgG-Research_FSharp_Analyzers)/analyzers/dotnet/fs" --report "$(MSBuildProjectName)-$(TargetFramework).sarif" --treat-as-warning IONIDE-004 --verbosity d ``` @@ -101,7 +101,7 @@ This is effectively the same as adding a property to each `*proj` file which exi ./ . - --analyzers-path "$(PkgG-Research_FSharp_Analyzers)/analyzers/dotnet/fs" --report "$(SarifOutput)$(MSBuildProjectName)-$(TargetFramework).sarif" --code-root $(CodeRoot) --treat-as-warning IONIDE-004 --verbose + --analyzers-path "$(PkgG-Research_FSharp_Analyzers)/analyzers/dotnet/fs" --report "$(SarifOutput)$(MSBuildProjectName)-$(TargetFramework).sarif" --code-root $(CodeRoot) --treat-as-warning IONIDE-004 --verbosity d ``` diff --git a/docs/content/Getting Started Writing.fsx b/docs/content/Getting Started Writing.fsx index 63eb0a1..c1cdde9 100644 --- a/docs/content/Getting Started Writing.fsx +++ b/docs/content/Getting Started Writing.fsx @@ -108,7 +108,7 @@ dotnet tool install --global fsharp-analyzers ``` ```shell -fsharp-analyzers --project YourProject.fsproj --analyzers-path ./OptionAnalyzer/bin/Release --verbose +fsharp-analyzers --project YourProject.fsproj --analyzers-path ./OptionAnalyzer/bin/Release --verbosity d ``` ### Packaging and Distribution @@ -190,6 +190,13 @@ Target.create ) (** + +### Known footguns to avoid + +There's a footgun in the FCS-API that you can easily trigger when working on an analyzer: +Accessing the [FullName](https://fsharp.github.io/fsharp-compiler-docs/reference/fsharp-compiler-symbols-fsharpentity.html#FullName) property of the [FSharpEntity](https://fsharp.github.io/fsharp-compiler-docs/reference/fsharp-compiler-symbols-fsharpentity.html) type throws an exception if the entity doesn't have one. +Use the [TryGetFullName](https://fsharp.github.io/fsharp-compiler-docs/reference/fsharp-compiler-symbols-fsharpentity.html#TryGetFullName) function for safe access. + [Previous]({{fsdocs-previous-page-link}}) [Next]({{fsdocs-next-page-link}}) diff --git a/docs/content/Programmatic access.fsx b/docs/content/Programmatic access.fsx index d1ebffb..76a86e3 100644 --- a/docs/content/Programmatic access.fsx +++ b/docs/content/Programmatic access.fsx @@ -15,6 +15,7 @@ The `Client` needs to know what type of analyzer you intend to load: *console* o (*** hide ***) #r "../../src/FSharp.Analyzers.Cli/bin/Release/net6.0/FSharp.Analyzers.SDK.dll" #r "../../src/FSharp.Analyzers.Cli/bin/Release/net6.0/FSharp.Compiler.Service.dll" +#r "../../src/FSharp.Analyzers.Cli/bin/Release/net6.0/Microsoft.Extensions.Logging.Abstractions.dll" (** *) open FSharp.Analyzers.SDK diff --git a/docs/index.md b/docs/index.md index 592f0e1..ed1c374 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,7 +15,7 @@ F# analyzers are live, real-time, project based plugins that enables to diagnose 2. Run the console application: ```shell -dotnet run --project src\FSharp.Analyzers.Cli\FSharp.Analyzers.Cli.fsproj -- --project ./samples/OptionAnalyzer/OptionAnalyzer.fsproj --analyzers-path ./samples/OptionAnalyzer/bin/Release --verbose +dotnet run --project src\FSharp.Analyzers.Cli\FSharp.Analyzers.Cli.fsproj -- --project ./samples/OptionAnalyzer/OptionAnalyzer.fsproj --analyzers-path ./samples/OptionAnalyzer/bin/Release --verbosity d ``` You can also set up a run configuration of FSharp.Analyzers.Cli in your favorite IDE using similar arguments. This also allows you to debug FSharp.Analyzers.Cli. @@ -36,7 +36,7 @@ There might be a little voice inside that tells you you're not ready; that you n I assure you, that's not the case. -This project has some clear Contribution Guidelines and expectations that you can [read here](https://github.com/Krzysztof-Cieslak/FSharp.Analyzers.SDK/blob/master/CONTRIBUTING.md). +This project has some clear Contribution Guidelines and expectations that you can [read here](https://github.com/Krzysztof-Cieslak/FSharp.Analyzers.SDK/blob/main/CONTRIBUTING.md). The contribution guidelines outline the process that you'll need to follow to get a patch merged. By making expectations and process explicit, I hope it will make it easier for you to contribute. @@ -50,6 +50,6 @@ Thank you for contributing! The project is hosted on [GitHub](https://github.com/Krzysztof-Cieslak/FSharp.Analyzers.SDK) where you can [report issues](https://github.com/Krzysztof-Cieslak/FSharp.Analyzers.SDK/issues), fork the project and submit pull requests. -The library is available under [MIT license](https://github.com/Krzysztof-Cieslak/FSharp.Analyzers.SDK/blob/master/LICENSE.md), which allows modification and redistribution for both commercial and non-commercial purposes. +The library is available under [MIT license](https://github.com/Krzysztof-Cieslak/FSharp.Analyzers.SDK/blob/main/LICENSE.md), which allows modification and redistribution for both commercial and non-commercial purposes. [Next]({{fsdocs-next-page-link}}) diff --git a/src/FSharp.Analyzers.Cli/CustomLogging.fs b/src/FSharp.Analyzers.Cli/CustomLogging.fs new file mode 100644 index 0000000..60011d7 --- /dev/null +++ b/src/FSharp.Analyzers.Cli/CustomLogging.fs @@ -0,0 +1,80 @@ +module FSharp.Analyzers.Cli.CustomLogging + +open System +open System.IO +open System.Runtime.CompilerServices +open Microsoft.Extensions.Logging +open Microsoft.Extensions.Logging.Console +open Microsoft.Extensions.Logging.Abstractions +open Microsoft.Extensions.Options + +type CustomOptions() = + inherit ConsoleFormatterOptions() + + /// if true: no LogLevel as prefix, colored output according to LogLevel + /// if false: LogLevel as prefix, no colored output + member val UseAnalyzersMsgStyle = false with get, set + +type CustomFormatter(options: IOptionsMonitor) as this = + inherit ConsoleFormatter("customName") + + let mutable optionsReloadToken: IDisposable = null + let mutable formatterOptions = options.CurrentValue + let origColor = Console.ForegroundColor + + do optionsReloadToken <- options.OnChange(fun x -> this.ReloadLoggerOptions(x)) + + member private _.ReloadLoggerOptions(opts: CustomOptions) = formatterOptions <- opts + + override this.Write<'TState> + ( + logEntry: inref>, + _scopeProvider: IExternalScopeProvider, + textWriter: TextWriter + ) + = + let message = logEntry.Formatter.Invoke(logEntry.State, logEntry.Exception) + + if formatterOptions.UseAnalyzersMsgStyle then + this.SetColor(textWriter, logEntry.LogLevel) + textWriter.WriteLine(message) + this.ResetColor() + else + this.WritePrefix(textWriter, logEntry.LogLevel) + textWriter.WriteLine(message) + + member private _.WritePrefix(textWriter: TextWriter, logLevel: LogLevel) = + match logLevel with + | LogLevel.Trace -> textWriter.Write("trace: ") + | LogLevel.Debug -> textWriter.Write("debug: ") + | LogLevel.Information -> textWriter.Write("info: ") + | LogLevel.Warning -> textWriter.Write("warn: ") + | LogLevel.Error -> textWriter.Write("error: ") + | LogLevel.Critical -> textWriter.Write("critical: ") + | _ -> () + + // see https://learn.microsoft.com/en-us/dotnet/core/extensions/console-log-formatter + member private _.SetColor(textWriter: TextWriter, logLevel: LogLevel) = + let color = + match logLevel with + | LogLevel.Error -> "\x1B[1m\x1B[31m" // ConsoleColor.Red + | LogLevel.Warning -> "\x1B[33m" // ConsoleColor.DarkYellow + | LogLevel.Information -> "\x1B[1m\x1B[34m" // ConsoleColor.Blue + | LogLevel.Trace -> "\x1B[1m\x1B[36m" // ConsoleColor.Cyan + | _ -> "\x1B[37m" // ConsoleColor.Gray + + textWriter.Write(color) + + member private _.ResetColor() = Console.ForegroundColor <- origColor + + interface IDisposable with + member _.Dispose() = optionsReloadToken.Dispose() + +[] +type ConsoleLoggerExtensions = + + [] + static member AddCustomFormatter(builder: ILoggingBuilder, configure: Action) : ILoggingBuilder = + builder + .AddConsole(fun options -> options.FormatterName <- "customName") + .AddConsoleFormatter(configure) diff --git a/src/FSharp.Analyzers.Cli/FSharp.Analyzers.Cli.fsproj b/src/FSharp.Analyzers.Cli/FSharp.Analyzers.Cli.fsproj index 62eecb6..eb2711a 100644 --- a/src/FSharp.Analyzers.Cli/FSharp.Analyzers.Cli.fsproj +++ b/src/FSharp.Analyzers.Cli/FSharp.Analyzers.Cli.fsproj @@ -13,6 +13,7 @@ + @@ -26,6 +27,7 @@ + diff --git a/src/FSharp.Analyzers.Cli/Program.fs b/src/FSharp.Analyzers.Cli/Program.fs index dda7a67..f3e39ce 100644 --- a/src/FSharp.Analyzers.Cli/Program.fs +++ b/src/FSharp.Analyzers.Cli/Program.fs @@ -10,7 +10,9 @@ open FSharp.Analyzers.SDK open GlobExpressions open Microsoft.CodeAnalysis.Sarif open Microsoft.CodeAnalysis.Sarif.Writers +open Microsoft.Extensions.Logging open Ionide.ProjInfo +open FSharp.Analyzers.Cli.CustomLogging type Arguments = | Project of string list @@ -29,7 +31,8 @@ type Arguments = | [] Report of string | [] FSC_Args of string | [] Code_Root of string - | [] Verbose + | [] Verbosity of string + | [] Allow_Version_Mismatch interface IArgParserTemplate with member s.Usage = @@ -52,10 +55,13 @@ type Arguments = | 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." + | Verbosity _ -> + "The verbosity level. The available verbosity levels are: n[ormal], d[etailed], diag[nostic]." | FSC_Args _ -> "Pass in the raw fsc compiler arguments. Cannot be combined with the `--project` flag." | Code_Root _ -> "Root of the current code repository, used in the sarif report to construct the relative file path. The current working directory is used by default." + | Allow_Version_Mismatch -> + "When not set (default), the analyzers that don't match with the major and minor parts of the Analyzer SDK version will not be executed. This can be overriden using this flag." type SeverityMappings = { @@ -92,7 +98,7 @@ let mapMessageToSeverity (mappings: SeverityMappings) (msg: FSharp.Analyzers.SDK } } -let mutable verbose = false +let mutable logLevel = LogLevel.Warning let fcs = Utils.createFCS None @@ -106,22 +112,7 @@ let rec mkKn (ty: Type) = else box () -let origForegroundColor = Console.ForegroundColor - -let printInfo (fmt: Printf.TextWriterFormat<'a>) : 'a = - if verbose then - Console.ForegroundColor <- ConsoleColor.DarkGray - printf "Info : " - Console.ForegroundColor <- origForegroundColor - printfn fmt - else - unbox (mkKn typeof<'a>) - -let printError (text: string) : unit = - Console.ForegroundColor <- ConsoleColor.Red - Console.Write "Error : " - Console.WriteLine(text) - Console.ForegroundColor <- origForegroundColor +let mutable logger: ILogger = Abstractions.NullLogger.Instance let loadProject toolsPath properties projPath = async { @@ -129,7 +120,7 @@ let loadProject toolsPath properties projPath = let parsed = loader.LoadProjects [ projPath ] |> Seq.toList if parsed.IsEmpty then - printError $"Failed to load project '{projPath}'" + logger.LogError("Failed to load project '{0}'", projPath) exit 1 let fcsPo = FCS.mapToFSharpProjectOptions parsed.Head parsed @@ -151,7 +142,7 @@ let runProjectAux |> Array.filter (fun file -> match ignoreFiles |> List.tryFind (fun g -> g.IsMatch file) with | Some g -> - printInfo $"Ignoring file %s{file} for pattern %s{g.Pattern}" + logger.LogInformation("Ignoring file {0} for pattern {1}", file, g.Pattern) false | None -> true ) @@ -159,11 +150,11 @@ let runProjectAux let fileContent = File.ReadAllText fileName let sourceText = SourceText.ofString fileContent - Utils.typeCheckFile fcs printError fsharpOptions fileName (Utils.SourceOfSource.SourceText sourceText) + Utils.typeCheckFile fcs logger fsharpOptions fileName (Utils.SourceOfSource.SourceText sourceText) |> Option.map (Utils.createContext checkProjectResults fileName sourceText) ) |> Array.map (fun ctx -> - printInfo "Running analyzers for %s" ctx.FileName + logger.LogInformation("Running analyzers for {0}", ctx.FileName) client.RunAnalyzers ctx ) |> Async.Parallel @@ -194,7 +185,7 @@ let runProject let fsharpFiles = set [| ".fs"; ".fsi"; ".fsx" |] let isFSharpFile (file: string) = - Seq.exists (fun (ext: string) -> file.EndsWith ext) fsharpFiles + Set.exists (fun (ext: string) -> file.EndsWith(ext, StringComparison.Ordinal)) fsharpFiles let runFscArgs (client: Client) @@ -203,7 +194,7 @@ let runFscArgs (mappings: SeverityMappings) = if String.IsNullOrWhiteSpace fscArgs then - printError "Empty --fsc-args were passed!" + logger.LogError("Empty --fsc-args were passed!") exit 1 else @@ -241,35 +232,43 @@ let runFscArgs runProjectAux client projectOptions globs mappings let printMessages (msgs: AnalyzerMessage list) = - if verbose then - printfn "" - if verbose && List.isEmpty msgs then - printfn "No messages found from the analyzer(s)" + let severityToLogLevel = + Map.ofArray + [| + Error, LogLevel.Error + Warning, LogLevel.Warning + Info, LogLevel.Information + Hint, LogLevel.Trace + |] + + if List.isEmpty msgs then + logger.LogInformation("No messages found from the analyzer(s)") + + use factory = + LoggerFactory.Create(fun builder -> + builder + .AddCustomFormatter(fun options -> options.UseAnalyzersMsgStyle <- true) + .SetMinimumLevel(LogLevel.Trace) + |> ignore + ) + + let msgLogger = factory.CreateLogger("") msgs |> Seq.iter (fun analyzerMessage -> let m = analyzerMessage.Message - let color = - match m.Severity with - | Error -> ConsoleColor.Red - | Warning -> ConsoleColor.DarkYellow - | Info -> ConsoleColor.Blue - | Hint -> ConsoleColor.Cyan - - Console.ForegroundColor <- color - - printfn - "%s(%d,%d): %s %s - %s" - m.Range.FileName - m.Range.StartLine - m.Range.StartColumn - (m.Severity.ToString()) - m.Code + msgLogger.Log( + severityToLogLevel[m.Severity], + "{0}({1},{2}): {3} {4} - {5}", + m.Range.FileName, + m.Range.StartLine, + m.Range.StartColumn, + (m.Severity.ToString()), + m.Code, m.Message - - Console.ForegroundColor <- origForegroundColor + ) ) () @@ -290,7 +289,7 @@ let writeReport (results: AnalyzerMessage list option) (codeRoot: string option) 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) + driver.Version <- string (System.Reflection.Assembly.GetExecutingAssembly().GetName().Version) let tool = Tool() tool.Driver <- driver let run = Run() @@ -362,8 +361,8 @@ let writeReport (results: AnalyzerMessage list option) (codeRoot: string option) sarifLogger.Dispose() with ex -> - let details = if not verbose then "" else $" %A{ex}" - printfn $"Could not write sarif to %s{report}%s{details}" + logger.LogError("Could not write sarif to {report}") + logger.LogInformation("{0}", ex) let calculateExitCode (msgs: AnalyzerMessage list option) : int = match msgs with @@ -397,7 +396,7 @@ let expandMultiProperties (properties: (string * string) list) = for pair in splits.[1..] |> Seq.chunkBySize 2 do match pair with | [| k; v |] when String.IsNullOrWhiteSpace(v) -> - printError $"Missing property value for '{k}'" + logger.LogError("Missing property value for '{0}'", k) exit 1 | [| k; v |] -> yield (k, v) | _ -> () @@ -409,10 +408,10 @@ let expandMultiProperties (properties: (string * string) list) = let validateRuntimeOsArchCombination (runtime, arch, os) = match runtime, os, arch with | Some _, Some _, _ -> - printError "Specifying both the `-r|--runtime` and `-os` options is not supported." + logger.LogError("Specifying both the `-r|--runtime` and `-os` options is not supported.") exit 1 | Some _, _, Some _ -> - printError "Specifying both the `-r|--runtime` and `-a|--arch` options is not supported." + logger.LogError("Specifying both the `-r|--runtime` and `-a|--arch` options is not supported.") exit 1 | _ -> () @@ -456,8 +455,35 @@ let main argv = let toolsPath = Init.init (DirectoryInfo Environment.CurrentDirectory) None let results = parser.ParseCommandLine argv - verbose <- results.Contains <@ Verbose @> - printInfo "Running in verbose mode" + + let logLevel = + let verbosity = results.TryGetResult <@ Verbosity @> + + match verbosity with + | Some "d" + | Some "detailed" -> LogLevel.Information + | Some "diag" + | Some "diagnostic" -> LogLevel.Debug + | Some "n" -> LogLevel.Warning + | Some "normal" -> LogLevel.Warning + | None -> LogLevel.Warning + | Some x -> + use factory = LoggerFactory.Create(fun b -> b.AddConsole() |> ignore) + let logger = factory.CreateLogger("") + logger.LogError("unknown verbosity level given {0}", x) + exit 1 + + use factory = + LoggerFactory.Create(fun builder -> + builder + .AddCustomFormatter(fun options -> options.UseAnalyzersMsgStyle <- false) + .SetMinimumLevel(logLevel) + |> ignore + ) + + logger <- factory.CreateLogger("") + + logger.LogInformation("Running in verbose mode") let severityMapping = { @@ -467,13 +493,15 @@ let main argv = TreatAsError = results.GetResult(<@ Treat_As_Error @>, []) |> Set.ofList } - printInfo "Treat as Hints: [%s]" (severityMapping.TreatAsHint |> String.concat ", ") - printInfo "Treat as Info: [%s]" (severityMapping.TreatAsInfo |> String.concat ", ") - printInfo "Treat as Warning: [%s]" (severityMapping.TreatAsWarning |> String.concat ", ") - printInfo "Treat as Error: [%s]" (severityMapping.TreatAsError |> String.concat ", ") + logger.LogInformation("Treat as Hints: [{0}]", (severityMapping.TreatAsHint |> String.concat ", ")) + logger.LogInformation("Treat as Info: [{0}]", (severityMapping.TreatAsInfo |> String.concat ", ")) + logger.LogInformation("Treat as Warning: [{0}]", (severityMapping.TreatAsWarning |> String.concat ", ")) + logger.LogInformation("Treat as Error: [{0}]", (severityMapping.TreatAsError |> String.concat ", ")) + let allowAnalyzerSDKVersionMismatch = results.Contains <@ Allow_Version_Mismatch @> + if not (severityMapping.IsValid()) then - printError "An analyzer code may only be listed once in the arguments." + logger.LogError("An analyzer code may only be listed once in the arguments.") exit 1 @@ -482,16 +510,16 @@ let main argv = let report = results.TryGetResult <@ Report @> let codeRoot = results.TryGetResult <@ Code_Root @> let ignoreFiles = results.GetResult(<@ Ignore_Files @>, []) - printInfo "Ignore Files: [%s]" (ignoreFiles |> String.concat ", ") + logger.LogInformation("Ignore Files: [{0}]", (ignoreFiles |> String.concat ", ")) let ignoreFiles = ignoreFiles |> List.map Glob let properties = getProperties results if Option.isSome fscArgs && not properties.IsEmpty then - printError "fsc-args can't be combined with MSBuild properties." + logger.LogError("fsc-args can't be combined with MSBuild properties.") exit 1 - if verbose then - properties |> List.iter (fun (k, v) -> printInfo $"Property %s{k}=%s{v}") + properties + |> List.iter (fun (k, v) -> logger.LogInformation("Property {0}={1}", k, v)) let analyzersPaths = results.GetResults(<@ Analyzers_Path @>) @@ -506,19 +534,10 @@ let main argv = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, path)) ) - printInfo "Loading analyzers from %s" (String.concat ", " analyzersPaths) + logger.LogInformation("Loading analyzers from {0}", (String.concat ", " analyzersPaths)) let excludeAnalyzers = results.GetResult(<@ Exclude_Analyzer @>, []) - let logger = - { new Logger with - member _.Error msg = printError msg - - member _.Verbose msg = - if verbose then - printInfo "%s" msg - } - AssemblyLoadContext.Default.add_Resolving (fun _ctx assemblyName -> if assemblyName.Name <> "FSharp.Core" then null @@ -530,12 +549,12 @@ let main argv = The correct version can be found over at https://www.nuget.org/packages/FSharp.Analyzers.SDK#dependencies-body-tab. """ - printError msg + logger.LogError(msg) exit 1 ) let client = - Client(logger, Set.ofList excludeAnalyzers) + Client(logger, Set.ofList excludeAnalyzers, allowAnalyzerSDKVersionMismatch) let dlls, analyzers = ((0, 0), analyzersPaths) @@ -544,7 +563,7 @@ let main argv = (accDlls + dlls), (accAnalyzers + analyzers) ) - printInfo "Registered %d analyzers from %d dlls" analyzers dlls + logger.LogInformation("Registered {0} analyzers from {1} dlls", analyzers, dlls) let results = if analyzers = 0 then @@ -552,17 +571,17 @@ let main argv = else match projOpts, fscArgs with | [], None -> - printError "No project given. Use `--project PATH_TO_FSPROJ`." + logger.LogError("No project given. Use `--project PATH_TO_FSPROJ`.") None | _ :: _, Some _ -> - printError "`--project` and `--fsc-args` cannot be combined." + logger.LogError("`--project` and `--fsc-args` cannot be combined.") exit 1 | [], Some fscArgs -> runFscArgs client fscArgs ignoreFiles severityMapping |> Async.RunSynchronously | projects, None -> for projPath in projects do if not (File.Exists(projPath)) then - printError $"Invalid `--project` argument. File does not exist: '{projPath}'" + logger.LogError("Invalid `--project` argument. File does not exist: '{projPath}'") exit 1 projects diff --git a/src/FSharp.Analyzers.Cli/Properties/launchSettings.json b/src/FSharp.Analyzers.Cli/Properties/launchSettings.json index 75b12e2..3a967a7 100644 --- a/src/FSharp.Analyzers.Cli/Properties/launchSettings.json +++ b/src/FSharp.Analyzers.Cli/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "ReadMeSample": { "commandName": "Project", - "commandLineArgs": "--project ./samples/OptionAnalyzer/OptionAnalyzer.fsproj --analyzers-path ./samples/OptionAnalyzer/bin/Release --verbose" + "commandLineArgs": "--project ./samples/OptionAnalyzer/OptionAnalyzer.fsproj --analyzers-path ./samples/OptionAnalyzer/bin/Release --verbosity d" } } } 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 be04b01..6769c5e 100644 --- a/src/FSharp.Analyzers.SDK.Testing/FSharp.Analyzers.SDK.Testing.fs +++ b/src/FSharp.Analyzers.SDK.Testing/FSharp.Analyzers.SDK.Testing.fs @@ -4,6 +4,7 @@ module FSharp.Analyzers.SDK.Testing #nowarn "57" open Microsoft.Build.Logging.StructuredLogger +open Microsoft.Extensions.Logging open CliWrap open System open System.IO @@ -43,7 +44,7 @@ exception CompilerDiagnosticErrors of FSharpDiagnostic array let fsharpFiles = set [| ".fs"; ".fsi"; ".fsx" |] let isFSharpFile (file: string) = - Seq.exists (fun (ext: string) -> file.EndsWith ext) fsharpFiles + Set.exists (fun (ext: string) -> file.EndsWith(ext, StringComparison.Ordinal)) fsharpFiles let readCompilerArgsFromBinLog (build: Build) = if not build.Succeeded then @@ -81,7 +82,7 @@ let readCompilerArgsFromBinLog (build: Build) = match args with | None -> failwith $"Could not parse binlog at {build.LogFilePath}, does it contain CoreCompile?" | Some args -> - let idx = args.IndexOf "-o:" + let idx = args.IndexOf("-o:", StringComparison.Ordinal) args.Substring(idx).Split [| '\n' |] let mkOptions (compilerArgs: string array) = @@ -245,9 +246,14 @@ let getContextFor (opts: FSharpProjectOptions) isSignature source = if Array.isEmpty allSymbolUses then failwith "no symboluses" - let printError s = printf $"{s}" - - match Utils.typeCheckFile fcs printError opts fileName (Utils.SourceOfSource.DiscreteSource source) with + match + Utils.typeCheckFile + fcs + Abstractions.NullLogger.Instance + opts + fileName + (Utils.SourceOfSource.DiscreteSource source) + with | Some(parseFileResults, checkFileResults) -> let diagErrors = checkFileResults.Diagnostics diff --git a/src/FSharp.Analyzers.SDK.Testing/FSharp.Analyzers.SDK.Testing.fsproj b/src/FSharp.Analyzers.SDK.Testing/FSharp.Analyzers.SDK.Testing.fsproj index b7e1cd9..a094f6f 100644 --- a/src/FSharp.Analyzers.SDK.Testing/FSharp.Analyzers.SDK.Testing.fsproj +++ b/src/FSharp.Analyzers.SDK.Testing/FSharp.Analyzers.SDK.Testing.fsproj @@ -19,6 +19,7 @@ + diff --git a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fs b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fs index 8e5fa24..229bd43 100644 --- a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fs +++ b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fs @@ -7,6 +7,7 @@ open System.Reflection open System.Runtime.Loader open System.Text.RegularExpressions open McMaster.NETCore.Plugins +open Microsoft.Extensions.Logging type AnalysisResult = { @@ -27,7 +28,7 @@ module Client = let isAnalyzer<'TAttribute when 'TAttribute :> AnalyzerAttribute> (mi: MemberInfo) = mi.GetCustomAttributes true - |> Seq.tryFind (fun n -> n.GetType().Name = typeof<'TAttribute>.Name) + |> Array.tryFind (fun n -> n.GetType().Name = typeof<'TAttribute>.Name) |> Option.map unbox<'TAttribute> let analyzerFromMember<'TAnalyzerAttribute, 'TContext @@ -126,25 +127,15 @@ module Client = |> Seq.choose (analyzerFromMember<'TAnalyzerAttribute, 'TContext> path) |> Seq.toList -[] -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) + (logger: ILogger, excludedAnalyzers: string Set, allowAnalyzerSDKVersionMismatch: bool) = + do TASTCollecting.logger <- logger + let registeredAnalyzers = ConcurrentDictionary list>() - new() = - Client( - { new Logger with - member this.Error _ = () - member this.Verbose _ = () - }, - Set.empty - ) + new() = Client(Abstractions.NullLogger.Instance, Set.empty, false) member x.LoadAnalyzers(dir: string) : int * int = if Directory.Exists dir then @@ -192,10 +183,22 @@ type Client<'TAttribute, 'TContext when 'TAttribute :> AnalyzerAttribute and 'TC then true else - logger.Error - $"Trying to load %s{name} which was built using SDK version %A{version}. Expect %A{Utils.currentFSharpAnalyzersSDKVersion} instead. Assembly will be skipped." + if allowAnalyzerSDKVersionMismatch then + logger.LogWarning( + "{0} was built using SDK version {1}. Running {2} instead, may cause runtime error.", + name, + version, + Utils.currentFSharpAnalyzersSDKVersion + ) + else + logger.LogError( + "Trying to load {0} which was built using SDK version {1}. Running {2} instead. Assembly will be skipped.", + name, + version, + Utils.currentFSharpAnalyzersSDKVersion + ) - false + allowAnalyzerSDKVersionMismatch ) |> Array.map (fun (path, assembly) -> let analyzers = @@ -205,7 +208,11 @@ type Client<'TAttribute, 'TContext when 'TAttribute :> AnalyzerAttribute and 'TC let shouldExclude = excludedAnalyzers.Contains(registeredAnalyzer.Name) if shouldExclude then - logger.Verbose $"Excluding %s{registeredAnalyzer.Name} from %s{assembly.FullName}" + logger.LogInformation( + "Excluding {0} from {1}", + registeredAnalyzer.Name, + assembly.FullName + ) not shouldExclude ) @@ -218,7 +225,7 @@ type Client<'TAttribute, 'TContext when 'TAttribute :> AnalyzerAttribute and 'TC registeredAnalyzers.AddOrUpdate(path, analyzers, (fun _ _ -> analyzers)) |> ignore - Seq.length analyzers, analyzers |> Seq.collect snd |> Seq.length + Array.length analyzers, analyzers |> Seq.collect snd |> Seq.length else 0, 0 diff --git a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fsi b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fsi index d70dcb2..f2baf74 100644 --- a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fsi +++ b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fsi @@ -1,18 +1,15 @@ namespace FSharp.Analyzers.SDK +open Microsoft.Extensions.Logging + type AnalysisResult = { AnalyzerName: string 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: logger: ILogger * excludedAnalyzers: string Set * allowAnalyzerSDKVersionMismatch: bool -> Client<'TAttribute, 'TContext> new: unit -> Client<'TAttribute, 'TContext> /// /// Loads into private state any analyzers defined in any assembly diff --git a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fs b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fs index 564d950..bd494d9 100644 --- a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fs +++ b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fs @@ -3,6 +3,7 @@ namespace FSharp.Analyzers.SDK #nowarn "57" open System +open Microsoft.Extensions.Logging open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.Symbols open FSharp.Compiler.EditorServices @@ -224,7 +225,7 @@ module Utils = let typeCheckFile (fcs: FSharpChecker) - (printError: string -> unit) + (logger: ILogger) (options: FSharpProjectOptions) (fileName: string) (source: SourceOfSource) @@ -244,6 +245,6 @@ module Utils = match checkAnswer with | FSharpCheckFileAnswer.Aborted -> - printError $"Checking of file {fileName} aborted" + logger.LogError("Checking of file {0} aborted", fileName) None | FSharpCheckFileAnswer.Succeeded result -> Some(parseRes, result) diff --git a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fsi b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fsi index 960d586..b3ef146 100644 --- a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fsi +++ b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fsi @@ -2,6 +2,7 @@ namespace FSharp.Analyzers.SDK open System open System.Runtime.InteropServices +open Microsoft.Extensions.Logging open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.Symbols open FSharp.Compiler.EditorServices @@ -165,7 +166,7 @@ module Utils = val typeCheckFile: fcs: FSharpChecker -> - printError: (string -> unit) -> + logger: ILogger -> options: FSharpProjectOptions -> fileName: string -> source: SourceOfSource -> diff --git a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fsproj b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fsproj index d68b907..0c2f432 100644 --- a/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fsproj +++ b/src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fsproj @@ -7,6 +7,7 @@ Krzysztof Cieslak SDK for building custom analyzers for FSAC / F# editors Copyright 2019 Lambda Factory + true @@ -22,5 +23,6 @@ + \ No newline at end of file diff --git a/src/FSharp.Analyzers.SDK/TASTCollecting.fs b/src/FSharp.Analyzers.SDK/TASTCollecting.fs index ccc42e6..4f7587b 100644 --- a/src/FSharp.Analyzers.SDK/TASTCollecting.fs +++ b/src/FSharp.Analyzers.SDK/TASTCollecting.fs @@ -1,11 +1,14 @@ namespace FSharp.Analyzers.SDK +open Microsoft.Extensions.Logging open FSharp.Compiler.Symbols open FSharp.Compiler.Text open FSharp.Compiler.Symbols.FSharpExprPatterns module TASTCollecting = + let mutable logger: ILogger = Abstractions.NullLogger.Instance + type TypedTreeCollectorBase() = abstract WalkCall: objExprOpt: FSharpExpr option -> @@ -139,7 +142,9 @@ module TASTCollecting = try visitExpr f e with ex -> - printfn $"unhandled expression at {e.Range.FileName}:{e.Range.ToString()}" + logger.LogDebug("unhandled expression at {0}:{1}", e.Range.FileName, e.Range.ToString()) + logger.LogDebug("{0}", ex.Message) + logger.LogDebug("{0}", ex.StackTrace) | FSharpImplementationFileDeclaration.InitAction e -> visitExpr f e let walkTast (walker: TypedTreeCollectorBase) (tast: FSharpImplementationFileContents) : unit = diff --git a/src/FSharp.Analyzers.SDK/TASTCollecting.fsi b/src/FSharp.Analyzers.SDK/TASTCollecting.fsi index 9cff195..f3d41fc 100644 --- a/src/FSharp.Analyzers.SDK/TASTCollecting.fsi +++ b/src/FSharp.Analyzers.SDK/TASTCollecting.fsi @@ -1,5 +1,6 @@ namespace FSharp.Analyzers.SDK +open Microsoft.Extensions.Logging open FSharp.Compiler.Symbols open FSharp.Compiler.Text @@ -36,3 +37,6 @@ module TASTCollecting = /// Traverses the whole TAST and calls the appropriate members of the given TypedTreeCollectorBase /// to process the tree elements. val walkTast: walker: TypedTreeCollectorBase -> tast: FSharpImplementationFileContents -> unit + + /// Set this to use a custom logger + val mutable logger: ILogger