Skip to content

Commit 33ee73e

Browse files
authored
Merge PR #777 from Thorium/wildcard-support
Wildcard support added. Fixes #776
2 parents 74962b8 + 65374c3 commit 33ee73e

File tree

4 files changed

+130
-4
lines changed

4 files changed

+130
-4
lines changed

src/FSharpLint.Console/Program.fs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
module FSharpLint.Console.Program
1+
module FSharpLint.Console.Program
22

33
open Argu
44
open System
@@ -18,6 +18,7 @@ type internal FileType =
1818
| Solution = 2
1919
| File = 3
2020
| Source = 4
21+
| Wildcard = 5
2122

2223
// Allowing underscores in union case names for proper Argu command line option formatting.
2324
// fsharplint:disable UnionCasesNames
@@ -49,6 +50,42 @@ with
4950
| Lint_Config _ -> "Path to the config for the lint."
5051
// fsharplint:enable UnionCasesNames
5152

53+
/// Expands a wildcard pattern to a list of matching files.
54+
/// Supports recursive search using ** (e.g., "**/*.fs" or "src/**/*.fs")
55+
let internal expandWildcard (pattern:string) =
56+
let isFSharpFile (filePath:string) =
57+
filePath.EndsWith ".fs" || filePath.EndsWith ".fsx"
58+
59+
let normalizedPattern = pattern.Replace('\\', '/')
60+
61+
let directory, searchPattern, searchOption =
62+
match normalizedPattern.IndexOf "**/" with
63+
| -1 ->
64+
// Non-recursive pattern
65+
match normalizedPattern.LastIndexOf '/' with
66+
| -1 -> (".", normalizedPattern, SearchOption.TopDirectoryOnly)
67+
| lastSeparator ->
68+
let dir = normalizedPattern.Substring(0, lastSeparator)
69+
let pat = normalizedPattern.Substring(lastSeparator + 1)
70+
((if String.IsNullOrEmpty dir then "." else dir), pat, SearchOption.TopDirectoryOnly)
71+
| 0 ->
72+
// Pattern starts with **/
73+
let pat = normalizedPattern.Substring 3
74+
(".", pat, SearchOption.AllDirectories)
75+
| doubleStarIndex ->
76+
// Pattern has **/ in the middle
77+
let dir = normalizedPattern.Substring(0, doubleStarIndex).TrimEnd '/'
78+
let pat = normalizedPattern.Substring(doubleStarIndex + 3)
79+
(dir, pat, SearchOption.AllDirectories)
80+
81+
let fullDirectory = Path.GetFullPath directory
82+
if Directory.Exists fullDirectory then
83+
Directory.GetFiles(fullDirectory, searchPattern, searchOption)
84+
|> Array.filter isFSharpFile
85+
|> Array.toList
86+
else
87+
List.empty
88+
5289
let private parserProgress (output:Output.IOutput) = function
5390
| Starting file ->
5491
String.Format(Resources.GetString("ConsoleStartingFile"), file) |> output.WriteInfo
@@ -59,9 +96,15 @@ let private parserProgress (output:Output.IOutput) = function
5996
output.WriteError
6097
$"Exception Message:{Environment.NewLine}{parseException.Message}{Environment.NewLine}Exception Stack Trace:{Environment.NewLine}{parseException.StackTrace}{Environment.NewLine}"
6198

