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
36 changes: 27 additions & 9 deletions src/FSharpLint.Core/Application/Lint.fs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ module Lint =
GlobalConfig: Rules.GlobalRuleConfig
TypeCheckResults: FSharpCheckFileResults option
ProjectCheckResults: FSharpCheckProjectResults option
ProjectOptions: Lazy<FSharpProjectOptions option>
ProjectFileName: Lazy<string option>
FilePath: string
FileContent: string
Lines: string[]
Expand All @@ -150,7 +150,7 @@ module Lint =
Lines = config.Lines
CheckInfo = config.TypeCheckResults
ProjectCheckInfo = config.ProjectCheckResults
ProjectOptions = config.ProjectOptions
ProjectFileName = config.ProjectFileName
GlobalConfig = config.GlobalConfig
}
// Build state for rules with context.
Expand Down Expand Up @@ -265,9 +265,18 @@ module Lint =
GlobalConfig = enabledRules.GlobalConfig
TypeCheckResults = fileInfo.TypeCheckResults
ProjectCheckResults = fileInfo.ProjectCheckResults
ProjectOptions = lazy(
fileInfo.ProjectCheckResults
|> Option.map _.ProjectContext.ProjectOptions
ProjectFileName = lazy(
fileInfo.ProjectFileName
|> Option.orElseWith (fun () ->
// A caller (project-mode lint, or an analyzer host) that knows the
// project file supplies it directly above. Otherwise derive it from
// the check results — but FSharpProjectContext.ProjectOptions throws
// by design under FCS's TransparentCompiler, so guard the access.
try
fileInfo.ProjectCheckResults
|> Option.map (fun projectCheckResults ->
projectCheckResults.ProjectContext.ProjectOptions.ProjectFileName)
with _ -> None)
)
FilePath = fileInfo.File
FileContent = fileInfo.Text
Expand Down Expand Up @@ -399,6 +408,10 @@ module Lint =
TypeCheckResults:FSharpCheckFileResults option
/// Optional results of project-wide type info (allows for a more accurate lint).
ProjectCheckResults:FSharpCheckProjectResults option
/// Path to the project file (.fsproj), when known. Lets the library-heuristic
/// rules work under hosts (e.g. TransparentCompiler analyzer hosts) where the
/// project options cannot be derived from the check results.
ProjectFileName:string option
}

/// Gets a FSharpLint Configuration based on the provided ConfigurationParam.
Expand Down Expand Up @@ -468,7 +481,7 @@ module Lint =
|> Array.iter (fun fileParseResult ->
lint
lintInformation
{ fileParseResult with ProjectCheckResults = Some projectCheckResults })
{ fileParseResult with ProjectCheckResults = Some projectCheckResults; ProjectFileName = Some projectOptions.ProjectFileName })

return Success ()
else
Expand Down Expand Up @@ -581,6 +594,7 @@ module Lint =
ParseFile.Ast = parsedFileInfo.Ast
ParseFile.TypeCheckResults = parsedFileInfo.TypeCheckResults
ParseFile.ProjectCheckResults = parsedFileInfo.ProjectCheckResults
ParseFile.ProjectFileName = parsedFileInfo.ProjectFileName
ParseFile.File = "<inline source>" }

lint lintInformation parsedFileInfo
Expand All @@ -600,7 +614,8 @@ module Lint =
{ Source = parseFileInformation.Text
Ast = parseFileInformation.Ast
TypeCheckResults = parseFileInformation.TypeCheckResults
ProjectCheckResults = None }
ProjectCheckResults = None
ProjectFileName = None }

return lintParsedSource optionalParams parsedFileInfo
| ParseFile.Failed failure -> return LintResult.Failure(FailedToParseFile failure)
Expand Down Expand Up @@ -635,6 +650,7 @@ module Lint =
ParseFile.Ast = parsedFileInfo.Ast
ParseFile.TypeCheckResults = parsedFileInfo.TypeCheckResults
ParseFile.ProjectCheckResults = parsedFileInfo.ProjectCheckResults
ParseFile.ProjectFileName = parsedFileInfo.ProjectFileName
ParseFile.File = filePath }

