From ba63f6c7ff71a892e4e878aabdea008d341b75f7 Mon Sep 17 00:00:00 2001 From: Richard Webb Date: Sun, 10 Mar 2024 12:06:36 +0000 Subject: [PATCH] WIP: Add Sarif output support to FSharpLint.Console --- paket.dependencies | 3 +- paket.lock | 15 ++- .../FSharpLint.Console.fsproj | 1 + src/FSharpLint.Console/Program.fs | 11 ++ src/FSharpLint.Console/Sarif.fs | 103 ++++++++++++++++++ src/FSharpLint.Console/paket.references | 3 +- 6 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 src/FSharpLint.Console/Sarif.fs diff --git a/paket.dependencies b/paket.dependencies index 55a3eab40..d60f779d7 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -14,6 +14,7 @@ nuget Ionide.ProjInfo == 0.61.3 nuget Ionide.ProjInfo.Sln == 0.61.3 nuget FSharp.Core ~> 6.0 nuget nunit ~> 3.0 +nuget Sarif.Sdk nuget System.Reactive ~> 5 nuget NUnit3TestAdapter nuget Microsoft.NET.Test.Sdk 17.7.2 @@ -60,4 +61,4 @@ group Build nuget Fake.Core.UserInput nuget Fake.IO.FileSystem nuget Fake.DotNet.MsBuild - nuget Fake.Api.GitHub + nuget Fake.Api.GitHub \ No newline at end of file diff --git a/paket.lock b/paket.lock index 177bf2fb8..5baf32c4a 100644 --- a/paket.lock +++ b/paket.lock @@ -143,6 +143,7 @@ NUGET Microsoft.Diagnostics.NETCore.Client (>= 0.2.410101) System.Collections.Immutable (>= 6.0) System.Runtime.CompilerServices.Unsafe (>= 6.0) + Microsoft.Diagnostics.Tracing.EventRegister (1.1.28) Microsoft.Diagnostics.Tracing.TraceEvent (3.1.7) Microsoft.Win32.Registry (>= 4.4) System.Runtime.CompilerServices.Unsafe (>= 5.0) @@ -250,6 +251,15 @@ NUGET runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) runtime.ubuntu.18.04-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) + Sarif.Sdk (4.5.4) + Microsoft.Diagnostics.Tracing.EventRegister (>= 1.1.28) + Microsoft.Diagnostics.Tracing.TraceEvent (>= 3.1.3) + Newtonsoft.Json (>= 9.0.1) + System.Collections.Immutable (>= 5.0) + System.Diagnostics.Debug (>= 4.3) + System.IO.FileSystem.Primitives (>= 4.3) + System.Text.Encoding.CodePages (>= 4.3) + System.Text.Encoding.Extensions (>= 4.3) SemanticVersioning (2.0.2) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) System.Buffers (4.5.1) System.CodeDom (8.0) - copy_local: false @@ -570,7 +580,6 @@ NUGET System.Security.Cryptography.Primitives (>= 4.3) System.Text.Encoding (>= 4.3) System.Security.Cryptography.Cng (5.0) - copy_local: false - System.Formats.Asn1 (>= 5.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= netcoreapp3.0)) System.Security.Cryptography.Csp (4.3) Microsoft.NETCore.Platforms (>= 1.1) System.IO (>= 4.3) @@ -663,9 +672,9 @@ NUGET Microsoft.NETCore.Targets (>= 1.1) System.Runtime (>= 4.3) System.Text.Encoding (>= 4.3) - System.Text.Encodings.Web (8.0) - copy_local: false, restriction: || (== net6.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net5.0)) + System.Text.Encodings.Web (8.0) - copy_local: false, restriction: || (== net6.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net6.0)) System.Runtime.CompilerServices.Unsafe (>= 6.0) - System.Text.Json (8.0) - copy_local: false, restriction: || (== net6.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net5.0)) + System.Text.Json (8.0) - copy_local: false, restriction: || (== net6.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net6.0)) System.Runtime.CompilerServices.Unsafe (>= 6.0) System.Text.Encodings.Web (>= 8.0) System.Threading (4.3) diff --git a/src/FSharpLint.Console/FSharpLint.Console.fsproj b/src/FSharpLint.Console/FSharpLint.Console.fsproj index 2a9d92b88..98ce1e188 100644 --- a/src/FSharpLint.Console/FSharpLint.Console.fsproj +++ b/src/FSharpLint.Console/FSharpLint.Console.fsproj @@ -17,6 +17,7 @@ + diff --git a/src/FSharpLint.Console/Program.fs b/src/FSharpLint.Console/Program.fs index 73d43cd43..c43cffdd4 100644 --- a/src/FSharpLint.Console/Program.fs +++ b/src/FSharpLint.Console/Program.fs @@ -24,6 +24,8 @@ type private FileType = type private ToolArgs = | [] Format of OutputFormat | [] Lint of ParseResults + | [] Report of string + | [] Code_Root of string | Version with interface IArgParserTemplate with @@ -31,6 +33,8 @@ with match this with | Format _ -> "Output format of the linter." | Lint _ -> "Runs FSharpLint against a file or a collection of files." + | Report _ -> "Write the result messages to a (sarif) report file." + | 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." | Version -> "Prints current version." // TODO: investigate erroneous warning on this type definition @@ -94,6 +98,9 @@ let private start (arguments:ParseResults) (toolsPath:Ionide.ProjInfo. output.WriteError str exitCode <- -1 + let reportPath = arguments.TryGetResult Report + let codeRoot = arguments.TryGetResult Code_Root + match arguments.GetSubCommand() with | Lint lintArgs -> @@ -101,6 +108,10 @@ let private start (arguments:ParseResults) (toolsPath:Ionide.ProjInfo. | LintResult.Success(warnings) -> String.Format(Resources.GetString("ConsoleFinished"), List.length warnings) |> output.WriteInfo + + reportPath + |> Option.iter (fun report -> Sarif.writeReport warnings codeRoot report output) + if not (List.isEmpty warnings) then exitCode <- -1 | LintResult.Failure(failure) -> handleError failure.Description diff --git a/src/FSharpLint.Console/Sarif.fs b/src/FSharpLint.Console/Sarif.fs new file mode 100644 index 000000000..4b1cd1734 --- /dev/null +++ b/src/FSharpLint.Console/Sarif.fs @@ -0,0 +1,103 @@ +module internal Sarif + +open FSharpLint.Framework +open System.IO +open System +open Microsoft.CodeAnalysis.Sarif +open Microsoft.CodeAnalysis.Sarif.Writers +open FSharpLint.Console.Output + +let writeReport (results: Suggestion.LintWarning list) (codeRoot: string option) (report: string) (logger: IOutput) = + try + let codeRoot = + match codeRoot with + | None -> Directory.GetCurrentDirectory() |> Uri + | Some root -> Path.GetFullPath root |> Uri + + // Construct full path to ensure path separators are normalized. + let report = Path.GetFullPath report + // Ensure the parent directory exists + let reportFile = FileInfo(report) + reportFile.Directory.Create() + + let driver = ToolComponent() + driver.Name <- "FSharpLint.Console" + driver.InformationUri <- Uri("https://fsprojects.github.io/FSharpLint/") + 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 results do + let reportDescriptor = ReportingDescriptor() + reportDescriptor.Id <- analyzerResult.RuleIdentifier + reportDescriptor.Name <- analyzerResult.RuleName + + (* + analyzerResult.ShortDescription + |> Option.iter (fun shortDescription -> + reportDescriptor.ShortDescription <- + MultiformatMessageString(shortDescription, shortDescription, dict []) + ) + *) + + let helpUri = $"https://fsprojects.github.io/FSharpLint/how-tos/rules/%s{analyzerResult.RuleIdentifier}.html" + reportDescriptor.HelpUri <- Uri(helpUri) + + let result = Result() + result.RuleId <- reportDescriptor.Id + + (* + result.Level <- + match analyzerResult.Message.Severity with + | Severity.Info -> FailureLevel.Note + | Severity.Hint -> FailureLevel.Note + | Severity.Warning -> FailureLevel.Warning + | Severity.Error -> FailureLevel.Error + *) + result.Level <- FailureLevel.Warning + + let msg = Message() + msg.Text <- analyzerResult.Details.Message + result.Message <- msg + + let physicalLocation = PhysicalLocation() + + physicalLocation.ArtifactLocation <- + let al = ArtifactLocation() + al.Uri <- codeRoot.MakeRelativeUri(Uri(analyzerResult.Details.Range.FileName)) + al + + physicalLocation.Region <- + let r = Region() + r.StartLine <- analyzerResult.Details.Range.StartLine + r.StartColumn <- analyzerResult.Details.Range.StartColumn + 1 + r.EndLine <- analyzerResult.Details.Range.EndLine + r.EndColumn <- analyzerResult.Details.Range.EndColumn + 1 + 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 -> + logger.WriteError($"Could not write sarif to %s{report}: %s{ex.Message}") \ No newline at end of file diff --git a/src/FSharpLint.Console/paket.references b/src/FSharpLint.Console/paket.references index fc097e42e..46776b312 100644 --- a/src/FSharpLint.Console/paket.references +++ b/src/FSharpLint.Console/paket.references @@ -1,4 +1,5 @@ Argu FSharp.Compiler.Service FSharp.Core -Microsoft.SourceLink.GitHub \ No newline at end of file +Microsoft.SourceLink.GitHub +Sarif.Sdk import_targets: false \ No newline at end of file