From 39a3d58acb0a2afdf7c43f0d7bbea629925e7b0a Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 11:01:29 +0200 Subject: [PATCH 01/15] Build a project's TcGlobals with its own path map when the framework imports are cached FrameworkImportsCache keys the framework imports, and the TcGlobals built with them, by the framework set alone. A project reusing the entry got a fresh TcGlobals only when langVersion or realsig differed, and even then took pathMap from the cached instance. Since TypedTreePickle applies that map to every range it writes, the in-memory reference data of each project carried the --pathmap of whichever project filled the cache first, and a project without a map handed its consumers file names nothing on disk matches. pathMap now takes part in the decision like langVersion and realsig, and the new TcGlobals takes it from the project's own TcConfig, in the incremental builder and the transparent compiler alike. Fixes #20474 Co-Authored-By: Claude Fable 5.1 --- src/Compiler/Service/IncrementalBuild.fs | 11 ++++++----- src/Compiler/Service/TransparentCompiler.fs | 9 +++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Compiler/Service/IncrementalBuild.fs b/src/Compiler/Service/IncrementalBuild.fs index f68f3495547..d6121baa6b3 100644 --- a/src/Compiler/Service/IncrementalBuild.fs +++ b/src/Compiler/Service/IncrementalBuild.fs @@ -560,12 +560,13 @@ type FrameworkImportsCache(size) = let node = this.GetNode(tcConfig, frameworkDLLs, nonFrameworkResolutions) let! tcGlobals, frameworkTcImports = node.GetOrComputeValue() - // If the tcGlobals was loaded from a different project, langVersion and realsig may be different - // for each cached project. So here we create a new tcGlobals, with the existing framework values - // and updated realsig and langversion + // If the tcGlobals was loaded from a different project, langVersion, realsig and pathMap may be + // different for each cached project. So here we create a new tcGlobals, with the existing framework + // values and the updated realsig, langversion and pathMap let tcGlobals = if tcGlobals.langVersion <> tcConfig.langVersion - || tcGlobals.realsig <> tcConfig.realsig then + || tcGlobals.realsig <> tcConfig.realsig + || tcGlobals.pathMap <> tcConfig.pathMap then TcGlobals( tcGlobals.compilingFSharpCore, tcGlobals.ilg, @@ -577,7 +578,7 @@ type FrameworkImportsCache(size) = tcGlobals.tryFindSysTypeCcuHelper, tcGlobals.emitDebugInfoInQuotations, tcGlobals.noDebugAttributes, - tcGlobals.pathMap, + tcConfig.pathMap, tcConfig.langVersion, tcConfig.realsig, tcConfig.compilationMode diff --git a/src/Compiler/Service/TransparentCompiler.fs b/src/Compiler/Service/TransparentCompiler.fs index 8821c39aaca..ca6e1a65abe 100644 --- a/src/Compiler/Service/TransparentCompiler.fs +++ b/src/Compiler/Service/TransparentCompiler.fs @@ -964,13 +964,14 @@ type internal TransparentCompiler // Prepare the frameworkTcImportsCache let! tcGlobals, frameworkTcImports = ComputeFrameworkImports tcConfig frameworkDLLs nonFrameworkResolutions - // If the tcGlobals was loaded from a different project, langVersion and realsig may be different - // for each cached project. So here we create a new tcGlobals, with the existing framework values - // and updated realsig and langversion + // If the tcGlobals was loaded from a different project, langVersion, realsig and pathMap may be + // different for each cached project. So here we create a new tcGlobals, with the existing + // framework values and the updated realsig, langversion and pathMap let tcGlobals = if tcGlobals.langVersion <> tcConfig.langVersion || tcGlobals.realsig <> tcConfig.realsig + || tcGlobals.pathMap <> tcConfig.pathMap then TcGlobals( tcGlobals.compilingFSharpCore, @@ -983,7 +984,7 @@ type internal TransparentCompiler tcGlobals.tryFindSysTypeCcuHelper, tcGlobals.emitDebugInfoInQuotations, tcGlobals.noDebugAttributes, - tcGlobals.pathMap, + tcConfig.pathMap, tcConfig.langVersion, tcConfig.realsig, tcConfig.compilationMode From 9320a3e8d9ce7dd7345d9ced37acc98ac3b11c30 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 11:01:29 +0200 Subject: [PATCH 02/15] Test that a cached framework set does not lend its path map to a sibling project Co-Authored-By: Claude Fable 5.1 --- .../FSharp.Compiler.ComponentTests.fsproj | 3 +- .../FSharpChecker/PathMap.fs | 57 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj index 8cd5ee0e922..2b473df5e1b 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -1,4 +1,4 @@ - + @@ -549,6 +549,7 @@ + diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs new file mode 100644 index 00000000000..18e99d829ce --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs @@ -0,0 +1,57 @@ +module FSharpChecker.PathMap + +open System.IO +open System.Threading.Tasks +open Xunit +open FSharp.Test.ProjectGeneration +open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.Text + +let private checkWith (checker: FSharpChecker) (project: SyntheticProject) = + ProjectWorkflowBuilder(project, checker = checker).Yield() |> Async.Ignore + +/// The framework imports, and the TcGlobals with them, are cached per framework set; the path map of +/// the project that filled the cache must not reach the ranges a sibling exposes to its consumers. +[] +[] +[] +let ``a sibling's path map does not reach the ranges of a project without one`` (useTransparentCompiler: bool) : Task = + task { + let checker = FSharpChecker.Create(useTransparentCompiler = useTransparentCompiler) + let library = SyntheticProject.Create("Library", sourceFile "Library" []) + + let mapped = + { SyntheticProject.Create("Mapped", sourceFile "Mapped" []) with + OtherOptions = [ $"--pathmap:{Path.GetDirectoryName library.ProjectDir}=.\\" ] } + + let app = + { SyntheticProject.Create("App", sourceFile "App" [ "Library" ]) with + DependsOn = [ library ] } + + do! checkWith checker mapped + do! checkWith checker app + + let appFile = app.GetFilePath "App" + + let! _, answer = + checker.ParseAndCheckFileInProject( + appFile, + 0, + SourceText.ofString (File.ReadAllText appFile), + app.GetProjectOptions checker + ) + + let checkResults = + match answer with + | FSharpCheckFileAnswer.Succeeded checkResults -> checkResults + | FSharpCheckFileAnswer.Aborted -> failwith "the check was aborted" + + let libraryFunction = + checkResults.GetAllUsesOfAllSymbolsInFile() + |> Seq.tryFind (fun symbolUse -> symbolUse.Symbol.FullName = $"{library.Name}.ModuleLibrary.f") + |> Option.defaultWith (fun () -> + failwith + $"""ModuleLibrary.f not used; symbols: %A{checkResults.GetAllUsesOfAllSymbolsInFile() |> Seq.map _.Symbol.FullName |> Seq.distinct |> List.ofSeq}""") + + Assert.Equal(library.GetFilePath "Library", libraryFunction.Symbol.DeclarationLocation.Value.FileName) + } From 34490e5722142dd460d1c4c0ed7338dba67120b5 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 12:32:15 +0200 Subject: [PATCH 03/15] Add the release note for PR #20476 --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index f383908e195..9c87ffc9f13 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -167,6 +167,7 @@ * Reject runtime-only Await calls inside ordinary task state-machine methods with FS3918, while preserving normal task composition inside runtime-async methods. ([Issue #20576](https://github.com/dotnet/fsharp/issues/20576), [PR #20584](https://github.com/dotnet/fsharp/pull/20584)) * Fix dependency ordering and stack safety for recursive inline bindings. ([PR #20111](https://github.com/dotnet/fsharp/pull/20111)) * Fix signature generation (`fsc --sig`, `GenerateSignature`, `GetValSignatureText`) dropping the parentheses around a destructured or pattern-annotated tuple parameter when a sibling argument in the same curried group is named, so `(int * float) * z: string` no longer prints as the flat 3-tuple `int * float * z: string`. ([Issue #20397](https://github.com/dotnet/fsharp/issues/20397), [PR #20589](https://github.com/dotnet/fsharp/pull/20589)) +* A project reusing cached framework imports is checked with its own `--pathmap` instead of the map of the project that filled the cache, so the ranges of its in-memory reference data name its real files. ([Issue #20474](https://github.com/dotnet/fsharp/issues/20474), [PR #20476](https://github.com/dotnet/fsharp/pull/20476)) ### Added From 340683f6a24be1abf2bbee9205ed9c48ea21cf55 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sat, 26 Sep 2026 13:47:16 +0200 Subject: [PATCH 04/15] Move the release note to the version in development `main` opened 11.0.200 for SDK 11.0.200; 11.0.100 has shipped. Co-Authored-By: Claude Opus 5 (1M context) --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 1 - docs/release-notes/.FSharp.Compiler.Service/11.0.200.md | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 9c87ffc9f13..f383908e195 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -167,7 +167,6 @@ * Reject runtime-only Await calls inside ordinary task state-machine methods with FS3918, while preserving normal task composition inside runtime-async methods. ([Issue #20576](https://github.com/dotnet/fsharp/issues/20576), [PR #20584](https://github.com/dotnet/fsharp/pull/20584)) * Fix dependency ordering and stack safety for recursive inline bindings. ([PR #20111](https://github.com/dotnet/fsharp/pull/20111)) * Fix signature generation (`fsc --sig`, `GenerateSignature`, `GetValSignatureText`) dropping the parentheses around a destructured or pattern-annotated tuple parameter when a sibling argument in the same curried group is named, so `(int * float) * z: string` no longer prints as the flat 3-tuple `int * float * z: string`. ([Issue #20397](https://github.com/dotnet/fsharp/issues/20397), [PR #20589](https://github.com/dotnet/fsharp/pull/20589)) -* A project reusing cached framework imports is checked with its own `--pathmap` instead of the map of the project that filled the cache, so the ranges of its in-memory reference data name its real files. ([Issue #20474](https://github.com/dotnet/fsharp/issues/20474), [PR #20476](https://github.com/dotnet/fsharp/pull/20476)) ### Added diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md index 4cd1275d7d6..b707be7c4b3 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md @@ -5,6 +5,7 @@ ### Fixed * Capture diagnostics emitted when transparent project checking finishes a project, including FS0247 for namespace-module name collisions. ([Issue #6036](https://github.com/dotnet/fsharp/issues/6036), [PR #20706](https://github.com/dotnet/fsharp/pull/20706)) +* A project reusing cached framework imports is checked with its own `--pathmap` instead of the map of the project that filled the cache, so the ranges of its in-memory reference data name its real files. ([Issue #20474](https://github.com/dotnet/fsharp/issues/20474), [PR #20476](https://github.com/dotnet/fsharp/pull/20476)) * Split Linux CoreCLR CI into two parallel test batches to reduce test turnaround time. ([PR #20587](https://github.com/dotnet/fsharp/pull/20587)) * Fix `NativePtr.stackalloc` used inside the `with` handler, filter or `finally` block of a `try` expression producing an assembly that throws `InvalidProgramException` at method load. Direct or mandatory-inline allocations are now reported at compile time as error FS3924; calls to non-inline allocation helpers, including handler-local helpers, remain legal and retain their method boundary during inlining and let-binding elimination. ([Issue #20295](https://github.com/dotnet/fsharp/issues/20295), [PR #20491](https://github.com/dotnet/fsharp/pull/20491)) * `FsiEvaluationSession.EvalInteraction` and `EvalInteractionNonThrowing` evaluate every `;;`-separated interaction in the text they are given, as their documentation has always said, instead of silently discarding everything after the first. ([PR #20396](https://github.com/dotnet/fsharp/pull/20396)) From 814135c369bcd3b2a7d9e5c0468893f1f3378d5b Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 11:01:29 +0200 Subject: [PATCH 05/15] Build a project's TcGlobals with its own path map when the framework imports are cached FrameworkImportsCache keys the framework imports, and the TcGlobals built with them, by the framework set alone. A project reusing the entry got a fresh TcGlobals only when langVersion or realsig differed, and even then took pathMap from the cached instance. Since TypedTreePickle applies that map to every range it writes, the in-memory reference data of each project carried the --pathmap of whichever project filled the cache first, and a project without a map handed its consumers file names nothing on disk matches. pathMap now takes part in the decision like langVersion and realsig, and the new TcGlobals takes it from the project's own TcConfig, in the incremental builder and the transparent compiler alike. Fixes #20474 Co-Authored-By: Claude Fable 5.1 --- src/Compiler/Service/IncrementalBuild.fs | 11 ++++++----- src/Compiler/Service/TransparentCompiler.fs | 9 +++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Compiler/Service/IncrementalBuild.fs b/src/Compiler/Service/IncrementalBuild.fs index f68f3495547..d6121baa6b3 100644 --- a/src/Compiler/Service/IncrementalBuild.fs +++ b/src/Compiler/Service/IncrementalBuild.fs @@ -560,12 +560,13 @@ type FrameworkImportsCache(size) = let node = this.GetNode(tcConfig, frameworkDLLs, nonFrameworkResolutions) let! tcGlobals, frameworkTcImports = node.GetOrComputeValue() - // If the tcGlobals was loaded from a different project, langVersion and realsig may be different - // for each cached project. So here we create a new tcGlobals, with the existing framework values - // and updated realsig and langversion + // If the tcGlobals was loaded from a different project, langVersion, realsig and pathMap may be + // different for each cached project. So here we create a new tcGlobals, with the existing framework + // values and the updated realsig, langversion and pathMap let tcGlobals = if tcGlobals.langVersion <> tcConfig.langVersion - || tcGlobals.realsig <> tcConfig.realsig then + || tcGlobals.realsig <> tcConfig.realsig + || tcGlobals.pathMap <> tcConfig.pathMap then TcGlobals( tcGlobals.compilingFSharpCore, tcGlobals.ilg, @@ -577,7 +578,7 @@ type FrameworkImportsCache(size) = tcGlobals.tryFindSysTypeCcuHelper, tcGlobals.emitDebugInfoInQuotations, tcGlobals.noDebugAttributes, - tcGlobals.pathMap, + tcConfig.pathMap, tcConfig.langVersion, tcConfig.realsig, tcConfig.compilationMode diff --git a/src/Compiler/Service/TransparentCompiler.fs b/src/Compiler/Service/TransparentCompiler.fs index 8821c39aaca..ca6e1a65abe 100644 --- a/src/Compiler/Service/TransparentCompiler.fs +++ b/src/Compiler/Service/TransparentCompiler.fs @@ -964,13 +964,14 @@ type internal TransparentCompiler // Prepare the frameworkTcImportsCache let! tcGlobals, frameworkTcImports = ComputeFrameworkImports tcConfig frameworkDLLs nonFrameworkResolutions - // If the tcGlobals was loaded from a different project, langVersion and realsig may be different - // for each cached project. So here we create a new tcGlobals, with the existing framework values - // and updated realsig and langversion + // If the tcGlobals was loaded from a different project, langVersion, realsig and pathMap may be + // different for each cached project. So here we create a new tcGlobals, with the existing + // framework values and the updated realsig, langversion and pathMap let tcGlobals = if tcGlobals.langVersion <> tcConfig.langVersion || tcGlobals.realsig <> tcConfig.realsig + || tcGlobals.pathMap <> tcConfig.pathMap then TcGlobals( tcGlobals.compilingFSharpCore, @@ -983,7 +984,7 @@ type internal TransparentCompiler tcGlobals.tryFindSysTypeCcuHelper, tcGlobals.emitDebugInfoInQuotations, tcGlobals.noDebugAttributes, - tcGlobals.pathMap, + tcConfig.pathMap, tcConfig.langVersion, tcConfig.realsig, tcConfig.compilationMode From 9f0fe38c0384e9ed540f8313871c2706db2efded Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 11:01:29 +0200 Subject: [PATCH 06/15] Test that a cached framework set does not lend its path map to a sibling project Co-Authored-By: Claude Fable 5.1 --- .../FSharp.Compiler.ComponentTests.fsproj | 3 +- .../FSharpChecker/PathMap.fs | 57 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj index 8cd5ee0e922..2b473df5e1b 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -1,4 +1,4 @@ - + @@ -549,6 +549,7 @@ + diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs new file mode 100644 index 00000000000..18e99d829ce --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs @@ -0,0 +1,57 @@ +module FSharpChecker.PathMap + +open System.IO +open System.Threading.Tasks +open Xunit +open FSharp.Test.ProjectGeneration +open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.Text + +let private checkWith (checker: FSharpChecker) (project: SyntheticProject) = + ProjectWorkflowBuilder(project, checker = checker).Yield() |> Async.Ignore + +/// The framework imports, and the TcGlobals with them, are cached per framework set; the path map of +/// the project that filled the cache must not reach the ranges a sibling exposes to its consumers. +[] +[] +[] +let ``a sibling's path map does not reach the ranges of a project without one`` (useTransparentCompiler: bool) : Task = + task { + let checker = FSharpChecker.Create(useTransparentCompiler = useTransparentCompiler) + let library = SyntheticProject.Create("Library", sourceFile "Library" []) + + let mapped = + { SyntheticProject.Create("Mapped", sourceFile "Mapped" []) with + OtherOptions = [ $"--pathmap:{Path.GetDirectoryName library.ProjectDir}=.\\" ] } + + let app = + { SyntheticProject.Create("App", sourceFile "App" [ "Library" ]) with + DependsOn = [ library ] } + + do! checkWith checker mapped + do! checkWith checker app + + let appFile = app.GetFilePath "App" + + let! _, answer = + checker.ParseAndCheckFileInProject( + appFile, + 0, + SourceText.ofString (File.ReadAllText appFile), + app.GetProjectOptions checker + ) + + let checkResults = + match answer with + | FSharpCheckFileAnswer.Succeeded checkResults -> checkResults + | FSharpCheckFileAnswer.Aborted -> failwith "the check was aborted" + + let libraryFunction = + checkResults.GetAllUsesOfAllSymbolsInFile() + |> Seq.tryFind (fun symbolUse -> symbolUse.Symbol.FullName = $"{library.Name}.ModuleLibrary.f") + |> Option.defaultWith (fun () -> + failwith + $"""ModuleLibrary.f not used; symbols: %A{checkResults.GetAllUsesOfAllSymbolsInFile() |> Seq.map _.Symbol.FullName |> Seq.distinct |> List.ofSeq}""") + + Assert.Equal(library.GetFilePath "Library", libraryFunction.Symbol.DeclarationLocation.Value.FileName) + } From e5c17aea784b08e637a428dbc9d2965ab3c2945b Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 7 Sep 2026 12:32:15 +0200 Subject: [PATCH 07/15] Add the release note for PR #20476 --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index f383908e195..9c87ffc9f13 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -167,6 +167,7 @@ * Reject runtime-only Await calls inside ordinary task state-machine methods with FS3918, while preserving normal task composition inside runtime-async methods. ([Issue #20576](https://github.com/dotnet/fsharp/issues/20576), [PR #20584](https://github.com/dotnet/fsharp/pull/20584)) * Fix dependency ordering and stack safety for recursive inline bindings. ([PR #20111](https://github.com/dotnet/fsharp/pull/20111)) * Fix signature generation (`fsc --sig`, `GenerateSignature`, `GetValSignatureText`) dropping the parentheses around a destructured or pattern-annotated tuple parameter when a sibling argument in the same curried group is named, so `(int * float) * z: string` no longer prints as the flat 3-tuple `int * float * z: string`. ([Issue #20397](https://github.com/dotnet/fsharp/issues/20397), [PR #20589](https://github.com/dotnet/fsharp/pull/20589)) +* A project reusing cached framework imports is checked with its own `--pathmap` instead of the map of the project that filled the cache, so the ranges of its in-memory reference data name its real files. ([Issue #20474](https://github.com/dotnet/fsharp/issues/20474), [PR #20476](https://github.com/dotnet/fsharp/pull/20476)) ### Added From 697fff7926a2404685540de5eafae6e473535f6c Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 10 Sep 2026 20:14:19 +0200 Subject: [PATCH 08/15] Name a mapped declaration from its root once A build that maps its source paths - as DeterministicSourcePaths does - maps the directory it compiled in along with the file names of its ranges, so the two reach the same root and neither records what that root is. `fileNameOfItem` joined them anyway, on the assumption that the directory is absolute and the file name relative to it, and the declaration of a symbol imported from such an assembly came back naming the directory twice. Nothing matches a path like that, so Go To Definition reported the symbol as external and opened generated metadata instead of its source. Join only when the directory is rooted. When it is not, the file name already reaches the root on its own and is as complete as the metadata can make it. The IDE drops the path map from the options it builds, so this is reached through an assembly on disk - which is where a path map is normally applied, and which no change to those options can rewrite. Co-Authored-By: Claude Opus 5 (1M context) --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + src/Compiler/Symbols/SymbolHelpers.fs | 10 +++- .../FSharpChecker/PathMap.fs | 57 +++++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 9c87ffc9f13..4ab8b75d0c5 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -6,6 +6,7 @@ * Fix reference-assembly MVID collisions when a public member is renamed in an early file of a large (>~32 file) project under `--optimize-`. The per-file signature hashes were folded with a left-shift-by-one combiner over a 32-bit `Hash`, which truncated the contribution of any file more than ~32 positions from the end of the compile order, so MSBuild's `CopyRefAssembly` saw an unchanged MVID and kept a stale reference assembly (surfacing as `FS0039` downstream). The combiner is now an FNV-1a multiply mix over a 64-bit `Hash`. ([Issue #20389](https://github.com/dotnet/fsharp/issues/20389)) * Fix internal error FS0192 "Iterate2D" when a `[]` parameter auto-quotes an argument that captures a not-yet-generalized use of an inferred generically-recursive function. The auto-quoted (`Expr.WithValue`) copy now keeps a fresh link to the recursive-value use so it receives the same inferred type arguments as the executable expression at the letrec point. ([Issue #20379](https://github.com/dotnet/fsharp/issues/20379)) * Fix `StackOverflowException` when checking a long `seq { ... }` body. ([PR #20480](https://github.com/dotnet/fsharp/pull/20480)) +* `GetDeclarationLocation` no longer names the compile-time directory twice for a declaration in an assembly built with `--pathmap` (as `DeterministicSourcePaths` sets it). Such a build maps the directory it compiled in as well as the file names of its ranges, so both already reach the same root and joining them repeated the directory, leaving a path that matches no file. * Fix `NativePtr.stackalloc` nested in a larger expression (e.g. a call argument or the right of an assignment) producing an assembly that throws `InvalidProgramException` at load. ([Issue #8083](https://github.com/dotnet/fsharp/issues/8083), [PR #20302](https://github.com/dotnet/fsharp/pull/20302)) * Fix internal error "Unexpected generalized type variables when compiling an active pattern" when an active pattern is used in a `let` binding whose right-hand side is a generic value, e.g. `let (T) = id`. Such a binding is now checked like the equivalent `match` and is not generalized. ([Issue #16856](https://github.com/dotnet/fsharp/issues/16856), [PR #20383](https://github.com/dotnet/fsharp/pull/20383)) * Fix Release-only (`--optimize+`) `System.InvalidProgramException` from `Seq.collect` / `yield!` over a value-type (struct) collection implementing `seq<'T>` (e.g. `ImmutableArray<_>`) when materialised with `List.ofSeq` / `Seq.toList` / `Seq.toArray` or a list/array comprehension. The collector lowering now boxes a struct sub-collection to `seq<'T>` before calling `AddMany`/`AddManyAndClose` (matching the coercion the type checker already inserts for `yield!`), and uses `unit` as the try/finally result type instead of the body type (removing a spurious `ldnull` store). ([Issue #20203](https://github.com/dotnet/fsharp/issues/20203)) diff --git a/src/Compiler/Symbols/SymbolHelpers.fs b/src/Compiler/Symbols/SymbolHelpers.fs index 222edab7c13..1ee4c203a68 100644 --- a/src/Compiler/Symbols/SymbolHelpers.fs +++ b/src/Compiler/Symbols/SymbolHelpers.fs @@ -230,14 +230,20 @@ module internal SymbolHelpers = let fileNameOfItem (g: TcGlobals) qualProjectDir (m: range) h = let file = m.FileName if verbose then dprintf "file stored in metadata is '%s'\n" file + + // A build that maps its source paths maps the directory it compiled in as well, so that directory + // and the file name reach the same unrecorded root on their own: joining them repeats the directory. + let underDirectory (dir: string) = + if FileSystem.IsPathRootedShim dir then Path.Combine(dir, file) else file + if not (FileSystem.IsPathRootedShim file) then match ccuOfItem g h with | Some ccu -> - Path.Combine(ccu.SourceCodeDirectory, file) + underDirectory ccu.SourceCodeDirectory | None -> match qualProjectDir with | None -> file - | Some dir -> Path.Combine(dir, file) + | Some dir -> underDirectory dir else file let ParamNameAndTypesOfUnaryCustomOperation g minfo = diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs index 18e99d829ce..8bf70c250d5 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs @@ -5,6 +5,7 @@ open System.Threading.Tasks open Xunit open FSharp.Test.ProjectGeneration open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.EditorServices open FSharp.Compiler.Text let private checkWith (checker: FSharpChecker) (project: SyntheticProject) = @@ -55,3 +56,59 @@ let ``a sibling's path map does not reach the ranges of a project without one`` Assert.Equal(library.GetFilePath "Library", libraryFunction.Symbol.DeclarationLocation.Value.FileName) } + +/// A build that maps its source paths maps the directory it compiled in as well, so the file name of a +/// range and that directory both reach the same root on their own. Joining them names the directory twice. +[] +[] +[] +let ``a declaration in a project with a path map is named from the root once`` (useTransparentCompiler: bool) : Task = + task { + let checker = FSharpChecker.Create(useTransparentCompiler = useTransparentCompiler) + let library = SyntheticProject.Create("MappedLibrary", sourceFile "Library" []) + let root = Path.GetDirectoryName library.ProjectDir + + let library = + { library with + OtherOptions = [ $"--pathmap:{root}=.\\" ] } + + let app = + { SyntheticProject.Create("MappedApp", sourceFile "App" [ "Library" ]) with + DependsOn = [ library ] } + + do! checkWith checker library + do! checkWith checker app + + let appFile = app.GetFilePath "App" + let appLines = File.ReadAllLines appFile + + let! _, answer = + checker.ParseAndCheckFileInProject( + appFile, + 0, + SourceText.ofString (File.ReadAllText appFile), + app.GetProjectOptions checker + ) + + let checkResults = + match answer with + | FSharpCheckFileAnswer.Succeeded checkResults -> checkResults + | FSharpCheckFileAnswer.Aborted -> failwith "the check was aborted" + + let usage = + checkResults.GetAllUsesOfAllSymbolsInFile() + |> Seq.find (fun symbolUse -> symbolUse.Symbol.FullName = $"{library.Name}.ModuleLibrary.f") + + let declaration = + checkResults.GetDeclarationLocation( + usage.Range.EndLine, + usage.Range.EndColumn, + appLines[usage.Range.EndLine - 1], + [ "ModuleLibrary"; "f" ] + ) + + match declaration with + | FindDeclResult.DeclFound range -> + Assert.Equal(library.GetFilePath "Library", Path.GetFullPath(Path.Combine(root, range.FileName))) + | result -> failwith $"expected the declaration of ModuleLibrary.f, got %A{result}" + } From 48226961eb1636552e49af7a422a85aa3a6b7fb6 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 11 Sep 2026 04:26:12 +0200 Subject: [PATCH 09/15] Map the test's root with the platform's separator On Linux and macOS a backslash is part of a file name, so a map to .\ left the declaration's name unresolvable against the root there. Co-Authored-By: Claude Opus 5 --- tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs index 8bf70c250d5..e8e607db286 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs @@ -70,7 +70,7 @@ let ``a declaration in a project with a path map is named from the root once`` ( let library = { library with - OtherOptions = [ $"--pathmap:{root}=.\\" ] } + OtherOptions = [ $"--pathmap:{root}=.{Path.DirectorySeparatorChar}" ] } let app = { SyntheticProject.Create("MappedApp", sourceFile "App" [ "Library" ]) with From 5e338b1db95f89b602656dd4df469e373f86df06 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 11 Sep 2026 04:31:04 +0200 Subject: [PATCH 10/15] Link the release note to PR #20518 Co-Authored-By: Claude Opus 5 --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 4ab8b75d0c5..d1c0af616cc 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -7,6 +7,7 @@ * Fix internal error FS0192 "Iterate2D" when a `[]` parameter auto-quotes an argument that captures a not-yet-generalized use of an inferred generically-recursive function. The auto-quoted (`Expr.WithValue`) copy now keeps a fresh link to the recursive-value use so it receives the same inferred type arguments as the executable expression at the letrec point. ([Issue #20379](https://github.com/dotnet/fsharp/issues/20379)) * Fix `StackOverflowException` when checking a long `seq { ... }` body. ([PR #20480](https://github.com/dotnet/fsharp/pull/20480)) * `GetDeclarationLocation` no longer names the compile-time directory twice for a declaration in an assembly built with `--pathmap` (as `DeterministicSourcePaths` sets it). Such a build maps the directory it compiled in as well as the file names of its ranges, so both already reach the same root and joining them repeated the directory, leaving a path that matches no file. +* `GetDeclarationLocation` no longer names the compile-time directory twice for a declaration in an assembly built with `--pathmap` (as `DeterministicSourcePaths` sets it). Such a build maps the directory it compiled in as well as the file names of its ranges, so both already reach the same root and joining them repeated the directory, leaving a path that matches no file. ([PR #20518](https://github.com/dotnet/fsharp/pull/20518)) * Fix `NativePtr.stackalloc` nested in a larger expression (e.g. a call argument or the right of an assignment) producing an assembly that throws `InvalidProgramException` at load. ([Issue #8083](https://github.com/dotnet/fsharp/issues/8083), [PR #20302](https://github.com/dotnet/fsharp/pull/20302)) * Fix internal error "Unexpected generalized type variables when compiling an active pattern" when an active pattern is used in a `let` binding whose right-hand side is a generic value, e.g. `let (T) = id`. Such a binding is now checked like the equivalent `match` and is not generalized. ([Issue #16856](https://github.com/dotnet/fsharp/issues/16856), [PR #20383](https://github.com/dotnet/fsharp/pull/20383)) * Fix Release-only (`--optimize+`) `System.InvalidProgramException` from `Seq.collect` / `yield!` over a value-type (struct) collection implementing `seq<'T>` (e.g. `ImmutableArray<_>`) when materialised with `List.ofSeq` / `Seq.toList` / `Seq.toArray` or a list/array comprehension. The collector lowering now boxes a struct sub-collection to `seq<'T>` before calling `AddMany`/`AddManyAndClose` (matching the coercion the type checker already inserts for `yield!`), and uses `unit` as the try/finally result type instead of the body type (removing a spurious `ldnull` store). ([Issue #20203](https://github.com/dotnet/fsharp/issues/20203)) From 36d9a1aa75589d30c6f48a214e144b103ef328b2 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sat, 26 Sep 2026 13:47:04 +0200 Subject: [PATCH 11/15] Keep one release note, in the version in development Linking the note added a copy of it instead of editing the first, and `main` has since opened 11.0.200 for SDK 11.0.200, leaving 11.0.100 shipped. Co-Authored-By: Claude Opus 5 (1M context) --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 3 --- docs/release-notes/.FSharp.Compiler.Service/11.0.200.md | 2 ++ 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index d1c0af616cc..f383908e195 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -6,8 +6,6 @@ * Fix reference-assembly MVID collisions when a public member is renamed in an early file of a large (>~32 file) project under `--optimize-`. The per-file signature hashes were folded with a left-shift-by-one combiner over a 32-bit `Hash`, which truncated the contribution of any file more than ~32 positions from the end of the compile order, so MSBuild's `CopyRefAssembly` saw an unchanged MVID and kept a stale reference assembly (surfacing as `FS0039` downstream). The combiner is now an FNV-1a multiply mix over a 64-bit `Hash`. ([Issue #20389](https://github.com/dotnet/fsharp/issues/20389)) * Fix internal error FS0192 "Iterate2D" when a `[]` parameter auto-quotes an argument that captures a not-yet-generalized use of an inferred generically-recursive function. The auto-quoted (`Expr.WithValue`) copy now keeps a fresh link to the recursive-value use so it receives the same inferred type arguments as the executable expression at the letrec point. ([Issue #20379](https://github.com/dotnet/fsharp/issues/20379)) * Fix `StackOverflowException` when checking a long `seq { ... }` body. ([PR #20480](https://github.com/dotnet/fsharp/pull/20480)) -* `GetDeclarationLocation` no longer names the compile-time directory twice for a declaration in an assembly built with `--pathmap` (as `DeterministicSourcePaths` sets it). Such a build maps the directory it compiled in as well as the file names of its ranges, so both already reach the same root and joining them repeated the directory, leaving a path that matches no file. -* `GetDeclarationLocation` no longer names the compile-time directory twice for a declaration in an assembly built with `--pathmap` (as `DeterministicSourcePaths` sets it). Such a build maps the directory it compiled in as well as the file names of its ranges, so both already reach the same root and joining them repeated the directory, leaving a path that matches no file. ([PR #20518](https://github.com/dotnet/fsharp/pull/20518)) * Fix `NativePtr.stackalloc` nested in a larger expression (e.g. a call argument or the right of an assignment) producing an assembly that throws `InvalidProgramException` at load. ([Issue #8083](https://github.com/dotnet/fsharp/issues/8083), [PR #20302](https://github.com/dotnet/fsharp/pull/20302)) * Fix internal error "Unexpected generalized type variables when compiling an active pattern" when an active pattern is used in a `let` binding whose right-hand side is a generic value, e.g. `let (T) = id`. Such a binding is now checked like the equivalent `match` and is not generalized. ([Issue #16856](https://github.com/dotnet/fsharp/issues/16856), [PR #20383](https://github.com/dotnet/fsharp/pull/20383)) * Fix Release-only (`--optimize+`) `System.InvalidProgramException` from `Seq.collect` / `yield!` over a value-type (struct) collection implementing `seq<'T>` (e.g. `ImmutableArray<_>`) when materialised with `List.ofSeq` / `Seq.toList` / `Seq.toArray` or a list/array comprehension. The collector lowering now boxes a struct sub-collection to `seq<'T>` before calling `AddMany`/`AddManyAndClose` (matching the coercion the type checker already inserts for `yield!`), and uses `unit` as the try/finally result type instead of the body type (removing a spurious `ldnull` store). ([Issue #20203](https://github.com/dotnet/fsharp/issues/20203)) @@ -169,7 +167,6 @@ * Reject runtime-only Await calls inside ordinary task state-machine methods with FS3918, while preserving normal task composition inside runtime-async methods. ([Issue #20576](https://github.com/dotnet/fsharp/issues/20576), [PR #20584](https://github.com/dotnet/fsharp/pull/20584)) * Fix dependency ordering and stack safety for recursive inline bindings. ([PR #20111](https://github.com/dotnet/fsharp/pull/20111)) * Fix signature generation (`fsc --sig`, `GenerateSignature`, `GetValSignatureText`) dropping the parentheses around a destructured or pattern-annotated tuple parameter when a sibling argument in the same curried group is named, so `(int * float) * z: string` no longer prints as the flat 3-tuple `int * float * z: string`. ([Issue #20397](https://github.com/dotnet/fsharp/issues/20397), [PR #20589](https://github.com/dotnet/fsharp/pull/20589)) -* A project reusing cached framework imports is checked with its own `--pathmap` instead of the map of the project that filled the cache, so the ranges of its in-memory reference data name its real files. ([Issue #20474](https://github.com/dotnet/fsharp/issues/20474), [PR #20476](https://github.com/dotnet/fsharp/pull/20476)) ### Added diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md index 4cd1275d7d6..693bd2c31e2 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md @@ -5,6 +5,8 @@ ### Fixed * Capture diagnostics emitted when transparent project checking finishes a project, including FS0247 for namespace-module name collisions. ([Issue #6036](https://github.com/dotnet/fsharp/issues/6036), [PR #20706](https://github.com/dotnet/fsharp/pull/20706)) +* A project reusing cached framework imports is checked with its own `--pathmap` instead of the map of the project that filled the cache, so the ranges of its in-memory reference data name its real files. ([Issue #20474](https://github.com/dotnet/fsharp/issues/20474), [PR #20476](https://github.com/dotnet/fsharp/pull/20476)) +* `GetDeclarationLocation` no longer names the compile-time directory twice for a declaration in an assembly built with `--pathmap` (as `DeterministicSourcePaths` sets it). Such a build maps the directory it compiled in as well as the file names of its ranges, so both already reach the same root and joining them repeated the directory, leaving a path that matches no file. ([PR #20518](https://github.com/dotnet/fsharp/pull/20518)) * Split Linux CoreCLR CI into two parallel test batches to reduce test turnaround time. ([PR #20587](https://github.com/dotnet/fsharp/pull/20587)) * Fix `NativePtr.stackalloc` used inside the `with` handler, filter or `finally` block of a `try` expression producing an assembly that throws `InvalidProgramException` at method load. Direct or mandatory-inline allocations are now reported at compile time as error FS3924; calls to non-inline allocation helpers, including handler-local helpers, remain legal and retain their method boundary during inlining and let-binding elimination. ([Issue #20295](https://github.com/dotnet/fsharp/issues/20295), [PR #20491](https://github.com/dotnet/fsharp/pull/20491)) * `FsiEvaluationSession.EvalInteraction` and `EvalInteractionNonThrowing` evaluate every `;;`-separated interaction in the text they are given, as their documentation has always said, instead of silently discarding everything after the first. ([PR #20396](https://github.com/dotnet/fsharp/pull/20396)) From 2b147da318549143d213a38f696034f21c49347a Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Thu, 1 Oct 2026 19:41:41 +0200 Subject: [PATCH 12/15] Keep the compile directory unless a mapped name already starts with it A relative name in metadata is relative to the directory the assembly was compiled in, as the #line names that compilers before F# 10 wrote are. A path map writes the files compiled from under that directory with the mapped directory at their start, so only such a name is used as is; the directory being relative did not prove a name was mapped. The in-memory test gives way to one that compiles the library out of process from its own directory and references the emitted assembly, with no map, a relative one and the rooted /_/ one. Co-Authored-By: Claude Opus 5.5 --- .../.FSharp.Compiler.Service/11.0.200.md | 2 +- src/Compiler/Symbols/SymbolHelpers.fs | 12 +- .../FSharpChecker/PathMap.fs | 110 +++++++++++------- tests/FSharp.Test.Utilities/Compiler.fs | 12 +- 4 files changed, 88 insertions(+), 48 deletions(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md index 693bd2c31e2..b2529248f16 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md @@ -6,7 +6,7 @@ * Capture diagnostics emitted when transparent project checking finishes a project, including FS0247 for namespace-module name collisions. ([Issue #6036](https://github.com/dotnet/fsharp/issues/6036), [PR #20706](https://github.com/dotnet/fsharp/pull/20706)) * A project reusing cached framework imports is checked with its own `--pathmap` instead of the map of the project that filled the cache, so the ranges of its in-memory reference data name its real files. ([Issue #20474](https://github.com/dotnet/fsharp/issues/20474), [PR #20476](https://github.com/dotnet/fsharp/pull/20476)) -* `GetDeclarationLocation` no longer names the compile-time directory twice for a declaration in an assembly built with `--pathmap` (as `DeterministicSourcePaths` sets it). Such a build maps the directory it compiled in as well as the file names of its ranges, so both already reach the same root and joining them repeated the directory, leaving a path that matches no file. ([PR #20518](https://github.com/dotnet/fsharp/pull/20518)) +* `GetDeclarationLocation` no longer names the compile-time directory twice for a declaration in an assembly built with a relative `--pathmap`. Such a build maps the directory it compiled in along with its file names, so a file from under that directory already starts with it, and joining the two left a path that matches no file. Other relative names, such as the `#line` names that compilers before F# 10 wrote, are still resolved against that directory. ([PR #20518](https://github.com/dotnet/fsharp/pull/20518)) * Split Linux CoreCLR CI into two parallel test batches to reduce test turnaround time. ([PR #20587](https://github.com/dotnet/fsharp/pull/20587)) * Fix `NativePtr.stackalloc` used inside the `with` handler, filter or `finally` block of a `try` expression producing an assembly that throws `InvalidProgramException` at method load. Direct or mandatory-inline allocations are now reported at compile time as error FS3924; calls to non-inline allocation helpers, including handler-local helpers, remain legal and retain their method boundary during inlining and let-binding elimination. ([Issue #20295](https://github.com/dotnet/fsharp/issues/20295), [PR #20491](https://github.com/dotnet/fsharp/pull/20491)) * `FsiEvaluationSession.EvalInteraction` and `EvalInteractionNonThrowing` evaluate every `;;`-separated interaction in the text they are given, as their documentation has always said, instead of silently discarding everything after the first. ([PR #20396](https://github.com/dotnet/fsharp/pull/20396)) diff --git a/src/Compiler/Symbols/SymbolHelpers.fs b/src/Compiler/Symbols/SymbolHelpers.fs index 1ee4c203a68..bb6708f4464 100644 --- a/src/Compiler/Symbols/SymbolHelpers.fs +++ b/src/Compiler/Symbols/SymbolHelpers.fs @@ -231,10 +231,16 @@ module internal SymbolHelpers = let file = m.FileName if verbose then dprintf "file stored in metadata is '%s'\n" file - // A build that maps its source paths maps the directory it compiled in as well, so that directory - // and the file name reach the same unrecorded root on their own: joining them repeats the directory. + // A relative name is relative to the directory the assembly was compiled in, as the #line names that + // compilers before F# 10 wrote are. A path map rewrites that directory along with the files compiled + // from under it, though, so such a file already starts with the directory: joining them repeats it. let underDirectory (dir: string) = - if FileSystem.IsPathRootedShim dir then Path.Combine(dir, file) else file + let startsWithDirectory = + file.Length > dir.Length + && file.StartsWithOrdinal dir + && (file[dir.Length] = '\\' || file[dir.Length] = '/') + + if startsWithDirectory then file else Path.Combine(dir, file) if not (FileSystem.IsPathRootedShim file) then match ccuOfItem g h with diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs index e8e607db286..2ffb73379a4 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs @@ -3,9 +3,13 @@ module FSharpChecker.PathMap open System.IO open System.Threading.Tasks open Xunit +open FSharp.Test +open FSharp.Test.Compiler open FSharp.Test.ProjectGeneration +open FSharp.Test.Utilities open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.EditorServices +open FSharp.Compiler.Symbols open FSharp.Compiler.Text let private checkWith (checker: FSharpChecker) (project: SyntheticProject) = @@ -57,58 +61,84 @@ let ``a sibling's path map does not reach the ranges of a project without one`` Assert.Equal(library.GetFilePath "Library", libraryFunction.Symbol.DeclarationLocation.Value.FileName) } -/// A build that maps its source paths maps the directory it compiled in as well, so the file name of a -/// range and that directory both reach the same root on their own. Joining them names the directory twice. +/// Compiles the library from its own directory, as a build does: the assembly records that directory as the one it +/// was compiled in. +let private emitLibrary (root: string) (pathMap: string list) = + let projectDir = Directory.CreateDirectory(Path.Combine(root, "src", "Domain")).FullName + File.WriteAllText(Path.Combine(projectDir, "Library.fs"), "module Library\n\nlet f x = x + 1\n") + + let result = + runFscProcessIn projectDir [ + "--target:library" + yield! CompilerAssert.DefaultProjectOptions(TargetFramework.Current).OtherOptions + yield! pathMap + "-o:Library.dll" + "Library.fs" + ] + + Assert.True((result.ExitCode = 0), $"fsc exited with {result.ExitCode}:\n{result.StdOut}\n{result.StdErr}") + projectDir + +/// Read back through the path map, a declaration names the file the library was compiled from, whatever the map. +/// The compiler writes each name under a path map with the mapped directory it compiled in at its start; a #line +/// name, which compilers before F# 10 wrote instead, is relative to that directory. [] -[] -[] -let ``a declaration in a project with a path map is named from the root once`` (useTransparentCompiler: bool) : Task = +[] +[] +[] +let ``a declaration in a referenced assembly names the file it was compiled from`` (mappedRoot: string) : Task = task { - let checker = FSharpChecker.Create(useTransparentCompiler = useTransparentCompiler) - let library = SyntheticProject.Create("MappedLibrary", sourceFile "Library" []) - let root = Path.GetDirectoryName library.ProjectDir + let root = TestFramework.createTemporaryDirectory().FullName + let pathMap = if mappedRoot = "" then [] else [ $"--pathmap:{root}={mappedRoot}" ] + let projectDir = emitLibrary root pathMap - let library = - { library with - OtherOptions = [ $"--pathmap:{root}=.{Path.DirectorySeparatorChar}" ] } + let usageLine = "let _ = Library.f 1" + let source = $"module Consumer\n{usageLine}\n" + let consumer = Path.Combine(root, "Consumer.fs") + File.WriteAllText(consumer, source) - let app = - { SyntheticProject.Create("MappedApp", sourceFile "App" [ "Library" ]) with - DependsOn = [ library ] } + let options = + let defaults = CompilerAssert.DefaultProjectOptions(TargetFramework.Current) - do! checkWith checker library - do! checkWith checker app - - let appFile = app.GetFilePath "App" - let appLines = File.ReadAllLines appFile + { defaults with + SourceFiles = [| consumer |] + OtherOptions = [| yield! defaults.OtherOptions; $"""-r:{Path.Combine(projectDir, "Library.dll")}""" |] } - let! _, answer = - checker.ParseAndCheckFileInProject( - appFile, - 0, - SourceText.ofString (File.ReadAllText appFile), - app.GetProjectOptions checker - ) + let! _, answer = FSharpChecker.Create().ParseAndCheckFileInProject(consumer, 0, SourceText.ofString source, options) let checkResults = match answer with | FSharpCheckFileAnswer.Succeeded checkResults -> checkResults | FSharpCheckFileAnswer.Aborted -> failwith "the check was aborted" - let usage = - checkResults.GetAllUsesOfAllSymbolsInFile() - |> Seq.find (fun symbolUse -> symbolUse.Symbol.FullName = $"{library.Name}.ModuleLibrary.f") + let endOfName = usageLine.Length - " 1".Length + let names = [ "Library"; "f" ] let declaration = - checkResults.GetDeclarationLocation( - usage.Range.EndLine, - usage.Range.EndColumn, - appLines[usage.Range.EndLine - 1], - [ "ModuleLibrary"; "f" ] - ) - - match declaration with - | FindDeclResult.DeclFound range -> - Assert.Equal(library.GetFilePath "Library", Path.GetFullPath(Path.Combine(root, range.FileName))) - | result -> failwith $"expected the declaration of ModuleLibrary.f, got %A{result}" + match checkResults.GetDeclarationLocation(2, endOfName, usageLine, names) with + | FindDeclResult.DeclFound range -> range + | result -> failwith $"expected the declaration of Library.f, got %A{result}" + + // Since F# 10 the compiler writes the real file for a #line directive, so the name an older one wrote is + // resolved against this assembly's recorded directory directly + let fromLineDirective = + let symbol = checkResults.GetSymbolUseAtLocation(2, endOfName, usageLine, names).Value.Symbol + let range = Range.mkRange "Original.fs" declaration.Start declaration.End + SymbolHelpers.fileNameOfItem symbol.SymbolEnv.g None range symbol.Item + + // A reader that knows the map turns a name back into the file it stands for, reading a rooted name as the + // full path GetDeclarationLocation gives + let compiledFrom (fileName: string) = + let asRead (path: string) = + if Path.IsPathRooted path then Path.GetFullPath path else path + + if mappedRoot = "" then + Path.GetFullPath fileName + else + let fileName, mappedRoot = asRead fileName, asRead mappedRoot + Assert.StartsWith(mappedRoot, fileName) + Path.GetFullPath(Path.Combine(root, fileName.Substring(mappedRoot.Length).TrimStart('/', '\\'))) + + Assert.Equal(Path.Combine(projectDir, "Library.fs"), compiledFrom declaration.FileName) + Assert.Equal(Path.Combine(projectDir, "Original.fs"), compiledFrom fromLineDirective) } diff --git a/tests/FSharp.Test.Utilities/Compiler.fs b/tests/FSharp.Test.Utilities/Compiler.fs index f7a22ac6c64..f93165a83e0 100644 --- a/tests/FSharp.Test.Utilities/Compiler.fs +++ b/tests/FSharp.Test.Utilities/Compiler.fs @@ -2552,7 +2552,7 @@ $ code --diff {outFile} {expectedFile} type ProcessResult = { ExitCode: int; StdOut: string; StdErr: string } /// Run an F# tool (FSI or FSC) as a subprocess. Shared helper for runFsiProcess / runFscProcess. - let private runToolProcess (toolPath: string) (args: string list) : ProcessResult = + let private runToolProcess (toolPath: string) (workingDirectory: string) (args: string list) : ProcessResult = #if NETCOREAPP let exe = TestFramework.initialConfig.DotNetExe let arguments = toolPath + " " + (args |> String.concat " ") @@ -2560,16 +2560,20 @@ $ code --diff {outFile} {expectedFile} let exe = toolPath let arguments = args |> String.concat " " #endif - let exitCode, stdout, stderr = Commands.executeProcess exe arguments (Directory.GetCurrentDirectory()) + let exitCode, stdout, stderr = Commands.executeProcess exe arguments workingDirectory { ExitCode = exitCode; StdOut = stdout; StdErr = stderr } /// Run FSI as a subprocess with the given arguments. For CLI-level tests only (--help, exit codes, etc.). let runFsiProcess (args: string list) : ProcessResult = - runToolProcess TestFramework.initialConfig.FSI args + runToolProcess TestFramework.initialConfig.FSI (Directory.GetCurrentDirectory()) args + + /// Run FSC as a subprocess in the given directory, which the compiled assembly records as the one it was compiled in. + let runFscProcessIn (workingDirectory: string) (args: string list) : ProcessResult = + runToolProcess TestFramework.initialConfig.FSC workingDirectory args /// Run FSC as a subprocess with the given arguments. For CLI-level tests only (missing files, exit codes, etc.). let runFscProcess (args: string list) : ProcessResult = - runToolProcess TestFramework.initialConfig.FSC args + runFscProcessIn (Directory.GetCurrentDirectory()) args /// Compile-and-run a compilation unit that depends on a FSharp.Core attribute /// which may not yet be shipped in the SDK's NuGet package. From 46e85e2b0f6174dff852a2ec39c4d42a83e28a7b Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 11 Oct 2026 10:32:51 +0200 Subject: [PATCH 13/15] Test that a project keeps its path map after a sibling without one filled the cache Co-Authored-By: Claude Opus 5.5 --- .../FSharpChecker/PathMap.fs | 54 ++++++++++++++----- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs index 18e99d829ce..32cae8490e4 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs @@ -10,25 +10,17 @@ open FSharp.Compiler.Text let private checkWith (checker: FSharpChecker) (project: SyntheticProject) = ProjectWorkflowBuilder(project, checker = checker).Yield() |> Async.Ignore -/// The framework imports, and the TcGlobals with them, are cached per framework set; the path map of -/// the project that filled the cache must not reach the ranges a sibling exposes to its consumers. -[] -[] -[] -let ``a sibling's path map does not reach the ranges of a project without one`` (useTransparentCompiler: bool) : Task = +/// The file an App names as declaring the function it uses from the library, once another project has filled the +/// cache: the framework imports, and the TcGlobals with them, are cached per framework set. +let private libraryFileSeenAfter (useTransparentCompiler: bool) (cacheFiller: SyntheticProject) (library: SyntheticProject) = task { let checker = FSharpChecker.Create(useTransparentCompiler = useTransparentCompiler) - let library = SyntheticProject.Create("Library", sourceFile "Library" []) - - let mapped = - { SyntheticProject.Create("Mapped", sourceFile "Mapped" []) with - OtherOptions = [ $"--pathmap:{Path.GetDirectoryName library.ProjectDir}=.\\" ] } let app = { SyntheticProject.Create("App", sourceFile "App" [ "Library" ]) with DependsOn = [ library ] } - do! checkWith checker mapped + do! checkWith checker cacheFiller do! checkWith checker app let appFile = app.GetFilePath "App" @@ -53,5 +45,41 @@ let ``a sibling's path map does not reach the ranges of a project without one`` failwith $"""ModuleLibrary.f not used; symbols: %A{checkResults.GetAllUsesOfAllSymbolsInFile() |> Seq.map _.Symbol.FullName |> Seq.distinct |> List.ofSeq}""") - Assert.Equal(library.GetFilePath "Library", libraryFunction.Symbol.DeclarationLocation.Value.FileName) + return libraryFunction.Symbol.DeclarationLocation.Value.FileName + } + +/// The path map of the project that filled the cache must not reach the ranges a sibling exposes to its consumers. +[] +[] +[] +let ``a sibling's path map does not reach the ranges of a project without one`` (useTransparentCompiler: bool) : Task = + task { + let library = SyntheticProject.Create("Library", sourceFile "Library" []) + + let mapped = + { SyntheticProject.Create("Mapped", sourceFile "Mapped" []) with + OtherOptions = [ $"--pathmap:{Path.GetDirectoryName library.ProjectDir}=.\\" ] } + + let! declaredIn = libraryFileSeenAfter useTransparentCompiler mapped library + + Assert.Equal(library.GetFilePath "Library", declaredIn) + } + +/// Nor may a project lose its own path map to a sibling without one that filled the cache first. +[] +[] +[] +let ``a project's path map reaches its ranges after a sibling without one filled the cache`` (useTransparentCompiler: bool) : Task = + task { + let unmapped = SyntheticProject.Create("Unmapped", sourceFile "Unmapped" []) + let library = SyntheticProject.Create("Library", sourceFile "Library" []) + let root = Path.GetDirectoryName library.ProjectDir + + let library = + { library with + OtherOptions = [ $"--pathmap:{root}=." ] } + + let! declaredIn = libraryFileSeenAfter useTransparentCompiler unmapped library + + Assert.Equal("." + (library.GetFilePath "Library").Substring(root.Length), declaredIn) } From fc76015d41c965c11d54ef301424548a191f2d2a Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 11 Oct 2026 11:01:37 +0200 Subject: [PATCH 14/15] Quote the options the path map test hands to fsc The test's arguments are joined into one command line. On .NET Framework the default references come from the targeting pack under "Program Files (x86)", so each was split at its spaces and fsc stopped with FS2020. Co-Authored-By: Claude Opus 5.5 --- .../FSharpChecker/PathMap.fs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs index 570974a51d7..5658bb7fbc1 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs @@ -95,11 +95,17 @@ let private emitLibrary (root: string) (pathMap: string list) = let projectDir = Directory.CreateDirectory(Path.Combine(root, "src", "Domain")).FullName File.WriteAllText(Path.Combine(projectDir, "Library.fs"), "module Library\n\nlet f x = x + 1\n") + // The arguments are joined into one command line, where a reference under "Program Files" or a root with a + // space in it would fall apart + let quoted (option: string) = $"\"{option}\"" + let result = runFscProcessIn projectDir [ "--target:library" - yield! CompilerAssert.DefaultProjectOptions(TargetFramework.Current).OtherOptions - yield! pathMap + for option in CompilerAssert.DefaultProjectOptions(TargetFramework.Current).OtherOptions do + quoted option + for option in pathMap do + quoted option "-o:Library.dll" "Library.fs" ] From 75bb45b80599314cc31f69cffcb2a38ded3b9dce Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 11 Oct 2026 11:01:52 +0200 Subject: [PATCH 15/15] Keep the compile directory for a #line name that starts with it Compilers before F# 10 recorded what follows a #line directive under the name the directive gives, relative to the compile directory. Under a relative path map such a name can start with the mapped directory exactly as the name of a compiled file does, and was then taken for one. A name is now used as written only if the item also lies in the file its outermost module or type is declared in; a #line directive leaves the two in different files. A module read from a signature file gives no such evidence, since only values record where their implementation is. PathMapFSharp9.dll is real F# 9 output (FSharp.Compiler.Service 43.9.300). Co-Authored-By: Claude Opus 5.5 --- src/Compiler/Symbols/SymbolHelpers.fs | 68 +++++++++++++- .../FSharpChecker/PathMap.fs | 85 ++++++++++++++++-- .../FSharpChecker/PathMapFSharp9.dll | Bin 0 -> 4096 bytes 3 files changed, 143 insertions(+), 10 deletions(-) create mode 100644 tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMapFSharp9.dll diff --git a/src/Compiler/Symbols/SymbolHelpers.fs b/src/Compiler/Symbols/SymbolHelpers.fs index bb6708f4464..97d5a08ebd4 100644 --- a/src/Compiler/Symbols/SymbolHelpers.fs +++ b/src/Compiler/Symbols/SymbolHelpers.fs @@ -226,11 +226,77 @@ module internal SymbolHelpers = // This is not expected: NewDef only occurs within checking | Item.NewDef _ -> None + /// The module or type of another assembly that an item is declared in, or the item itself when it is one. + let rec declaringEntityOfItem d = + let ofValRef (vref: ValRef) = + if vref.IsLocalRef then ValueNone else ValueSome vref.nlr.EnclosingEntity + + let ofArbitraryValRef (vref: ValRef option) = + match vref with + | Some vref -> ofValRef vref + | None -> ValueNone + + match d with + | Item.Value vref + | Item.CustomBuilder (_, vref) -> ofValRef vref + | Item.ActivePatternCase apref -> ofValRef apref.ActivePatternVal + | Item.UnionCase(ucinfo, _) + | Item.UnionCaseField (ucinfo, _) -> ValueSome ucinfo.TyconRef + | Item.RecdField rfinfo -> ValueSome rfinfo.TyconRef + | Item.ExnCase tcref -> ValueSome tcref + | Item.Event einfo -> ofArbitraryValRef einfo.ArbitraryValRef + | Item.Property(info = pinfo :: _) -> ofArbitraryValRef pinfo.ArbitraryValRef + | Item.MethodGroup(_, minfo :: _, _) + | Item.CtorGroup(_, minfo :: _) -> + match minfo.ArbitraryValRef with + | Some vref -> ofValRef vref + | None -> ValueSome minfo.DeclaringTyconRef + | Item.Types(_, ty :: _) + | Item.DelegateCtor ty -> + match tryNiceEntityRefOfTyOption ty with + | Some tcref -> ValueSome tcref + | None -> ValueNone + | Item.ModuleOrNamespaces (eref :: _) + | Item.UnqualifiedType (eref :: _) -> ValueSome eref + | Item.SetterArg (_, item) -> declaringEntityOfItem item + | _ -> ValueNone + + /// The module or type directly under a namespace that an entity of another assembly lies in, or is. + let outermostDeclaringEntity (eref: EntityRef) = + if eref.IsLocalRef then + ValueNone + else + let (NonLocalEntityRef(ccu, path)) = eref.nlr + + let rec search length = + if length > path.Length then + ValueNone + else + match NonLocalEntityRef(ccu, path[.. length - 1]).TryDeref(canError = false) with + | ValueSome entity when not entity.IsNamespace -> ValueSome entity + | ValueSome _ -> search (length + 1) + | ValueNone -> ValueNone + + search 1 + /// Work out the source file for an item and fix it up relative to the CCU if it is relative. let fileNameOfItem (g: TcGlobals) qualProjectDir (m: range) h = let file = m.FileName if verbose then dprintf "file stored in metadata is '%s'\n" file + // Compilers before F# 10 named whatever follows a #line directive by the directive, and such a name can + // start with the directory too. The directive leaves the item in another file than the module or type + // around it, though, where a compiled file holds both. An entity read from a signature says nothing + // of the file its implementation is in. + let namedByLineDirective () = + match declaringEntityOfItem h |> ValueOption.bind outermostDeclaringEntity with + | ValueSome entity -> + let declaredIn = entity.Range + + m.FileIndex <> declaredIn.FileIndex + && not (CompilerConfig.FSharpSigFileSuffixes |> List.exists (FileSystemUtils.checkSuffix declaredIn.FileName)) + | ValueNone -> false + // A relative name is relative to the directory the assembly was compiled in, as the #line names that // compilers before F# 10 wrote are. A path map rewrites that directory along with the files compiled // from under it, though, so such a file already starts with the directory: joining them repeats it. @@ -240,7 +306,7 @@ module internal SymbolHelpers = && file.StartsWithOrdinal dir && (file[dir.Length] = '\\' || file[dir.Length] = '/') - if startsWithDirectory then file else Path.Combine(dir, file) + if startsWithDirectory && not (namedByLineDirective ()) then file else Path.Combine(dir, file) if not (FileSystem.IsPathRootedShim file) then match ccuOfItem g h with diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs index 5658bb7fbc1..a72b279bdcb 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs @@ -91,10 +91,13 @@ let ``a project's path map reaches its ranges after a sibling without one filled /// Compiles the library from its own directory, as a build does: the assembly records that directory as the one it /// was compiled in. -let private emitLibrary (root: string) (pathMap: string list) = +let private emitLibrary (root: string) (pathMap: string list) (withSignature: bool) = let projectDir = Directory.CreateDirectory(Path.Combine(root, "src", "Domain")).FullName File.WriteAllText(Path.Combine(projectDir, "Library.fs"), "module Library\n\nlet f x = x + 1\n") + if withSignature then + File.WriteAllText(Path.Combine(projectDir, "Library.fsi"), "module Library\n\nval f: x: int -> int\n") + // The arguments are joined into one command line, where a reference under "Program Files" or a root with a // space in it would fall apart let quoted (option: string) = $"\"{option}\"" @@ -107,6 +110,8 @@ let private emitLibrary (root: string) (pathMap: string list) = for option in pathMap do quoted option "-o:Library.dll" + if withSignature then + "Library.fsi" "Library.fs" ] @@ -115,16 +120,18 @@ let private emitLibrary (root: string) (pathMap: string list) = /// Read back through the path map, a declaration names the file the library was compiled from, whatever the map. /// The compiler writes each name under a path map with the mapped directory it compiled in at its start; a #line -/// name, which compilers before F# 10 wrote instead, is relative to that directory. +/// name, which compilers before F# 10 wrote instead, is relative to that directory. With a signature, the module +/// is read from the one file and the implementation of its function is in the other. [] -[] -[] -[] -let ``a declaration in a referenced assembly names the file it was compiled from`` (mappedRoot: string) : Task = +[] +[] +[] +[] +let ``a declaration in a referenced assembly names the file it was compiled from`` (mappedRoot: string) (withSignature: bool) : Task = task { let root = TestFramework.createTemporaryDirectory().FullName let pathMap = if mappedRoot = "" then [] else [ $"--pathmap:{root}={mappedRoot}" ] - let projectDir = emitLibrary root pathMap + let projectDir = emitLibrary root pathMap withSignature let usageLine = "let _ = Library.f 1" let source = $"module Consumer\n{usageLine}\n" @@ -148,11 +155,13 @@ let ``a declaration in a referenced assembly names the file it was compiled from let endOfName = usageLine.Length - " 1".Length let names = [ "Library"; "f" ] - let declaration = - match checkResults.GetDeclarationLocation(2, endOfName, usageLine, names) with + let declarationOf (preferSignature: bool) = + match checkResults.GetDeclarationLocation(2, endOfName, usageLine, names, preferSignature) with | FindDeclResult.DeclFound range -> range | result -> failwith $"expected the declaration of Library.f, got %A{result}" + let declaration = declarationOf false + // Since F# 10 the compiler writes the real file for a #line directive, so the name an older one wrote is // resolved against this assembly's recorded directory directly let fromLineDirective = @@ -175,4 +184,62 @@ let ``a declaration in a referenced assembly names the file it was compiled from Assert.Equal(Path.Combine(projectDir, "Library.fs"), compiledFrom declaration.FileName) Assert.Equal(Path.Combine(projectDir, "Original.fs"), compiledFrom fromLineDirective) + + if withSignature then + Assert.Equal(Path.Combine(projectDir, "Library.fsi"), compiledFrom (declarationOf true).FileName) + } + +/// PathMapFSharp9.dll, next to this file, is what the F# 9 compiler (FSharp.Compiler.Service 43.9.300) made of +/// +/// module Library +/// +/// let ordinary x = x + 1 +/// +/// #line 1 ".\src\Domain\Original.fs" +/// let fromLine x = x + 2 +/// +/// module Nested = +/// let fromLineNested x = x + 3 +/// +/// in a root's src\Domain, with --pathmap:root=. and --deterministic+. Before F# 10 the compiler named whatever +/// follows a #line directive by the directive: here that name starts with the mapped directory, as the name of +/// the compiled file does, and yet is relative to it. +[] +[] +[] +[] +let ``a #line name an older compiler wrote keeps the directory that the name of a compiled file already starts with`` + (name: string) + (recorded: string) + (keepsDirectory: bool) + : Task = + task { + let usageLine = $"let _ = Library.{name}" + let source = $"module Consumer\n{usageLine} 1\n" + let consumer = Path.Combine(TestFramework.createTemporaryDirectory().FullName, "Consumer.fs") + File.WriteAllText(consumer, source) + + let options = + let defaults = CompilerAssert.DefaultProjectOptions(TargetFramework.Current) + + { defaults with + SourceFiles = [| consumer |] + OtherOptions = [| yield! defaults.OtherOptions; $"""-r:{Path.Combine(__SOURCE_DIRECTORY__, "PathMapFSharp9.dll")}""" |] } + + let! _, answer = FSharpChecker.Create().ParseAndCheckFileInProject(consumer, 0, SourceText.ofString source, options) + + let checkResults = + match answer with + | FSharpCheckFileAnswer.Succeeded checkResults -> checkResults + | FSharpCheckFileAnswer.Aborted -> failwith "the check was aborted" + + let declaration = + match checkResults.GetDeclarationLocation(2, usageLine.Length, $"{usageLine} 1", "Library" :: List.ofArray (name.Split '.')) with + | FindDeclResult.DeclFound range -> range + | result -> failwith $"expected the declaration of Library.{name}, got %A{result}" + + let expected = + if keepsDirectory then Path.Combine(@".\src\Domain", recorded) else recorded + + Assert.Equal(expected, declaration.FileName) } diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMapFSharp9.dll b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMapFSharp9.dll new file mode 100644 index 0000000000000000000000000000000000000000..01fb458cb5774244f93cbe57a36c22d1b9d2a80b GIT binary patch literal 4096 zcmeHKZ%|a%6+drT5Fr{^qDY#Kj{s`QxCLQcF$Na|fy%Ori%95LdAs{uUfq3fef!o* zj1~!v)}~FgozxhUm}EM&PLt7wIt*4n#E~G@#Ke%GlQc-9j-66#F&Il4`nzv;`6DLH zhknZBEa&e%_uPBVyYHQgt^O&BAtEDiWQ3?4oPkxRUrw4aJbiY@bZVP=CcT}jJd$jcf_l%)TS@rcSC0A+~lVS<$?6Y(ZN z1JPZRj93g$6U=Jz*I1a4X zy;&7~3LN#lHeis&nhqQXR-)o@sxCgp>Iz_cGf*rw@u49x#w zP=utjz*t1)XdCpwQLP2qa72rtW@xiH1IIj#t3(2ihV>6~qfO%M%_r~y2IHoz+kOt=R) z3j&jifGh@+72u~4`31-^(Z~)#YqSZ0<<3Sy4HTF?J|Bf;r{t9dEu@Oe6n{V!gF(?# zB4~nn;)E}EF21w3J)lW`=@CJb6#1(}|6>113oNlIo{�Bxh4l6a8d&{ZMpkB#T;5 z6J(E|dZRU$k*w8W_isul_?!&fkKVseF)8FijGKsL#(DnVtPW)2t2yi8HW zNG&R4O;pzjZjqI%R#bz?vPi>g;|gg}pRm8#ttl$OAFotUa@zxw?NC zL@FhDgSk2+s!c0H^0XBqtwoSB7Rs3)hi9I{Xr$0s9DUj)%&MS1C|r6+TmM&fAf{L!>`S+s|o$>v)AheEdTlGGn-c* zJ$Uuxw-(eLK0NLCJKaZndV5tZaOv%5SGzyiT-|eQ`yb@)(&HJO)dT0!j5GJ;p8CP= zA@1?MyKdxV-EWxp?!}bk(?tV2pL|c4S#&CM7eA${d*s43slfPLa^{L!e`5O8VT&M0{XH(CSSD(GF zyIT9%UPtQ1&ZX!Xs7*>=zlk#RGSM4gTYmA7Y{bSi1gUny=_>0-WnLDp}FP}?!;`pP5zyEB*^)3Iry6D9I>^U#w_B5wG zefm&E;hKYcHokcM>gz#e#*qh}?=MV!?)<^?t#eNNWR895!7V$No<95T$&SKHk61cx zu1QR6d9QADTYb^l)NbY0_A754NP6G0KK=W5-}m#L;+oD5^D0kyndzm4i%hN6Z>OK# zY`nGjz>T4!vBQIN-{>0PcgM|I(*Cc#+0E;BeLEGGBmnCpf9>z@H}x4}jIpH5)vY?& z#bka=A1_7Q?RXo}TD;LEKA%mHWby~yc)dma<2ca^+DI1h)=Ad?pMlf+>MWw>i7~^y zFy{_-=Ev?1bylJ-_VJHE7ncvImYlQ}w3JptsvtX6L9YN^4w=C*)Mz$>c>e2{#B4k!4z^_aCF)e{v}L%O@9PqD8o9ap+b-{6zA@>Z2w+ zty*X*Y!YV6@MB$b44Ka6Mc29Ad$NDN?(aZM1^3Gk`mXjr*KnXWbEpf;%ByE4V0Y0q zWfHJo5qon$(x{Pa-~o&~Aq$ulpaSgLgE{OrMN|UhuqqpiMj5LR?v(m%rv{?}at>k^ hiyN%~i*SgQ{~P!^SAY-y&v3}{bx!h|+`gy+{{_*5nlAtV literal 0 HcmV?d00001