lint lintInformation parsedFileInfo
Expand All @@ -653,7 +669,8 @@ module Lint =
{ Source = astFileParseInfo.Text
Ast = astFileParseInfo.Ast
TypeCheckResults = astFileParseInfo.TypeCheckResults
ProjectCheckResults = astFileParseInfo.ProjectCheckResults }
ProjectCheckResults = astFileParseInfo.ProjectCheckResults
ProjectFileName = astFileParseInfo.ProjectFileName }

return lintParsedFile optionalParams parsedFileInfo filePath
| ParseFile.Failed failure -> return LintResult.Failure(FailedToParseFile failure)
Expand Down Expand Up @@ -684,7 +701,8 @@ module Lint =
{ Source = astFileParseInfo.Text
Ast = astFileParseInfo.Ast
TypeCheckResults = astFileParseInfo.TypeCheckResults
ProjectCheckResults = astFileParseInfo.ProjectCheckResults }
ProjectCheckResults = astFileParseInfo.ProjectCheckResults
ProjectFileName = astFileParseInfo.ProjectFileName }
return lintParsedFile optionalParams parsedFileInfo filePath
| ParseFile.Failed failure ->
return LintResult.Failure (FailedToParseFile failure)
Expand Down
6 changes: 5 additions & 1 deletion src/FSharpLint.Core/Application/Lint.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ module Lint =

/// Optional results of project-wide type info (allows for a more accurate lint).
ProjectCheckResults:FSharpCheckProjectResults option
/// Path to the project file (.fsproj), when known. Lets the library-heuristic
/// rules work under hosts (e.g. TransparentCompiler analyzer hosts) where the
/// project options cannot be derived from the check results.
ProjectFileName:string option
}

type BuildFailure = | InvalidProjectFileMessage of string
Expand Down Expand Up @@ -129,7 +133,7 @@ module Lint =
GlobalConfig: Rules.GlobalRuleConfig
TypeCheckResults: FSharpCheckFileResults option
ProjectCheckResults: FSharpCheckProjectResults option
ProjectOptions: Lazy<FSharpProjectOptions option>
ProjectFileName: Lazy<string option>
FilePath: string
FileContent: string
Lines: string[]
Expand Down
6 changes: 6 additions & 0 deletions src/FSharpLint.Core/Framework/ParseFile.fs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ module ParseFile =

/// Path to the file.
File:string

/// Path to the project file (.fsproj) this file belongs to, when known.
/// Callers can supply this so the library-heuristic rules work even when the
/// project options cannot be derived from the check results.
ProjectFileName:string option
}

[<NoComparison>]
Expand All @@ -54,6 +59,7 @@ module ParseFile =
TypeCheckResults = Some(typeCheckResults)
ProjectCheckResults = None
File = file
ProjectFileName = None
}
| FSharpCheckFileAnswer.Aborted -> return Failed(AbortedTypeCheck)
}
Expand Down
2 changes: 1 addition & 1 deletion src/FSharpLint.Core/Framework/Rules.fs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ type AstNodeRuleParams =
Lines:string []
CheckInfo:FSharpCheckFileResults option
ProjectCheckInfo:FSharpCheckProjectResults option
ProjectOptions: Lazy<FSharpProjectOptions option>
ProjectFileName: Lazy<string option>
GlobalConfig:GlobalRuleConfig }

type LineRuleParams =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ let runner (config: Config) (args: AstNodeRuleParams) =
| _ -> config.Mode = AllAPIs

let likelyhoodOfBeingInLibrary =
match args.ProjectOptions.Value with
| Some projectOptions -> howLikelyProjectIsLibrary projectOptions.ProjectFileName
match args.ProjectFileName.Value with
| Some projectFileName -> howLikelyProjectIsLibrary projectFileName
| None -> Unlikely

if config.Mode = OnlyPublicAPIsInLibraries && likelyhoodOfBeingInLibrary <> Likely then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,8 @@ let runner (config: Config) (args: AstNodeRuleParams) =
Array.append (checkFuncs asyncFuncs taskFuncs) (checkFuncs taskFuncs asyncFuncs)

let likelyhoodOfBeingInLibrary =
match args.ProjectOptions.Value with
| Some projectOptions -> howLikelyProjectIsLibrary projectOptions.ProjectFileName
match args.ProjectFileName.Value with
| Some projectFileName -> howLikelyProjectIsLibrary projectFileName
| None -> Unlikely