99+
/// Checks if a string contains wildcard characters.
100+
let internal containsWildcard (target:string) =
101+
target.Contains("*") || target.Contains("?")
102+
62103
/// Infers the file type of the target based on its file extension.
63104
let internal inferFileType (target:string) =
64-
if target.EndsWith ".fs" || target.EndsWith ".fsx" then
105+
if containsWildcard target then
106+
FileType.Wildcard
107+
else if target.EndsWith ".fs" || target.EndsWith ".fsx" then
65108
FileType.File
66109
else if target.EndsWith ".fsproj" then
67110
FileType.Project
@@ -125,6 +168,15 @@ let private start (arguments:ParseResults<ToolArgs>) (toolsPath:Ionide.ProjInfo.
125168
| FileType.File -> Lint.lintFile lintParams target
126169
| FileType.Source -> Lint.lintSource lintParams target
127170
| FileType.Solution -> Lint.lintSolution lintParams target toolsPath
171+
| FileType.Wildcard ->
172+
output.WriteInfo $"Wildcard detected, but not recommended. Using a project (slnx/sln/fsproj) can detect more issues."
173+
let files = expandWildcard target
174+
if List.isEmpty files then
175+
output.WriteInfo $"No files matching pattern '%s{target}' were found."
176+
LintResult.Success List.empty
177+
else
178+
output.WriteInfo $"Found %d{List.length files} file(s) matching pattern '%s{target}'."
179+
Lint.lintFiles lintParams files
128180
| FileType.Project
129181
| _ -> Lint.lintProject lintParams target toolsPath
130182
handleLintResult lintResult

src/FSharpLint.Core/Application/Lint.fs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace FSharpLint.Application
1+
namespace FSharpLint.Application
22

33
open System
44
open System.Collections.Concurrent
@@ -643,3 +643,36 @@ module Lint =
643643
else
644644
FailedToLoadFile filePath
645645
|> LintResult.Failure
646+
647+
/// Lints multiple F# files from given file paths.
648+
let lintFiles optionalParams filePaths =
649+
let checker = FSharpChecker.Create(keepAssemblyContents=true)
650+
651+
match getConfig optionalParams.Configuration with
652+
| Ok config ->
653+
let optionalParams = { optionalParams with Configuration = ConfigurationParam.Configuration config }
654+
655+
let lintSingleFile filePath =
656+
if IO.File.Exists filePath then
657+
match ParseFile.parseFile filePath checker None with
658+
| ParseFile.Success astFileParseInfo ->
659+
let parsedFileInfo =
660+
{ Source = astFileParseInfo.Text
661+
Ast = astFileParseInfo.Ast
662+
TypeCheckResults = astFileParseInfo.TypeCheckResults }
663+
lintParsedFile optionalParams parsedFileInfo filePath
664+
| ParseFile.Failed failure ->
665+
LintResult.Failure (FailedToParseFile failure)
666+
else
667+
LintResult.Failure (FailedToLoadFile filePath)
668+
669+
let results = filePaths |> Seq.map lintSingleFile |> Seq.toList
670+
671+
let failures = results |> List.choose (function | LintResult.Failure failure -> Some failure | _ -> None)
672+
let warnings = results |> List.collect (function | LintResult.Success warning -> warning | _ -> List.empty)
673+
674+
match failures with
675+
| firstFailure :: _ -> LintResult.Failure firstFailure
676+
| [] -> LintResult.Success warnings
677+
| Error err ->
678+
LintResult.Failure (RunTimeConfigError err)

src/FSharpLint.Core/Application/Lint.fsi

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace FSharpLint.Application
1+
namespace FSharpLint.Application
22

33
/// Provides an API to manage/load FSharpLint configuration files.
44
/// <see cref="FSharpLint.Framework.Configuration" /> for more information on
@@ -163,6 +163,9 @@ module Lint =
163163
/// Lints an F# file from a given path to the `.fs` file.
164164
val lintFile : optionalParams:OptionalLintParameters -> filePath:string -> LintResult
165165

166+
/// Lints multiple F# files from given file paths.
167+
val lintFiles : optionalParams:OptionalLintParameters -> filePaths:string seq -> LintResult
168+
166169
/// Lints an F# file that has already been parsed using
167170
/// `FSharp.Compiler.Services` in the calling application.
168171
val lintParsedFile : optionalParams:OptionalLintParameters -> parsedFileInfo:ParsedFileInformation -> filePath:string -> LintResult

tests/FSharpLint.Console.Tests/TestApp.fs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,45 @@ type TestFileTypeInference() =
135135
[<TestCase(@"C:\Projects\MySolution.slnx", FileType.Solution, TestName = "inferFileType must handle .slnx files with full paths correctly")>]
136136
[<TestCase(@"C:\Projects\MySolution.slnf", FileType.Solution, TestName = "inferFileType must handle .slnf files with full paths correctly")>]
137137
[<TestCase("../MyProject.fsproj", FileType.Project, TestName = "inferFileType must handle .fsproj files with relative paths correctly")>]
138+
[<TestCase("*.fs", FileType.Wildcard, TestName = "inferFileType must recognize wildcard patterns with * as Wildcard type")>]
139+
[<TestCase("**/*.fs", FileType.Wildcard, TestName = "inferFileType must recognize recursive wildcard patterns as Wildcard type")>]
140+
[<TestCase("src/**/*.fsx", FileType.Wildcard, TestName = "inferFileType must recognize subdirectory recursive wildcard patterns as Wildcard type")>]
141+
[<TestCase("test?.fs", FileType.Wildcard, TestName = "inferFileType must recognize wildcard patterns with ? as Wildcard type")>]
138142
member _.``File type inference test cases``(filename: string, expectedType: int) =
139143
let result = FSharpLint.Console.Program.inferFileType filename
140144
let expectedType = enum<FileType>(expectedType)
141145
Assert.AreEqual(expectedType, result)
146+
147+
[<TestFixture>]
148+
type TestWildcardExpansion() =
149+
150+
[<Test>]
151+
member _.``expandWildcard finds .fs files in current directory``() =
152+
use file1 = new TemporaryFile("module Test1", "fs")
153+
use file2 = new TemporaryFile("module Test2", "fs")
154+
let dir = Path.GetDirectoryName(file1.FileName)
155+
let pattern = Path.Combine(dir, "*.fs")
156+
157+
let results = expandWildcard pattern
158+
159+
Assert.That(results, Is.Not.Empty)
160+
Assert.That(results, Does.Contain(file1.FileName))
161+
Assert.That(results, Does.Contain(file2.FileName))
162+
163+
[<Test>]
164+
member _.``expandWildcard finds .fsx files``() =
165+
use file1 = new TemporaryFile("printfn \"test\"", "fsx")
166+
let dir = Path.GetDirectoryName(file1.FileName)
167+
let pattern = Path.Combine(dir, "*.fsx")
168+
169+
let results = expandWildcard pattern
170+
171+
Assert.That(results, Does.Contain(file1.FileName))
172+
173+
[<Test>]
174+
member _.``expandWildcard returns empty list for non-existent directory``() =
175+
let pattern = "nonexistent_directory/*.fs"
176+
177+
let results = expandWildcard pattern
178+
179+
Assert.That(results, Is.Empty)

0 commit comments

Comments
 (0)