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
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<PackageVersion Include="FSharp.Compiler.Service" Version="[43.9.300]" />
<PackageVersion Include="Ionide.KeepAChangelog.Tasks" Version="0.1.8" PrivateAssets="all" />
<PackageVersion Include="McMaster.NETCore.Plugins" Version="1.4.0" />
<PackageVersion Include="Argu" Version="6.1.1" />
<PackageVersion Include="Argu" Version="6.2.5" />
Copy link
Member Author

Choose a reason for hiding this comment

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

Had to update Argu to deal with the Compiler attribute issue

<PackageVersion Include="Glob" Version="1.1.9" />
<PackageVersion Include="Ionide.ProjInfo.ProjectSystem" Version="0.71.0" />
<PackageVersion Include="Microsoft.Build" Version="$(MsBuildPackageVersion)" ExcludeAssets="runtime" />
Expand Down
11 changes: 10 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,19 @@ F# analyzers are live, real-time, project based plugins that enables to diagnose
1. `dotnet build -c Release`
2. Run the console application:

Against a project:

```shell
dotnet run --project src/FSharp.Analyzers.Cli/FSharp.Analyzers.Cli.fsproj -- --project ./samples/OptionAnalyzer/OptionAnalyzer.fsproj --analyzers-path ./artifacts/bin/OptionAnalyzer/release --verbosity d
```

Against a script:

```shell
dotnet run --project src\FSharp.Analyzers.Cli\FSharp.Analyzers.Cli.fsproj -- --project ./samples/OptionAnalyzer/OptionAnalyzer.fsproj --analyzers-path ./samples/OptionAnalyzer/bin/Release --verbosity d
dotnet run --project src/FSharp.Analyzers.Cli/FSharp.Analyzers.Cli.fsproj -- --script ./samples/BadOptionUsage.fsx --analyzers-path ./artifacts/bin/OptionAnalyzer/release --verbosity d
```


You can also set up a run configuration of FSharp.Analyzers.Cli in your favorite IDE using similar arguments. This also allows you to debug FSharp.Analyzers.Cli.

## Using Analyzers
Expand Down
3 changes: 2 additions & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"sdk": {
"version": "9.0.101"
"version": "9.0.101",
"rollForward": "latestMinor"
}
}
5 changes: 5 additions & 0 deletions samples/BadOptionUsage.fsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@


let value = Some 42

printfn "The value is: %d" value.Value // This will cause a warning from the OptionAnalyzer
163 changes: 123 additions & 40 deletions src/FSharp.Analyzers.Cli/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,29 @@ open Ionide.ProjInfo
open FSharp.Analyzers.Cli
open FSharp.Analyzers.Cli.CustomLogging


type ExitErrorCodes =
Copy link
Member Author

Choose a reason for hiding this comment

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

Since we have exit 1 in like 100 places, I decided it's time to make exit codes specific to errors.

| Success = 0
| NoAnalyzersFound = -1
| AnalyzerFoundError = -2
| FailedAssemblyLoading = -3
| AnalysisAborted = -4
| FailedToLoadProject = 10
| EmptyFscArgs = 11
| MissingPropertyValue = 12
| RuntimeAndOsOptions = 13
| RuntimeAndArchOptions = 14
| UnknownLoggerVerbosity = 15
| AnalyzerListedMultipleTimesInTreatAsSeverity = 16
| FscArgsCombinedWithMsBuildProperties = 17
| FSharpCoreAssemblyLoadFailed = 18
| ProjectAndFscArgs = 19
| InvalidScriptArguments = 20
| InvalidProjectArguments = 21

type Arguments =
| Project of string list
| Script of string list
| Analyzers_Path of string list
| [<EqualsAssignment; AltCommandLine("-p:"); AltCommandLine("-p")>] Property of string * string
| [<Unique; AltCommandLine("-c")>] Configuration of string
Expand All @@ -40,7 +61,8 @@ type Arguments =
interface IArgParserTemplate with
member s.Usage =
match s with
| Project _ -> "List of paths to your .fsproj file."
| Project _ -> "List of paths to your .fsproj file. Cannot be combined with `--fsc-args`."
| Script _ -> "List of paths to your .fsx file. Supports globs. Cannot be combined with `--fsc-args`."
| Analyzers_Path _ ->
"List of path to a folder where your analyzers are located. This will search recursively."
| Property _ -> "A key=value pair of an MSBuild property."
Expand Down Expand Up @@ -153,7 +175,7 @@ let loadProjects toolsPath properties (projPaths: string list) =