if config.Mode = OnlyPublicAPIsInLibraries && likelyhoodOfBeingInLibrary <> Likely then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ let checkIfInLibrary (args: AstNodeRuleParams) (range: range) : array<WarningDet
let ruleNotApplicable =
isInObsoleteMethodOrFunction (args.GetParents args.NodeIndex)
||
match (args.CheckInfo, args.ProjectOptions.Value) with
| Some checkFileResults, Some projectOptions ->
let projectFile = System.IO.FileInfo projectOptions.ProjectFileName
match (args.CheckInfo, args.ProjectFileName.Value) with
| Some checkFileResults, Some projectFileName ->
let projectFile = System.IO.FileInfo projectFileName
match howLikelyProjectIsLibrary projectFile.Name with
| Likely -> false
| Unlikely -> true
Expand Down
2 changes: 1 addition & 1 deletion tests/FSharpLint.Benchmarks/Benchmark.fs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type Benchmark () =
let (fileInfo, _lines) =
let text = File.ReadAllText sourceFile
let tree = generateAst text sourceFile
({ Ast = tree; Source = text; TypeCheckResults = None; ProjectCheckResults = None }, String.toLines text |> Array.toList)
({ Ast = tree; Source = text; TypeCheckResults = None; ProjectCheckResults = None; ProjectFileName = None }, String.toLines text |> Array.toList)

[<Benchmark>]
member this.LintParsedFile () =
Expand Down
2 changes: 1 addition & 1 deletion tests/FSharpLint.Core.Tests/Rules/TestAstNodeRule.fs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ type TestAstNodeRuleBase (rule:Rule) =
GlobalConfig = resolvedGlobalConfig
TypeCheckResults = checkResult
ProjectCheckResults = None
ProjectOptions = Lazy<_>(None)
ProjectFileName = Lazy<_>(None)
FilePath = (Option.defaultValue String.Empty maybeFileName)
FileContent = input
Lines = (input.Split("\n"))
Expand Down
2 changes: 1 addition & 1 deletion tests/FSharpLint.Core.Tests/Rules/TestHintMatcherBase.fs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ type TestHintMatcherBase () =
GlobalConfig = resolvedGlobalConfig
TypeCheckResults = checkResult
ProjectCheckResults = None
ProjectOptions = Lazy<_>()
ProjectFileName = Lazy<_>()
FilePath = (Option.defaultValue String.Empty maybeFileName)
FileContent = input
Lines = (input.Split("\n"))
Expand Down
2 changes: 1 addition & 1 deletion tests/FSharpLint.Core.Tests/Rules/TestIndentationRule.fs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type TestIndentationRuleBase (rule:Rule) =
GlobalConfig = resolvedGlobalConfig
TypeCheckResults = None
ProjectCheckResults = None
ProjectOptions = Lazy<_>(None)
ProjectFileName = Lazy<_>(None)
FilePath = resolvedFileName
FileContent = input
Lines = lines
Expand Down
2 changes: 1 addition & 1 deletion tests/FSharpLint.Core.Tests/Rules/TestLineRule.fs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type TestLineRuleBase (rule:Rule) =
GlobalConfig = resolvedGlobalConfig
TypeCheckResults = None
ProjectCheckResults = None
ProjectOptions = Lazy<_>(None)
ProjectFileName = Lazy<_>(None)
FilePath = resolvedFileName
FileContent = input
Lines = lines
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type TestNoTabCharactersRuleBase (rule:Rule) =
GlobalConfig = resolvedGlobalConfig
TypeCheckResults = None
ProjectCheckResults = None
ProjectOptions = Lazy<_>()
ProjectFileName = Lazy<_>()
FilePath = resolvedFileName
FileContent = input
Lines = lines
Expand Down
82 changes: 81 additions & 1 deletion tests/FSharpLint.FunctionalTest/TestApi.fs
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,28 @@ module TestApi =
/// Must be called once per process.
let toolsPath = Ionide.ProjInfo.Init.init (DirectoryInfo <| Directory.GetCurrentDirectory()) None

