diff --git a/.github/workflows/build+test+deploy.yml b/.github/workflows/build+test+deploy.yml index 1aa9c4182..b9e05b5b6 100644 --- a/.github/workflows/build+test+deploy.yml +++ b/.github/workflows/build+test+deploy.yml @@ -165,7 +165,7 @@ jobs: - name: Add .NET tools to PATH run: echo "$HOME/.dotnet/tools" >> $GITHUB_PATH - name: Lint FSharpLint.Console project (net8.0 only) - run: dotnet fsharplint lint ./src/FSharpLint.Console/FSharpLint.Console.fsproj --framework net8.0 + run: dotnet fsharplint lint ./src/FSharpLint.Console/Program.fs testReleaseBinariesWithDotNet10: needs: packReleaseBinaries diff --git a/src/FSharpLint.Console/Program.fs b/src/FSharpLint.Console/Program.fs index d62691ed3..6d282f398 100644 --- a/src/FSharpLint.Console/Program.fs +++ b/src/FSharpLint.Console/Program.fs @@ -41,7 +41,7 @@ with // TODO: investigate erroneous warning on this type definition // fsharplint:disable UnionDefinitionIndentation and private LintArgs = - | [] Target of target:string + | [] Target of target:string | [] Lint_Config of lintConfig:string | File_Type of FileType // fsharplint:enable UnionDefinitionIndentation @@ -54,6 +54,11 @@ with | Lint_Config _ -> "Path to the config for the lint." // fsharplint:enable UnionCasesNames +let errorHandler = ProcessExiter(colorizer = function + | ErrorCode.HelpText -> None + | _ -> Some ConsoleColor.Red) +let private parser = ArgumentParser.Create(programName = "fsharplint", errorHandler = errorHandler) + /// Expands a wildcard pattern to a list of matching files. /// Supports recursive search using ** (e.g., "**/*.fs" or "src/**/*.fs") let internal expandWildcard (pattern:string) = @@ -105,17 +110,17 @@ let internal containsWildcard (target:string) = target.Contains("*") || target.Contains("?") /// Infers the file type of the target based on its file extension. -let internal inferFileType (target:string) = +let internal inferFileType (target:string) : Option = if containsWildcard target then - FileType.Wildcard + Some FileType.Wildcard else if target.EndsWith ".fs" || target.EndsWith ".fsx" then - FileType.File + Some FileType.File else if target.EndsWith ".fsproj" then - FileType.Project + Some FileType.Project else if target.EndsWith ".slnx" || target.EndsWith ".slnf" || target.EndsWith ".sln" then - FileType.Solution + Some FileType.Solution else - FileType.Source + None let private lint (lintArgs: ParseResults) @@ -153,15 +158,21 @@ let private lint } let target = lintArgs.GetResult Target - let fileType = lintArgs.TryGetResult File_Type |> Option.defaultValue (inferFileType target) + + if target.StartsWith "-" then + let usage = parser.PrintUsage() + handleError <| sprintf "ERROR: unrecognized argument: '%s'.%s%s" target Environment.NewLine usage + exit <| int exitCode + + let fileType = lintArgs.TryGetResult File_Type |> Option.orElse (inferFileType target) try let lintResult = match fileType with - | FileType.File -> Lint.asyncLintFile lintParams target |> Async.RunSynchronously - | FileType.Source -> Lint.asyncLintSource lintParams target |> Async.RunSynchronously - | FileType.Solution -> Lint.asyncLintSolution lintParams target toolsPath |> Async.RunSynchronously - | FileType.Wildcard -> + | Some FileType.File -> Lint.asyncLintFile lintParams target |> Async.RunSynchronously + | Some FileType.Source -> Lint.asyncLintSource lintParams target |> Async.RunSynchronously + | Some FileType.Solution -> Lint.asyncLintSolution lintParams target toolsPath |> Async.RunSynchronously + | Some FileType.Wildcard -> output.WriteInfo "Wildcard detected, but not recommended. Using a project (slnx/sln/fsproj) can detect more issues." let files = expandWildcard target if List.isEmpty files then @@ -170,12 +181,13 @@ let private lint else output.WriteInfo $"Found %d{List.length files} file(s) matching pattern '%s{target}'." Lint.asyncLintFiles lintParams files |> Async.RunSynchronously - | FileType.Project - | _ -> Lint.asyncLintProject lintParams target toolsPath |> Async.RunSynchronously + | Some FileType.Project -> Lint.asyncLintProject lintParams target toolsPath |> Async.RunSynchronously + | Some unknownFileType -> failwith $"Unknown file type: {unknownFileType}" + | None -> LintResult.Failure (FailedToInferInputType target) handleLintResult lintResult with | exn -> - let target = if fileType = FileType.Source then "source" else target + let target = if fileType = Some FileType.Source then "source" else target handleError $"Lint failed while analysing %s{target}.{Environment.NewLine}Failed with: %s{exn.Message}{Environment.NewLine}Stack trace: {exn.StackTrace}" @@ -212,10 +224,6 @@ let toolsPath = Ionide.ProjInfo.Init.init (DirectoryInfo <| Directory.GetCurrent [] let main argv = - let errorHandler = ProcessExiter(colorizer = function - | ErrorCode.HelpText -> None - | _ -> Some ConsoleColor.Red) - let parser = ArgumentParser.Create(programName = "fsharplint", errorHandler = errorHandler) let parseResults = parser.ParseCommandLine argv start parseResults toolsPath |> int diff --git a/src/FSharpLint.Core/Application/Lint.fs b/src/FSharpLint.Core/Application/Lint.fs index 86f2f4c8c..37b802ba4 100644 --- a/src/FSharpLint.Core/Application/Lint.fs +++ b/src/FSharpLint.Core/Application/Lint.fs @@ -57,6 +57,9 @@ module Lint = /// `FSharp.Compiler.Services` failed when trying to parse one or more files in a project. | FailedToParseFilesInProject of ParseFile.ParseFileFailure list + /// Failed to infer input type from target + | FailedToInferInputType of string + member this.Description with get() = let getParseFailureReason = function @@ -83,6 +86,8 @@ module Lint = | FailedToParseFilesInProject failures -> let failureReasons = String.Join("\n", failures |> List.map getParseFailureReason) $"Lint failed to parse files. Failed with: {failureReasons}" + | FailedToInferInputType target -> + $"Input type could not be inferred from target '{target}'. Explicitly set input type using --file-type parameter." [] type Result<'SuccessType> = diff --git a/src/FSharpLint.Core/Application/Lint.fsi b/src/FSharpLint.Core/Application/Lint.fsi index 618d48317..551714dd8 100644 --- a/src/FSharpLint.Core/Application/Lint.fsi +++ b/src/FSharpLint.Core/Application/Lint.fsi @@ -105,6 +105,9 @@ module Lint = /// `FSharp.Compiler.Services` failed when trying to parse one or more files in a project. | FailedToParseFilesInProject of ParseFile.ParseFileFailure list + /// Failed to infer input type from target + | FailedToInferInputType of string + member Description: string type Context = diff --git a/tests/FSharpLint.Console.Tests/TestApp.fs b/tests/FSharpLint.Console.Tests/TestApp.fs index 432d233d3..39a99c412 100644 --- a/tests/FSharpLint.Console.Tests/TestApp.fs +++ b/tests/FSharpLint.Console.Tests/TestApp.fs @@ -56,7 +56,7 @@ type TestConsoleApplication() = abstract member PathName : string """ - let (returnCode, errors) = main [| "lint"; input |] + let (returnCode, errors) = main [| "lint"; "--file-type"; "source"; input |] Assert.AreEqual(int ExitCode.Failure, returnCode) Assert.AreEqual(set ["Consider changing `Signature` to be prefixed with `I`."], errors) @@ -78,7 +78,7 @@ type TestConsoleApplication() = abstract member PathName : string """ - let (returnCode, errors) = main [| "lint"; "--lint-config"; config.FileName; input |] + let (returnCode, errors) = main [| "lint"; "--lint-config"; config.FileName; "--file-type"; "source"; input |] Assert.AreEqual(int ExitCode.Success, returnCode) Assert.AreEqual(Set.empty, errors) @@ -92,7 +92,7 @@ type TestConsoleApplication() = abstract member PathName : string """ - let (returnCode, errors) = main [| "lint"; input |] + let (returnCode, errors) = main [| "lint"; "--file-type"; "source"; input |] Assert.AreEqual(int ExitCode.Success, returnCode) Assert.AreEqual(Set.empty, errors) @@ -114,7 +114,7 @@ type TestConsoleApplication() = type X = int Generic """ - let (returnCode, errors) = main [| "lint"; "--lint-config"; config.FileName; input |] + let (returnCode, errors) = main [| "lint"; "--lint-config"; config.FileName; "--file-type"; "source"; input |] Assert.AreEqual(int ExitCode.Failure, returnCode) Assert.AreEqual(set ["Use prefix syntax for generic type."], errors) @@ -128,8 +128,6 @@ type TestFileTypeInference() = [] [] [] - [] - [] [] [] [] @@ -140,7 +138,14 @@ type TestFileTypeInference() = [] member _.``File type inference test cases``(filename: string, expectedType: int) = let result = FSharpLint.Console.Program.inferFileType filename - let expectedType = enum(expectedType) + let expectedType = Some <| enum(expectedType) + Assert.AreEqual(expectedType, result) + + [] + [] + member _.``File type inference undecided test cases``(filename: string) = + let result = FSharpLint.Console.Program.inferFileType filename + let expectedType = None Assert.AreEqual(expectedType, result) [] diff --git a/tests/FSharpLint.FunctionalTest/TestConsoleApplication.fs b/tests/FSharpLint.FunctionalTest/TestConsoleApplication.fs index bd6f9cb06..abe647824 100644 --- a/tests/FSharpLint.FunctionalTest/TestConsoleApplication.fs +++ b/tests/FSharpLint.FunctionalTest/TestConsoleApplication.fs @@ -140,3 +140,20 @@ module Tests = $"Did not find the following expected errors: [{expectedMissingStr}]\n" + $"Found the following unexpected warnings: [{notExpectedStr}]\n" + $"Complete output: {output}") + + [] + member _.InvalidArgument() = + let invalidArgument = $"lint --invalidArg someValue" + let output = dotnetFslint invalidArgument + + Assert.IsTrue(output.Contains "unrecognized argument") + + let invalidArgument2 = $"lint -hlp" + let output2 = dotnetFslint invalidArgument2 + + Assert.IsTrue(output2.Contains "unrecognized argument") + + let multipleTargets = $"lint foo bar" + let output3 = dotnetFslint multipleTargets + + Assert.IsTrue(output3.Contains "unrecognized argument")