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