/// Parse + check `source` under FCS's TransparentCompiler (as an analyzer host would),
/// returning the parse tree together with the file and project check results.
let checkSourceUnderTransparentCompiler source =
let checker = FSharpChecker.Create(keepAssemblyContents = true, useTransparentCompiler = true)
let sourceText = SourceText.ofString source
let (options, _diagnostics) =
checker.GetProjectOptionsFromScript(sourceFile, sourceText) |> Async.RunSynchronously
let parseResults, checkAnswer =
checker.ParseAndCheckFileInProject(sourceFile, 0, sourceText, options) |> Async.RunSynchronously
let checkResults =
match checkAnswer with
| FSharpCheckFileAnswer.Succeeded results -> results
| FSharpCheckFileAnswer.Aborted -> failwith "type check aborted"
let projectResults = checker.ParseAndCheckProject options |> Async.RunSynchronously
(parseResults.ParseTree, checkResults, projectResults)

[<Category("Performance")>]
[<Test>]
member _.``Performance of linting an existing file``() =
let text = File.ReadAllText sourceFile
let tree = generateAst text
let fileInfo = { Ast = tree; Source = text; TypeCheckResults = None; ProjectCheckResults = None }
let fileInfo = { Ast = tree; Source = text; TypeCheckResults = None; ProjectCheckResults = None; ProjectFileName = None }

let stopwatch = Stopwatch.StartNew()
let times = ResizeArray()
Expand All @@ -59,6 +75,70 @@ module TestApi =
Assert.Less(result, 250)
fprintf TestContext.Out "Average runtime of linter on parsed file: %d (milliseconds)." result

/// Regression: analyzer hosts built on FCS's TransparentCompiler supply
/// ProjectCheckResults whose FSharpProjectContext.ProjectOptions getter throws by
/// design. Deriving the project file name from them must degrade to None (guarded)
/// rather than failing the whole file with an internal error.
[<Test>]
member _.``Lint parsed TransparentCompiler results without internal failure``() =
let source = "module TransparentCompilerRepro\n\nlet answer = async { return 42 }\n"
let parseTree, checkResults, projectResults = checkSourceUnderTransparentCompiler source

let fileInfo =
{ Ast = parseTree
Source = source
TypeCheckResults = Some checkResults
ProjectCheckResults = Some projectResults
ProjectFileName = None }

let mutable internalFailure = None
let optionalParams =
{ OptionalLintParameters.Default with
ReportLinterProgress =
Some (fun progress ->
match progress with
| ProjectProgress.Failed(_, ex) -> internalFailure <- Some ex
| _ -> ()) }

match lintParsedFile optionalParams fileInfo sourceFile with
| LintResult.Success _ ->
match internalFailure with
| Some ex -> Assert.Fail $"lint failed internally: {ex}"
| None -> ()
| LintResult.Failure failure -> Assert.Fail(string failure)

/// The recovery this branch adds: a caller (e.g. an analyzer host) that knows the
/// project file supplies it directly via ProjectFileName, so the library-heuristic
/// rules keep working under the TransparentCompiler where the project options
/// cannot be derived from the check results.
[<Test>]
member _.``Caller-supplied ProjectFileName drives the library heuristic under TransparentCompiler``() =
// A public function returning Async<'T>: AsynchronousFunctionNames (default mode
// OnlyPublicAPIsInLibraries) flags it only when the project looks like a library.
let source = "module Foo =\n let Bar(): Async<int> =\n async { return 1 }\n"
let parseTree, checkResults, projectResults = checkSourceUnderTransparentCompiler source

let flagsBar projectFileName =
let fileInfo =
{ Ast = parseTree
Source = source
TypeCheckResults = Some checkResults
ProjectCheckResults = Some projectResults
ProjectFileName = projectFileName }
match lintParsedFile OptionalLintParameters.Default fileInfo sourceFile with
| LintResult.Success warnings ->
warnings |> List.exists (fun warning -> warning.Details.Message.Contains "AsyncBar")
| LintResult.Failure failure -> failwith (string failure)

// "MyLib.fsproj" tokenises to ["My"; "Lib"; ".fsproj"] -> Likely a library.
Assert.IsTrue(
flagsBar (Some "MyLib.fsproj"),
"caller-supplied library project name should engage the async-naming rule under TransparentCompiler")
// With no project file name the derivation is impossible under TC -> degrade to not-a-library.
Assert.IsFalse(
flagsBar None,
"with no project file name the library-gated rule should not fire")

[<Test>]
member _.``Lint project via absolute path``() =
let projectPath = basePath </> "tests" </> "FSharpLint.FunctionalTest.TestedProject" </> "FSharpLint.FunctionalTest.TestedProject.NetCore"
Expand Down
Loading