Skip to content
Merged
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
56 changes: 54 additions & 2 deletions src/FSharpLint.Console/Program.fs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module FSharpLint.Console.Program
module FSharpLint.Console.Program

open Argu
open System
Expand All @@ -18,6 +18,7 @@ type internal FileType =
| Solution = 2
| File = 3
| Source = 4
| Wildcard = 5

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

/// 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) =
let isFSharpFile (filePath:string) =
filePath.EndsWith ".fs" || filePath.EndsWith ".fsx"

let normalizedPattern = pattern.Replace('\\', '/')

let directory, searchPattern, searchOption =
match normalizedPattern.IndexOf "**/" with
| -1 ->
// Non-recursive pattern
match normalizedPattern.LastIndexOf '/' with
| -1 -> (".", normalizedPattern, SearchOption.TopDirectoryOnly)
| lastSeparator ->
let dir = normalizedPattern.Substring(0, lastSeparator)
let pat = normalizedPattern.Substring(lastSeparator + 1)
((if String.IsNullOrEmpty dir then "." else dir), pat, SearchOption.TopDirectoryOnly)
| 0 ->
// Pattern starts with **/
let pat = normalizedPattern.Substring 3
(".", pat, SearchOption.AllDirectories)
| doubleStarIndex ->
// Pattern has **/ in the middle
let dir = normalizedPattern.Substring(0, doubleStarIndex).TrimEnd '/'
let pat = normalizedPattern.Substring(doubleStarIndex + 3)
(dir, pat, SearchOption.AllDirectories)

let fullDirectory = Path.GetFullPath directory
if Directory.Exists fullDirectory then
Directory.GetFiles(fullDirectory, searchPattern, searchOption)
|> Array.filter isFSharpFile
|> Array.toList
else
List.empty

let private parserProgress (output:Output.IOutput) = function
| Starting file ->
String.Format(Resources.GetString("ConsoleStartingFile"), file) |> output.WriteInfo
Expand All @@ -59,9 +96,15 @@ let private parserProgress (output:Output.IOutput) = function
output.WriteError
$"Exception Message:{Environment.NewLine}{parseException.Message}{Environment.NewLine}Exception Stack Trace:{Environment.NewLine}{parseException.StackTrace}{Environment.NewLine}"

/// Checks if a string contains wildcard characters.
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) =
if target.EndsWith ".fs" || target.EndsWith ".fsx" then
if containsWildcard target then
FileType.Wildcard
else if target.EndsWith ".fs" || target.EndsWith ".fsx" then
FileType.File
else if target.EndsWith ".fsproj" then
FileType.Project
Expand Down Expand Up @@ -125,6 +168,15 @@ let private start (arguments:ParseResults<ToolArgs>) (toolsPath:Ionide.ProjInfo.
| FileType.File -> Lint.lintFile lintParams target
| FileType.Source -> Lint.lintSource lintParams target
| FileType.Solution -> Lint.lintSolution lintParams target toolsPath
| 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
output.WriteInfo $"No files matching pattern '%s{target}' were found."
LintResult.Success List.empty
else
output.WriteInfo $"Found %d{List.length files} file(s) matching pattern '%s{target}'."
Lint.lintFiles lintParams files
| FileType.Project
| _ -> Lint.lintProject lintParams target toolsPath
handleLintResult lintResult
Expand Down
35 changes: 34 additions & 1 deletion src/FSharpLint.Core/Application/Lint.fs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace FSharpLint.Application
namespace FSharpLint.Application

open System
open System.Collections.Concurrent
Expand Down Expand Up @@ -643,3 +643,36 @@ module Lint =
else
FailedToLoadFile filePath
|> LintResult.Failure

/// Lints multiple F# files from given file paths.
let lintFiles optionalParams filePaths =
let checker = FSharpChecker.Create(keepAssemblyContents=true)

match getConfig optionalParams.Configuration with
| Ok config ->
let optionalParams = { optionalParams with Configuration = ConfigurationParam.Configuration config }

let lintSingleFile filePath =
if IO.File.Exists filePath then
match ParseFile.parseFile filePath checker None with
| ParseFile.Success astFileParseInfo ->
let parsedFileInfo =
{ Source = astFileParseInfo.Text
Ast = astFileParseInfo.Ast
TypeCheckResults = astFileParseInfo.TypeCheckResults }
lintParsedFile optionalParams parsedFileInfo filePath
| ParseFile.Failed failure ->
LintResult.Failure (FailedToParseFile failure)
else
LintResult.Failure (FailedToLoadFile filePath)

let results = filePaths |> Seq.map lintSingleFile |> Seq.toList

let failures = results |> List.choose (function | LintResult.Failure failure -> Some failure | _ -> None)
let warnings = results |> List.collect (function | LintResult.Success warning -> warning | _ -> List.empty)

match failures with
| firstFailure :: _ -> LintResult.Failure firstFailure
| [] -> LintResult.Success warnings
| Error err ->
LintResult.Failure (RunTimeConfigError err)
5 changes: 4 additions & 1 deletion src/FSharpLint.Core/Application/Lint.fsi
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace FSharpLint.Application
namespace FSharpLint.Application

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

/// Lints multiple F# files from given file paths.
val lintFiles : optionalParams:OptionalLintParameters -> filePaths:string seq -> LintResult

/// Lints an F# file that has already been parsed using
/// `FSharp.Compiler.Services` in the calling application.
val lintParsedFile : optionalParams:OptionalLintParameters -> parsedFileInfo:ParsedFileInformation -> filePath:string -> LintResult
38 changes: 38 additions & 0 deletions tests/FSharpLint.Console.Tests/TestApp.fs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,45 @@ type TestFileTypeInference() =
[<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")>]
[<TestCase("../MyProject.fsproj", FileType.Project, TestName = "inferFileType must handle .fsproj files with relative paths correctly")>]
[<TestCase("*.fs", FileType.Wildcard, TestName = "inferFileType must recognize wildcard patterns with * as Wildcard type")>]
[<TestCase("**/*.fs", FileType.Wildcard, TestName = "inferFileType must recognize recursive wildcard patterns as Wildcard type")>]
[<TestCase("src/**/*.fsx", FileType.Wildcard, TestName = "inferFileType must recognize subdirectory recursive wildcard patterns as Wildcard type")>]
[<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)
Assert.AreEqual(expectedType, result)

[<TestFixture>]
type TestWildcardExpansion() =

[<Test>]
member _.``expandWildcard finds .fs files in current directory``() =
use file1 = new TemporaryFile("module Test1", "fs")
use file2 = new TemporaryFile("module Test2", "fs")
let dir = Path.GetDirectoryName(file1.FileName)
let pattern = Path.Combine(dir, "*.fs")

let results = expandWildcard pattern

Assert.That(results, Is.Not.Empty)
Assert.That(results, Does.Contain(file1.FileName))
Assert.That(results, Does.Contain(file2.FileName))

[<Test>]
member _.``expandWildcard finds .fsx files``() =
use file1 = new TemporaryFile("printfn \"test\"", "fsx")
let dir = Path.GetDirectoryName(file1.FileName)
let pattern = Path.Combine(dir, "*.fsx")

let results = expandWildcard pattern

Assert.That(results, Does.Contain(file1.FileName))

[<Test>]
member _.``expandWildcard returns empty list for non-existent directory``() =
let pattern = "nonexistent_directory/*.fs"

let results = expandWildcard pattern

Assert.That(results, Is.Empty)
Loading