From e290e99f7e81f4dc2ab4dce4ee686163d992e8a4 Mon Sep 17 00:00:00 2001 From: webwarrior-ws Date: Wed, 7 Jan 2026 10:08:37 +0100 Subject: [PATCH 1/4] FunctionalTest: test for invalid args with lint subcommand Added a test that checks if passing invalid argument(s) to "fsharplint lint" results in "unrecognized argument" error. --- .../TestConsoleApplication.fs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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") From 11dafb4b8f71699a504b5eab994a2d7462f0792f Mon Sep 17 00:00:00 2001 From: webwarrior-ws Date: Tue, 6 Jan 2026 14:05:12 +0100 Subject: [PATCH 2/4] Console: print help on unrecognized argument If unknown named argument is passed to "fsharplint lint" or several targets are passed instead of one, show "unrecognized argument" error and print usage. Declare parser beforehand so that it can be used in other functions e.g. to print usage. Fixes https://github.com/fsprojects/FSharpLint/issues/800 --- src/FSharpLint.Console/Program.fs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/FSharpLint.Console/Program.fs b/src/FSharpLint.Console/Program.fs index d62691ed3..65569949a 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) = @@ -153,6 +158,12 @@ let private lint } let target = lintArgs.GetResult 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.defaultValue (inferFileType target) try @@ -212,10 +223,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 From 208fd8342342c64e650ac5c63a43c251bac6a7cd Mon Sep 17 00:00:00 2001 From: webwarrior-ws Date: Wed, 7 Jan 2026 10:27:39 +0100 Subject: [PATCH 3/4] GithubCI: remove incorrect arg in .NET8 job Also lint single file instead of project because project is using .NET 9 and that leads to error: ``` The current .NET SDK does not support targeting .NET 9.0. Either target .NET 8.0 or lower, or use a version of the .NET SDK that supports .NET 9.0. Download the .NET SDK from https://aka.ms/dotnet/download ``` --- .github/workflows/build+test+deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 5ee3cb75e3b9f0a60c216f962ed9e4780e701fa6 Mon Sep 17 00:00:00 2001 From: webwarrior-ws Date: Mon, 12 Jan 2026 09:59:29 +0100 Subject: [PATCH 4/4] Console,Core,Tests(Console): stop falling back to inline source When target type cannot be determined. Instead, show an error prompting to explicitly set input type using --file-type parameter. --- src/FSharpLint.Console/Program.fs | 29 ++++++++++++----------- src/FSharpLint.Core/Application/Lint.fs | 5 ++++ src/FSharpLint.Core/Application/Lint.fsi | 3 +++ tests/FSharpLint.Console.Tests/TestApp.fs | 19 +++++++++------ 4 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/FSharpLint.Console/Program.fs b/src/FSharpLint.Console/Program.fs index 65569949a..6d282f398 100644 --- a/src/FSharpLint.Console/Program.fs +++ b/src/FSharpLint.Console/Program.fs @@ -110,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) @@ -164,15 +164,15 @@ let private lint handleError <| sprintf "ERROR: unrecognized argument: '%s'.%s%s" target Environment.NewLine usage exit <| int exitCode - let fileType = lintArgs.TryGetResult File_Type |> Option.defaultValue (inferFileType target) + 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 @@ -181,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}" 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) []