From c2ad5b1fcc2a0ca1f976c8879de67ef299acf49d Mon Sep 17 00:00:00 2001 From: Tuomas Hietanen Date: Tue, 28 Oct 2025 09:03:03 +0000 Subject: [PATCH 1/4] Supports async-parsing --- src/FSharpLint.Core/Application/Lint.fs | 30 +++++++++------- src/FSharpLint.Core/Application/Lint.fsi | 3 ++ src/FSharpLint.Core/Framework/ParseFile.fs | 36 ++++++++++--------- .../Rules/TestAstNodeRule.fs | 2 +- .../Rules/TestHintMatcherBase.fs | 2 +- tests/FSharpLint.Core.Tests/TestUtils.fs | 21 ++++++----- 6 files changed, 55 insertions(+), 39 deletions(-) diff --git a/src/FSharpLint.Core/Application/Lint.fs b/src/FSharpLint.Core/Application/Lint.fs index c63694a91..b6614b3fe 100644 --- a/src/FSharpLint.Core/Application/Lint.fs +++ b/src/FSharpLint.Core/Application/Lint.fs @@ -476,7 +476,7 @@ module Lint = let parsedFiles = files |> List.filter (not << isIgnoredFile) - |> List.map (fun file -> ParseFile.parseFile file checker (Some projectOptions)) + |> List.map (fun file -> ParseFile.parseFile file checker (Some projectOptions) |> Async.RunSynchronously) let failedFiles = List.choose getFailedFiles parsedFiles @@ -585,19 +585,25 @@ module Lint = LintResult.Failure (RunTimeConfigError err) /// Lints F# source code. - let lintSource optionalParams source = - let checker = FSharpChecker.Create(keepAssemblyContents=true) + let lintSourceAsync optionalParams source = + async { + let checker = FSharpChecker.Create(keepAssemblyContents=true) - match ParseFile.parseSource source checker with - | ParseFile.Success(parseFileInformation) -> - let parsedFileInfo = - { Source = parseFileInformation.Text - Ast = parseFileInformation.Ast - TypeCheckResults = parseFileInformation.TypeCheckResults } + match! ParseFile.parseSource source checker with + | ParseFile.Success(parseFileInformation) -> + let parsedFileInfo = + { Source = parseFileInformation.Text + Ast = parseFileInformation.Ast + TypeCheckResults = parseFileInformation.TypeCheckResults } - lintParsedSource optionalParams parsedFileInfo - | ParseFile.Failed failure -> LintResult.Failure(FailedToParseFile failure) + return lintParsedSource optionalParams parsedFileInfo + | ParseFile.Failed failure -> return LintResult.Failure(FailedToParseFile failure) + } + /// Lints F# source code. + let lintSource optionalParams source = + lintSourceAsync optionalParams source |> Async.RunSynchronously + /// Lints an F# file that has already been parsed using `FSharp.Compiler.Services` in the calling application. let lintParsedFile (optionalParams:OptionalLintParameters) (parsedFileInfo:ParsedFileInformation) (filePath:string) = match getConfig optionalParams.Configuration with @@ -631,7 +637,7 @@ module Lint = if IO.File.Exists filePath then let checker = FSharpChecker.Create(keepAssemblyContents=true) - match ParseFile.parseFile filePath checker None with + match ParseFile.parseFile filePath checker None |> Async.RunSynchronously with | ParseFile.Success astFileParseInfo -> let parsedFileInfo = { Source = astFileParseInfo.Text diff --git a/src/FSharpLint.Core/Application/Lint.fsi b/src/FSharpLint.Core/Application/Lint.fsi index 6d7eb05ad..aed5c4935 100644 --- a/src/FSharpLint.Core/Application/Lint.fsi +++ b/src/FSharpLint.Core/Application/Lint.fsi @@ -156,6 +156,9 @@ module Lint = /// Lints F# source code. val lintSource : optionalParams:OptionalLintParameters -> source:string -> LintResult + /// Lints F# source code async. + val lintSourceAsync : optionalParams:OptionalLintParameters -> source:string -> Async + /// Lints F# source code that has already been parsed using /// `FSharp.Compiler.Services` in the calling application. val lintParsedSource : optionalParams:OptionalLintParameters -> parsedFileInfo:ParsedFileInformation -> LintResult diff --git a/src/FSharpLint.Core/Framework/ParseFile.fs b/src/FSharpLint.Core/Framework/ParseFile.fs index ac4208a36..6a5edcb56 100644 --- a/src/FSharpLint.Core/Framework/ParseFile.fs +++ b/src/FSharpLint.Core/Framework/ParseFile.fs @@ -1,4 +1,4 @@ -namespace FSharpLint.Framework +namespace FSharpLint.Framework /// Provides functionality to parse F# files using `FSharp.Compiler.Service`. module ParseFile = @@ -37,49 +37,51 @@ module ParseFile = | Failed of ParseFileFailure | Success of 'Content - let private parse file source (checker:FSharpChecker, options) = + let private parse file source (checker:FSharpChecker, options) = async { let sourceText = SourceText.ofString source - let (parseResults, checkFileAnswer) = + let! parseResults, checkFileAnswer = checker.ParseAndCheckFileInProject(file, 0, sourceText, options) - |> Async.RunSynchronously match checkFileAnswer with | FSharpCheckFileAnswer.Succeeded(typeCheckResults) -> - Success + return Success { Text = source Ast = parseResults.ParseTree TypeCheckResults = Some(typeCheckResults) File = file } - | FSharpCheckFileAnswer.Aborted -> Failed(AbortedTypeCheck) + | FSharpCheckFileAnswer.Aborted -> return Failed(AbortedTypeCheck) + } - let getProjectOptionsFromScript (checker:FSharpChecker) file (source:string) = + let getProjectOptionsFromScript (checker:FSharpChecker) file (source:string) = async { let sourceText = SourceText.ofString source let assumeDotNetFramework = false let otherOpts = [| "--targetprofile:netstandard" |] - let (options, _diagnostics) = + let! options, _diagnostics = checker.GetProjectOptionsFromScript(file, sourceText, assumeDotNetFramework = assumeDotNetFramework, useSdkRefs = not assumeDotNetFramework, otherFlags = otherOpts) - |> Async.RunSynchronously - options + return options + } /// Parses a file using `FSharp.Compiler.Service`. - let parseFile file (checker:FSharpChecker) projectOptions = + let parseFile file (checker:FSharpChecker) projectOptions = async { let source = File.ReadAllText(file) - let projectOptions = + let! projectOptions = match projectOptions with - | Some(existingOptions) -> existingOptions + | Some(existingOptions) -> async { return existingOptions } | None -> getProjectOptionsFromScript checker file source - parse file source (checker, projectOptions) + return! parse file source (checker, projectOptions) + } /// Parses source code using `FSharp.Compiler.Service`. - let parseSourceFile fileName source (checker:FSharpChecker) = - let options = getProjectOptionsFromScript checker fileName source + let parseSourceFile fileName source (checker:FSharpChecker) = async { + let! options = getProjectOptionsFromScript checker fileName source - parse fileName source (checker, options) + return! parse fileName source (checker, options) + } let parseSource source (checker:FSharpChecker) = let fileName = Path.ChangeExtension(Path.GetTempFileName(), "fsx") diff --git a/tests/FSharpLint.Core.Tests/Rules/TestAstNodeRule.fs b/tests/FSharpLint.Core.Tests/Rules/TestAstNodeRule.fs index 44fa93e72..fa93e9ff3 100644 --- a/tests/FSharpLint.Core.Tests/Rules/TestAstNodeRule.fs +++ b/tests/FSharpLint.Core.Tests/Rules/TestAstNodeRule.fs @@ -28,7 +28,7 @@ type TestAstNodeRuleBase (rule:Rule) = let globalConfig = Option.defaultValue GlobalRuleConfig.Default globalConfig - match parseResults with + match parseResults |> Async.RunSynchronously with | ParseFileResult.Success parseInfo -> let syntaxArray = AbstractSyntaxArray.astToArray parseInfo.Ast let checkResult = diff --git a/tests/FSharpLint.Core.Tests/Rules/TestHintMatcherBase.fs b/tests/FSharpLint.Core.Tests/Rules/TestHintMatcherBase.fs index e81a14b06..dc362e735 100644 --- a/tests/FSharpLint.Core.Tests/Rules/TestHintMatcherBase.fs +++ b/tests/FSharpLint.Core.Tests/Rules/TestHintMatcherBase.fs @@ -51,7 +51,7 @@ type TestHintMatcherBase () = let globalConfig = Option.defaultValue GlobalRuleConfig.Default globalConfig - match parseResults with + match parseResults |> Async.RunSynchronously with | ParseFileResult.Success parseInfo -> let syntaxArray = AbstractSyntaxArray.astToArray parseInfo.Ast let checkResult = diff --git a/tests/FSharpLint.Core.Tests/TestUtils.fs b/tests/FSharpLint.Core.Tests/TestUtils.fs index 1a26a61cf..69a4667b5 100644 --- a/tests/FSharpLint.Core.Tests/TestUtils.fs +++ b/tests/FSharpLint.Core.Tests/TestUtils.fs @@ -15,17 +15,22 @@ let private performanceTestSourceFile = basePath "TypeChecker.fs" - let generateAst source = - let checker = FSharpChecker.Create(keepAssemblyContents=true) - let sourceText = SourceText.ofString source + let generateAstAsync source = + async { + let checker = FSharpChecker.Create(keepAssemblyContents=true) + let sourceText = SourceText.ofString source + + let! options = + ParseFile.getProjectOptionsFromScript checker performanceTestSourceFile source - let options = ParseFile.getProjectOptionsFromScript checker performanceTestSourceFile source + let! parseResults = + checker.ParseFile(performanceTestSourceFile, sourceText, options |> checker.GetParsingOptionsFromProjectOptions |> fst) - let parseResults = - checker.ParseFile(performanceTestSourceFile, sourceText, options |> checker.GetParsingOptionsFromProjectOptions |> fst) - |> Async.RunSynchronously + return parseResults.ParseTree + } - parseResults.ParseTree + let generateAst source = + generateAstAsync source |> Async.RunSynchronously let getPerformanceTestInput = let memoizedResult = ref None From eadd6122a772b823c55f24d0172c8de45363696b Mon Sep 17 00:00:00 2001 From: Tuomas Hietanen Date: Thu, 30 Oct 2025 12:11:45 +0000 Subject: [PATCH 2/4] Selfcheck fix --- tests/FSharpLint.Core.Tests/Rules/TestAstNodeRule.fs | 2 +- tests/FSharpLint.Core.Tests/Rules/TestHintMatcherBase.fs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/FSharpLint.Core.Tests/Rules/TestAstNodeRule.fs b/tests/FSharpLint.Core.Tests/Rules/TestAstNodeRule.fs index fa93e9ff3..a1c99fe86 100644 --- a/tests/FSharpLint.Core.Tests/Rules/TestAstNodeRule.fs +++ b/tests/FSharpLint.Core.Tests/Rules/TestAstNodeRule.fs @@ -28,7 +28,7 @@ type TestAstNodeRuleBase (rule:Rule) = let globalConfig = Option.defaultValue GlobalRuleConfig.Default globalConfig - match parseResults |> Async.RunSynchronously with + match Async.RunSynchronously parseResults with | ParseFileResult.Success parseInfo -> let syntaxArray = AbstractSyntaxArray.astToArray parseInfo.Ast let checkResult = diff --git a/tests/FSharpLint.Core.Tests/Rules/TestHintMatcherBase.fs b/tests/FSharpLint.Core.Tests/Rules/TestHintMatcherBase.fs index dc362e735..1339649a8 100644 --- a/tests/FSharpLint.Core.Tests/Rules/TestHintMatcherBase.fs +++ b/tests/FSharpLint.Core.Tests/Rules/TestHintMatcherBase.fs @@ -51,7 +51,7 @@ type TestHintMatcherBase () = let globalConfig = Option.defaultValue GlobalRuleConfig.Default globalConfig - match parseResults |> Async.RunSynchronously with + match Async.RunSynchronously parseResults with | ParseFileResult.Success parseInfo -> let syntaxArray = AbstractSyntaxArray.astToArray parseInfo.Ast let checkResult = From 89f264ba844dc5eebc4b6ec51e142c9604d2a4e4 Mon Sep 17 00:00:00 2001 From: "Andres G. Aragoneses" Date: Tue, 11 Nov 2025 17:21:25 +0800 Subject: [PATCH 3/4] Core,Tests: respect .NET API conventions Suffix "Async" for methods is for methods that return "Task", not "Async Async.RunSynchronously + asyncLintSource optionalParams source |> Async.RunSynchronously /// Lints an F# file that has already been parsed using `FSharp.Compiler.Services` in the calling application. let lintParsedFile (optionalParams:OptionalLintParameters) (parsedFileInfo:ParsedFileInformation) (filePath:string) = diff --git a/src/FSharpLint.Core/Application/Lint.fsi b/src/FSharpLint.Core/Application/Lint.fsi index aed5c4935..29ddd3332 100644 --- a/src/FSharpLint.Core/Application/Lint.fsi +++ b/src/FSharpLint.Core/Application/Lint.fsi @@ -157,7 +157,7 @@ module Lint = val lintSource : optionalParams:OptionalLintParameters -> source:string -> LintResult /// Lints F# source code async. - val lintSourceAsync : optionalParams:OptionalLintParameters -> source:string -> Async + val asyncLintSource : optionalParams:OptionalLintParameters -> source:string -> Async /// Lints F# source code that has already been parsed using /// `FSharp.Compiler.Services` in the calling application. @@ -171,4 +171,4 @@ module Lint = /// 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 \ No newline at end of file + val lintParsedFile : optionalParams:OptionalLintParameters -> parsedFileInfo:ParsedFileInformation -> filePath:string -> LintResult diff --git a/tests/FSharpLint.Core.Tests/TestUtils.fs b/tests/FSharpLint.Core.Tests/TestUtils.fs index 69a4667b5..cdcb8ae46 100644 --- a/tests/FSharpLint.Core.Tests/TestUtils.fs +++ b/tests/FSharpLint.Core.Tests/TestUtils.fs @@ -15,7 +15,7 @@ let private performanceTestSourceFile = basePath "TypeChecker.fs" - let generateAstAsync source = + let asyncGenerateAst source = async { let checker = FSharpChecker.Create(keepAssemblyContents=true) let sourceText = SourceText.ofString source @@ -30,7 +30,7 @@ } let generateAst source = - generateAstAsync source |> Async.RunSynchronously + asyncGenerateAst source |> Async.RunSynchronously let getPerformanceTestInput = let memoizedResult = ref None From d0627fbee5dafb47ed839a31041926e20b3f791c Mon Sep 17 00:00:00 2001 From: webwarrior-ws Date: Tue, 11 Nov 2025 13:20:03 +0100 Subject: [PATCH 4/4] Core/Lint.fs: fix build --- src/FSharpLint.Core/Application/Lint.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FSharpLint.Core/Application/Lint.fs b/src/FSharpLint.Core/Application/Lint.fs index 415ec5cc3..da988cc96 100644 --- a/src/FSharpLint.Core/Application/Lint.fs +++ b/src/FSharpLint.Core/Application/Lint.fs @@ -660,7 +660,7 @@ module Lint = let lintSingleFile filePath = if IO.File.Exists filePath then - match ParseFile.parseFile filePath checker None with + match ParseFile.parseFile filePath checker None |> Async.RunSynchronously with | ParseFile.Success astFileParseInfo -> let parsedFileInfo = { Source = astFileParseInfo.Text