if Seq.length failedLoads > 0 then
logger.LogError("Failed to load project '{0}'", failedLoads)
exit 1
exit (int ExitErrorCodes.FailedToLoadProject)

let loaded =
FCS.mapManyOptions projectOptions
Expand Down Expand Up @@ -235,7 +257,7 @@ let runFscArgs
=
if String.IsNullOrWhiteSpace fscArgs then
logger.LogError("Empty --fsc-args were passed!")
exit 1
exit (int ExitErrorCodes.EmptyFscArgs)
else

let fscArgs = fscArgs.Split(';', StringSplitOptions.RemoveEmptyEntries)
Expand Down Expand Up @@ -480,7 +502,7 @@ let expandMultiProperties (properties: (string * string) list) =
match pair with
| [| k; v |] when String.IsNullOrWhiteSpace(v) ->
logger.LogError("Missing property value for '{0}'", k)
exit 1
exit (int ExitErrorCodes.MissingPropertyValue)
| [| k; v |] -> yield (k, v)
| _ -> ()

Expand All @@ -492,10 +514,10 @@ let validateRuntimeOsArchCombination (runtime, arch, os) =
match runtime, os, arch with
| Some _, Some _, _ ->
logger.LogError("Specifying both the `-r|--runtime` and `-os` options is not supported.")
exit 1
exit (int ExitErrorCodes.RuntimeAndOsOptions)
| Some _, _, Some _ ->
logger.LogError("Specifying both the `-r|--runtime` and `-a|--arch` options is not supported.")
exit 1
exit (int ExitErrorCodes.RuntimeAndArchOptions)
| _ -> ()

let getProperties (results: ParseResults<Arguments>) =
Expand Down Expand Up @@ -533,6 +555,7 @@ let getProperties (results: ParseResults<Arguments>) =
| _ -> ()
]


[<EntryPoint>]
let main argv =
let toolsPath = Init.init (DirectoryInfo Environment.CurrentDirectory) None
Expand All @@ -554,7 +577,7 @@ let main argv =
use factory = LoggerFactory.Create(fun b -> b.AddConsole() |> ignore)
let logger = factory.CreateLogger("")
logger.LogError("unknown verbosity level given {0}", x)
exit 1
exit (int ExitErrorCodes.UnknownLoggerVerbosity)

