Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/content/Running during CI.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Example when using MSBuild:

## GitHub Actions

### GitHub Advanced Security
If you are using [GitHub Actions](https://docs.github.com/en/code-security/codeql-cli/using-the-advanced-functionality-of-the-codeql-cli/sarif-output) you can easily send the *sarif file* to [CodeQL](https://codeql.github.com/).

```yml
Expand All @@ -68,4 +69,14 @@ Sample:

See [fsproject/fantomas#2962](https://github.com/fsprojects/fantomas/pull/2962) for more information.

### Github Workflow Commands
If you cannot use GitHub Advanced Security (e.g. if your repository is private), you can get similar annotations by running the analyzers with `--output-format github`.
This will make the analyzers print their results as [GitHub Workflow Commands](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions).
If you for instance have a GitHub Action to run analyzers on every pull request, these annotations will show up in the "Files changed" on the pull request.
If the annotations don't show correctly, you might need to set the `code-root` to the root of the repository.

Note that GitHub has a hard limit of 10 annotations of each type (notice, warning, error) per CI step.
This means that only the first 10 errors, the first 10 warnings and the first 10 hints/info results from analyzers will generate annotations.
The workflow log will contain all analyzer results even if a job hits the annotation limits.

[Previous]({{fsdocs-previous-page-link}})
101 changes: 93 additions & 8 deletions src/FSharp.Analyzers.Cli/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type Arguments =
| [<Unique>] FSC_Args of string
| [<Unique>] Code_Root of string
| [<Unique; AltCommandLine("-v")>] Verbosity of string
| [<Unique>] Output_Format of string

interface IArgParserTemplate with
member s.Usage =
Expand Down Expand Up @@ -67,6 +68,8 @@ type Arguments =
| 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."
| Output_Format _ ->
"Format in which to write analyzer results to stdout. The available options are: default, github."

type SeverityMappings =
{
Expand Down Expand Up @@ -103,6 +106,16 @@ let mapMessageToSeverity (mappings: SeverityMappings) (msg: FSharp.Analyzers.SDK
}
}

[<RequireQualifiedAccess>]
type OutputFormat =
| Default
| GitHub

let parseOutputFormat = function
| "github" -> Ok OutputFormat.GitHub
| "default" -> Ok OutputFormat.Default
| other -> Error $"Unknown output format: %s{other}."

let mutable logLevel = LogLevel.Warning

let fcs = Utils.createFCS None
Expand Down Expand Up @@ -258,7 +271,7 @@ let runFscArgs

runProject client projectOptions excludeIncludeFiles mappings

let printMessages (msgs: AnalyzerMessage list) =
let printMessagesInDefaultFormat (msgs: AnalyzerMessage list) =

let severityToLogLevel =
Map.ofArray
Expand Down Expand Up @@ -300,13 +313,70 @@ let printMessages (msgs: AnalyzerMessage list) =

()

let writeReport (results: AnalyzerMessage list) (codeRoot: string option) (report: string) =
try
let codeRoot =
match codeRoot with
| None -> Directory.GetCurrentDirectory() |> Uri
| Some root -> Path.GetFullPath root |> Uri
let printMessagesInGitHubFormat (codeRoot : Uri) (msgs: AnalyzerMessage list) =
let severityToLogLevel =
Map.ofArray
[|
Severity.Error, LogLevel.Error
Severity.Warning, LogLevel.Warning
Severity.Info, LogLevel.Information
Severity.Hint, LogLevel.Trace
|]

let severityToGitHubAnnotationType =
Map.ofArray
[|
Severity.Error, "error"
Severity.Warning, "warning"
Severity.Info, "notice"
Severity.Hint, "notice"
|]

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
)

// No category name because GitHub needs the annotation type to be the first
// element on each line.
let msgLogger = factory.CreateLogger("")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth commenting we don't want to specify a name since it would interfere with the output that github is expecting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 8348afd.


msgs
|> List.iter (fun analyzerMessage ->
let m = analyzerMessage.Message

// We want file names to be relative to the repository so GitHub will recognize them.
// GitHub also only understands Unix-style directory separators.
let relativeFileName =
codeRoot.MakeRelativeUri(Uri(m.Range.FileName))
|> _.OriginalString

msgLogger.Log(
severityToLogLevel[m.Severity],
"::{0} file={1},line={2},endLine={3},col={4},endColumn={5},title={6} ({7})::{8}: {9}",
severityToGitHubAnnotationType[m.Severity],
relativeFileName,
m.Range.StartLine,
m.Range.EndLine,
m.Range.StartColumn,
m.Range.EndColumn,
analyzerMessage.Name,
m.Code,
m.Severity.ToString(),
m.Message
)
)

()

let writeReport (results: AnalyzerMessage list) (codeRoot: Uri) (report: string) =
try
// Construct full path to ensure path separators are normalized.
let report = Path.GetFullPath report
// Ensure the parent directory exists
Expand Down Expand Up @@ -551,6 +621,14 @@ let main argv =
properties
|> List.iter (fun (k, v) -> logger.LogInformation("Property {0}={1}", k, v))

let outputFormat =
results.TryGetResult <@ Output_Format @>
|> Option.map parseOutputFormat
|> Option.defaultValue (Ok OutputFormat.Default)
|> Result.defaultWith (fun errMsg ->
logger.LogError("{0} Using default output format.", errMsg)
OutputFormat.Default)

let analyzersPaths =
results.GetResults(<@ Analyzers_Path @>)
|> List.concat
Expand Down Expand Up @@ -659,7 +737,14 @@ let main argv =

let results = results |> List.concat

printMessages results
let codeRoot =
match codeRoot with
| None -> Directory.GetCurrentDirectory() |> Uri
| Some root -> Path.GetFullPath root |> Uri

match outputFormat with
| OutputFormat.Default -> printMessagesInDefaultFormat results
| OutputFormat.GitHub -> printMessagesInGitHubFormat codeRoot results

report |> Option.iter (writeReport results codeRoot)

Expand Down