Skip to content
Open
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 .github/workflows/build+test+deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 27 additions & 19 deletions src/FSharpLint.Console/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ with
// TODO: investigate erroneous warning on this type definition
// fsharplint:disable UnionDefinitionIndentation
and private LintArgs =
| [<MainCommand; Mandatory>] Target of target:string
| [<MainCommand; Mandatory; Unique>] Target of target:string
| [<AltCommandLine("-l")>] Lint_Config of lintConfig:string
| File_Type of FileType
// fsharplint:enable UnionDefinitionIndentation
Expand All @@ -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<ToolArgs>(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) =
Expand Down Expand Up @@ -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<FileType> =
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<LintArgs>)
Expand Down Expand Up @@ -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
Expand All @@ -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}"

Expand Down Expand Up @@ -212,10 +224,6 @@ let toolsPath = Ionide.ProjInfo.Init.init (DirectoryInfo <| Directory.GetCurrent

[<EntryPoint>]
let main argv =
let errorHandler = ProcessExiter(colorizer = function
| ErrorCode.HelpText -> None
| _ -> Some ConsoleColor.Red)
let parser = ArgumentParser.Create<ToolArgs>(programName = "fsharplint", errorHandler = errorHandler)
let parseResults = parser.ParseCommandLine argv
start parseResults toolsPath
|> int
5 changes: 5 additions & 0 deletions src/FSharpLint.Core/Application/Lint.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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."

[<NoComparison>]
type Result<'SuccessType> =
Expand Down
3 changes: 3 additions & 0 deletions src/FSharpLint.Core/Application/Lint.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
19 changes: 12 additions & 7 deletions tests/FSharpLint.Console.Tests/TestApp.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<string>, errors)
Expand All @@ -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<string>, errors)
Expand All @@ -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)
Expand All @@ -128,8 +128,6 @@ type TestFileTypeInference() =
[<TestCase("MySolution.sln", FileType.Solution, TestName = "inferFileType must recognize .sln files as Solution type")>]
[<TestCase("MySolution.slnx", FileType.Solution, TestName = "inferFileType must recognize .slnx files as Solution type")>]
[<TestCase("MySolution.slnf", FileType.Solution, TestName = "inferFileType must recognize .slnf files as Solution type")>]
[<TestCase("unknown.txt", FileType.Source, TestName = "inferFileType must treat unknown extensions as Source type")>]
[<TestCase("noextension", FileType.Source, TestName = "inferFileType must treat files without extensions as Source type")>]
[<TestCase("src/MyProject/Program.fs", FileType.File, TestName = "inferFileType must handle .fs files in directories correctly")>]
[<TestCase(@"C:\Projects\MySolution.slnx", FileType.Solution, TestName = "inferFileType must handle .slnx files with full paths correctly")>]
[<TestCase(@"C:\Projects\MySolution.slnf", FileType.Solution, TestName = "inferFileType must handle .slnf files with full paths correctly")>]
Expand All @@ -140,7 +138,14 @@ type TestFileTypeInference() =
[<TestCase("test?.fs", FileType.Wildcard, TestName = "inferFileType must recognize wildcard patterns with ? as Wildcard type")>]
member _.``File type inference test cases``(filename: string, expectedType: int) =
let result = FSharpLint.Console.Program.inferFileType filename
let expectedType = enum<FileType>(expectedType)
let expectedType = Some <| enum<FileType>(expectedType)
Assert.AreEqual(expectedType, result)

[<TestCase("unknown.txt", TestName = "inferFileType must treat unknown extensions as undecided")>]
[<TestCase("noextension", TestName = "inferFileType must treat files without extensions as undecided")>]
member _.``File type inference undecided test cases``(filename: string) =
let result = FSharpLint.Console.Program.inferFileType filename
let expectedType = None
Assert.AreEqual(expectedType, result)

[<TestFixture>]
Expand Down
17 changes: 17 additions & 0 deletions tests/FSharpLint.FunctionalTest/TestConsoleApplication.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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}")

[<Test>]
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")
Loading