use factory =
LoggerFactory.Create(fun builder ->
Expand Down Expand Up @@ -588,12 +611,35 @@ let main argv =
if not (severityMapping.IsValid()) then
logger.LogError("An analyzer code may only be listed once in the <treat-as-severity> arguments.")

exit 1
exit (int ExitErrorCodes.AnalyzerListedMultipleTimesInTreatAsSeverity)

let projOpts = results.GetResults <@ Project @> |> List.concat
let fscArgs = results.TryGetResult <@ FSC_Args @>
let report = results.TryGetResult <@ Report @>
let codeRoot = results.TryGetResult <@ Code_Root @>
let cwd = Directory.GetCurrentDirectory() |> DirectoryInfo

let beginsWithCurrentPath (path: string) =
path.StartsWith("./") || path.StartsWith(".\\")

let scripts =
results.GetResult(<@ Script @>, [])
|> List.collect(fun scriptGlob ->
let root, scriptGlob =
if Path.IsPathRooted scriptGlob then
// Glob can't handle absolute paths, so we need to make sure the scriptGlob is a relative path
let root = Path.GetPathRoot scriptGlob
let glob = scriptGlob.Substring(root.Length)
DirectoryInfo root, glob
else if beginsWithCurrentPath scriptGlob then
// Glob can't handle relative paths starting with "./" or ".\", so we need trim it
let relativeGlob = scriptGlob.Substring(2) // remove "./" or ".\"
cwd, relativeGlob
else
cwd, scriptGlob
Comment on lines +629 to +639
Copy link
Member Author

Choose a reason for hiding this comment

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

Some annoying path math

Copy link
Contributor

Choose a reason for hiding this comment

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

If Glob doesn't support this stuff and hasn't been updated for some time, is it worth seeing if Microsoft.Extensions.FileSystemGlobbing would work? (as it's still updated, and doesn't have any depenencies)

Copy link
Member Author

Choose a reason for hiding this comment

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

Ooo didn’t know this existed. Would gladly replace it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ended up being a little more difficult because of how we curently use and I didn't want to spend that much time on it. Could be a good change for a subsequent PR.


root.GlobFiles scriptGlob |> Seq.map (fun file -> file.FullName) |> Seq.toList
)

let exclInclFiles =
let excludeFiles = results.GetResult(<@ Exclude_Files @>, [])
Expand All @@ -616,7 +662,7 @@ let main argv =

if Option.isSome fscArgs && not properties.IsEmpty then
logger.LogError("fsc-args can't be combined with MSBuild properties.")
exit 1
exit (int ExitErrorCodes.FscArgsCombinedWithMsBuildProperties)

properties
|> List.iter (fun (k, v) -> logger.LogInformation("Property {0}={1}", k, v))
Expand Down Expand Up @@ -675,7 +721,7 @@ let main argv =
"""

logger.LogError(msg)
exit 1
exit (int ExitErrorCodes.FSharpCoreAssemblyLoadFailed)
)

let client = Client<CliAnalyzerAttribute, CliContext>(logger)
Expand All @@ -696,39 +742,76 @@ let main argv =
if analyzers = 0 then
None
else
match projOpts, fscArgs with
| [], None ->
logger.LogError("No project given. Use `--project PATH_TO_FSPROJ`.")
None
| _ :: _, Some _ ->
match fscArgs with
| Some _ when projOpts |> List.isEmpty |> not ->
logger.LogError("`--project` and `--fsc-args` cannot be combined.")
exit 1
| [], Some fscArgs ->
exit (int ExitErrorCodes.ProjectAndFscArgs)
| Some _ when scripts |> List.isEmpty |> not ->
logger.LogError("`--script` and `--fsc-args` cannot be combined.")
exit (int ExitErrorCodes.ProjectAndFscArgs)
| Some fscArgs ->
runFscArgs client fscArgs exclInclFiles severityMapping
|> Async.RunSynchronously
|> Some
| projects, None ->
for projPath in projects do
if not (File.Exists(projPath)) then
logger.LogError("Invalid `--project` argument. File does not exist: '{projPath}'", projPath)
exit 1

async {
let! loadedProjects = loadProjects toolsPath properties projects

return!
loadedProjects
|> List.map (fun (projPath: FSharpProjectOptions) ->
runProject client projPath exclInclFiles severityMapping
| None ->
match projOpts, scripts with
| [], [] ->
logger.LogError("No projects or scripts were specified. Use `--project` or `--script` to specify them.")
exit (int ExitErrorCodes.EmptyFscArgs)
| projects, scripts ->

for script in scripts do
if not (File.Exists(script)) then
logger.LogError("Invalid `--script` argument. File does not exist: '{script}'", script)
exit (int ExitErrorCodes.InvalidProjectArguments)

let scriptOptions =
scripts
|> List.map(fun script -> async {
let! fileContent = File.ReadAllTextAsync script |> Async.AwaitTask
let sourceText = SourceText.ofString fileContent
// GetProjectOptionsFromScript cannot be run in parallel, it is not thread-safe.
let! options, diagnostics = fcs.GetProjectOptionsFromScript(script, sourceText)
if not (List.isEmpty diagnostics) then
diagnostics
|> List.iter (fun d ->
logger.LogError(
"Script {0} has a diagnostic: {1} at {2}",
script,
d.Message,
d.Range
)
)
return options
}
)
|> Async.Parallel
}
|> Async.RunSynchronously
|> List.concat
|> Some
|> Async.Sequential

for projPath in projects do
if not (File.Exists(projPath)) then
logger.LogError("Invalid `--project` argument. File does not exist: '{projPath}'", projPath)
exit (int ExitErrorCodes.InvalidProjectArguments)
async {
let! scriptOptions = scriptOptions |> Async.StartChild
let! loadedProjects = loadProjects toolsPath properties projects |> Async.StartChild
let! loadedProjects = loadedProjects
let! scriptOptions = scriptOptions

let loadedProjects = Array.toList scriptOptions @ loadedProjects

return!
loadedProjects
|> List.map (fun (projPath: FSharpProjectOptions) ->
runProject client projPath exclInclFiles severityMapping
)
|> Async.Parallel
}
|> Async.RunSynchronously
|> List.concat
|> Some

match results with
| None -> -1
| None -> int ExitErrorCodes.NoAnalyzersFound
| Some results ->
let results, hasError =
match Result.allOkOrError results with
Expand Down Expand Up @@ -762,8 +845,8 @@ let main argv =
failedAssemblies
)

exit -3
exit (int ExitErrorCodes.FailedAssemblyLoading)

if check then -2
elif hasError then -4
else 0
if check then (int ExitErrorCodes.AnalyzerFoundError)
elif hasError then (int ExitErrorCodes.AnalysisAborted)
else (int ExitErrorCodes.Success)