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..e116d2657
--- /dev/null
+++ b/src/FSharpLint.Console/Sarif.fs
@@ -0,0 +1,110 @@
+module internal Sarif
+
+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) =
+ 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(
+ 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(
+ report,
+ logFilePersistenceOptions =
+ (FilePersistenceOptions.PrettyPrint ||| FilePersistenceOptions.ForceOverwrite),
+ run = run,
+ levels = BaseLogger.ErrorWarningNote,
+ kinds = BaseLogger.Fail,
+ closeWriterOnDispose = true
+ )
+
+ sarifLogger.AnalysisStarted()
+
+ for analyzerResult in results do
+ 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
+ |> Option.iter (fun shortDescription ->
+ reportDescriptor.ShortDescription <-
+ MultiformatMessageString(shortDescription, shortDescription, dict [])
+ )
+ *)
+
+ (*
+ result.Level <-
+ match analyzerResult.Message.Severity with
+ | Severity.Info -> FailureLevel.Note
+ | Severity.Hint -> FailureLevel.Note
+ | Severity.Warning -> FailureLevel.Warning
+ | Severity.Error -> FailureLevel.Error
+ *)
+
+ 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())
+
+ 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