From 4a6823caf64e11bc7453b811b72daf58860a7090 Mon Sep 17 00:00:00 2001 From: Michael Glass Date: Thu, 2 Jul 2026 12:53:42 +0200 Subject: [PATCH] fix: degrade two-phase ProjectOptions to None under TransparentCompiler check results FSharpProjectContext.get_ProjectOptions() throws by design when the check results come from FCS's TransparentCompiler (as used by some analyzer hosts). Rules that force the two-phase ProjectOptions lazy (the library heuristics in AsynchronousFunctionNames, SimpleAsyncComplementaryHelpers, NoAsyncRunSynchronouslyInLibrary) then abort linting of the whole file with an internal error. Wrap the single lazy construction site in try/with -> None so those heuristics fall back gracefully instead of failing the file. Regression: TestApi lints TransparentCompiler-produced parse+check results and asserts no ProjectProgress.Failed is reported. --- src/FSharpLint.Core/Application/Lint.fs | 13 +++++- tests/FSharpLint.FunctionalTest/TestApi.fs | 50 ++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/FSharpLint.Core/Application/Lint.fs b/src/FSharpLint.Core/Application/Lint.fs index ac91adea9..1be821360 100644 --- a/src/FSharpLint.Core/Application/Lint.fs +++ b/src/FSharpLint.Core/Application/Lint.fs @@ -265,9 +265,18 @@ module Lint = GlobalConfig = enabledRules.GlobalConfig TypeCheckResults = fileInfo.TypeCheckResults ProjectCheckResults = fileInfo.ProjectCheckResults - ProjectOptions = lazy( + // FSharpProjectContext.ProjectOptions is unavailable by design when + // the check results come from FCS's TransparentCompiler (the getter + // throws). Degrade to None so rules needing project options (e.g. the + // library heuristics) fall back gracefully instead of failing the + // whole file with an internal error. + ProjectOptions = lazy( fileInfo.ProjectCheckResults - |> Option.map _.ProjectContext.ProjectOptions + |> Option.bind (fun projectCheckResults -> + try + Some projectCheckResults.ProjectContext.ProjectOptions + with _ -> + None) ) FilePath = fileInfo.File FileContent = fileInfo.Text diff --git a/tests/FSharpLint.FunctionalTest/TestApi.fs b/tests/FSharpLint.FunctionalTest/TestApi.fs index 635113f10..039d7eb85 100644 --- a/tests/FSharpLint.FunctionalTest/TestApi.fs +++ b/tests/FSharpLint.FunctionalTest/TestApi.fs @@ -59,6 +59,56 @@ 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. Rules forcing the two-phase ProjectOptions lazy (the library + /// heuristics in AsynchronousFunctionNames & co.) then failed the whole file with + /// an internal error. The lazy must degrade to None instead. + [] + member _.``Lint parsed TransparentCompiler results without internal failure``() = + let source = "module TransparentCompilerRepro\n\nlet answer = async { return 42 }\n" + let transparentChecker = FSharpChecker.Create(keepAssemblyContents = true, useTransparentCompiler = true) + let sourceText = SourceText.ofString source + + let (options, _diagnostics) = + transparentChecker.GetProjectOptionsFromScript(sourceFile, sourceText) + |> Async.RunSynchronously + + let parseResults, checkAnswer = + transparentChecker.ParseAndCheckFileInProject(sourceFile, 0, sourceText, options) + |> Async.RunSynchronously + + let checkResults = + match checkAnswer with + | FSharpCheckFileAnswer.Succeeded results -> results + | FSharpCheckFileAnswer.Aborted -> failwith "type check aborted" + + let projectResults = + transparentChecker.ParseAndCheckProject options |> Async.RunSynchronously + + let fileInfo = + { Ast = parseResults.ParseTree + Source = source + TypeCheckResults = Some checkResults + ProjectCheckResults = Some projectResults } + + 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) + [] member _.``Lint project via absolute path``() = let projectPath = basePath "tests" "FSharpLint.FunctionalTest.TestedProject" "FSharpLint.FunctionalTest.TestedProject.NetCore"