From 53b970d7716a52603cb2b4787d8d21d38e12bdc5 Mon Sep 17 00:00:00 2001 From: Richard Webb Date: Sun, 10 Mar 2024 12:06:36 +0000 Subject: [PATCH 1/2] WIP: Add Sarif output support to FSharpLint.Console This is using the Microsoft Sarif.Sdk to write Sarif files. --- Directory.Packages.props | 1 + .../FSharpLint.Console.fsproj | 2 + src/FSharpLint.Console/Program.fs | 11 ++ src/FSharpLint.Console/Sarif.fs | 103 ++++++++++++++++++ 4 files changed, 117 insertions(+) create mode 100644 src/FSharpLint.Console/Sarif.fs diff --git a/Directory.Packages.props b/Directory.Packages.props index efe44081a..021722ee5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,6 +23,7 @@ + diff --git a/src/FSharpLint.Console/FSharpLint.Console.fsproj b/src/FSharpLint.Console/FSharpLint.Console.fsproj index a614e3105..70bfbfaa1 100644 --- a/src/FSharpLint.Console/FSharpLint.Console.fsproj +++ b/src/FSharpLint.Console/FSharpLint.Console.fsproj @@ -27,12 +27,14 @@ + + diff --git a/src/FSharpLint.Console/Program.fs b/src/FSharpLint.Console/Program.fs index 0049ff76a..ec1a7b778 100644 --- a/src/FSharpLint.Console/Program.fs +++ b/src/FSharpLint.Console/Program.fs @@ -24,6 +24,8 @@ type internal 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 @@ -91,6 +95,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 -> @@ -98,6 +105,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 From dd48266cf2c84603cf34e0f84b81b5d49cf79fea Mon Sep 17 00:00:00 2001 From: Richard Webb Date: Sat, 26 Jul 2025 09:54:12 +0100 Subject: [PATCH 2/2] Update based on review comments --- src/FSharpLint.Console/Sarif.fs | 93 ++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/src/FSharpLint.Console/Sarif.fs b/src/FSharpLint.Console/Sarif.fs index 4b1cd1734..e116d2657 100644 --- a/src/FSharpLint.Console/Sarif.fs +++ b/src/FSharpLint.Console/Sarif.fs @@ -1,10 +1,10 @@ module internal Sarif -open FSharpLint.Framework -open System.IO open System +open System.IO open Microsoft.CodeAnalysis.Sarif open Microsoft.CodeAnalysis.Sarif.Writers +open FSharpLint.Framework open FSharpLint.Console.Output let writeReport (results: Suggestion.LintWarning list) (codeRoot: string option) (report: string) (logger: IOutput) = @@ -20,14 +20,15 @@ let writeReport (results: Suggestion.LintWarning list) (codeRoot: string option) 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 + let driver = + ToolComponent( + Name = "FSharpLint.Console", + InformationUri = Uri("https://fsprojects.github.io/FSharpLint/"), + Version = string (System.Reflection.Assembly.GetExecutingAssembly().GetName().Version) + ) + + let tool = Tool(Driver = driver) + let run = Run(Tool = tool) use sarifLogger = new SarifLogger( @@ -43,9 +44,14 @@ let writeReport (results: Suggestion.LintWarning list) (codeRoot: string option) sarifLogger.AnalysisStarted() for analyzerResult in results do - let reportDescriptor = ReportingDescriptor() - reportDescriptor.Id <- analyzerResult.RuleIdentifier - reportDescriptor.Name <- analyzerResult.RuleName + let helpUri = $"https://fsprojects.github.io/FSharpLint/how-tos/rules/%s{analyzerResult.RuleIdentifier}.html" + + let reportDescriptor = + ReportingDescriptor( + Id = analyzerResult.RuleIdentifier, + HelpUri = Uri(helpUri), + Name = analyzerResult.RuleName + ) (* analyzerResult.ShortDescription @@ -55,12 +61,6 @@ let writeReport (results: Suggestion.LintWarning list) (codeRoot: string option) ) *) - 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 @@ -69,30 +69,37 @@ let writeReport (results: Suggestion.LintWarning list) (codeRoot: string option) | 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 |] + + let msg = Message(Text = analyzerResult.Details.Message) + + let artifactLocation = + ArtifactLocation( + Uri = codeRoot.MakeRelativeUri(Uri(analyzerResult.Details.Range.FileName)) + ) + + let region = + Region( + StartLine = analyzerResult.Details.Range.StartLine, + StartColumn = analyzerResult.Details.Range.StartColumn + 1, + EndLine = analyzerResult.Details.Range.EndLine, + EndColumn = analyzerResult.Details.Range.EndColumn + 1 + ) + + let physicalLocation = + PhysicalLocation( + ArtifactLocation = artifactLocation, + Region = region + ) + + let location = Location(PhysicalLocation = physicalLocation) + + let result = + Result( + RuleId = reportDescriptor.Id, + Level = FailureLevel.Warning, + Locations = [| location |], + Message = msg + ) sarifLogger.Log(reportDescriptor, result, System.Nullable())