diff --git a/Directory.Packages.props b/Directory.Packages.props index fcb2aa4..ebdf4b2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,7 +10,7 @@ - + diff --git a/docs/index.md b/docs/index.md index 35743d5..37f5f51 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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 diff --git a/global.json b/global.json index b6b5c9f..93e6dbd 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,6 @@ { "sdk": { - "version": "9.0.101" + "version": "9.0.101", + "rollForward": "latestMinor" } } diff --git a/samples/BadOptionUsage.fsx b/samples/BadOptionUsage.fsx new file mode 100644 index 0000000..df4ed70 --- /dev/null +++ b/samples/BadOptionUsage.fsx @@ -0,0 +1,5 @@ + + +let value = Some 42 + +printfn "The value is: %d" value.Value // This will cause a warning from the OptionAnalyzer \ No newline at end of file diff --git a/src/FSharp.Analyzers.Cli/Program.fs b/src/FSharp.Analyzers.Cli/Program.fs index f4f27a3..303be22 100644 --- a/src/FSharp.Analyzers.Cli/Program.fs +++ b/src/FSharp.Analyzers.Cli/Program.fs @@ -15,8 +15,29 @@ open Ionide.ProjInfo open FSharp.Analyzers.Cli open FSharp.Analyzers.Cli.CustomLogging + +type ExitErrorCodes = + | 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 | [] Property of string * string | [] Configuration of string @@ -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." @@ -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 @@ -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) @@ -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) | _ -> () @@ -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) = @@ -533,6 +555,7 @@ let getProperties (results: ParseResults) = | _ -> () ] + [] let main argv = let toolsPath = Init.init (DirectoryInfo Environment.CurrentDirectory) None @@ -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 -> @@ -588,12 +611,35 @@ let main argv = if not (severityMapping.IsValid()) then logger.LogError("An analyzer code may only be listed once in the 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 + + root.GlobFiles scriptGlob |> Seq.map (fun file -> file.FullName) |> Seq.toList + ) let exclInclFiles = let excludeFiles = results.GetResult(<@ Exclude_Files @>, []) @@ -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)) @@ -675,7 +721,7 @@ let main argv = """ logger.LogError(msg) - exit 1 + exit (int ExitErrorCodes.FSharpCoreAssemblyLoadFailed) ) let client = Client(logger) @@ -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 @@ -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)