Skip to content

Commit 1e21fcf

Browse files
xperiandriclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 9aa05e5 commit 1e21fcf

4 files changed

Lines changed: 88 additions & 48 deletions

File tree

‎docs/release-notes/.FSharp.Compiler.Service/11.0.200.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
### Fixed
66

77
* 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))
8-
* `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))
8+
* `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))
99
* Split Linux CoreCLR CI into two parallel test batches to reduce test turnaround time. ([PR #20587](https://github.com/dotnet/fsharp/pull/20587))
1010
* 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))
1111
* `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))

‎src/Compiler/Symbols/SymbolHelpers.fs‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,10 +231,16 @@ module internal SymbolHelpers =
231231
let file = m.FileName
232232
if verbose then dprintf "file stored in metadata is '%s'\n" file
233233

234-
// A build that maps its source paths maps the directory it compiled in as well, so that directory
235-
// and the file name reach the same unrecorded root on their own: joining them repeats the directory.
234+
// A relative name is relative to the directory the assembly was compiled in, as the #line names that
235+
// compilers before F# 10 wrote are. A path map rewrites that directory along with the files compiled
236+
// from under it, though, so such a file already starts with the directory: joining them repeats it.
236237
let underDirectory (dir: string) =
237-
if FileSystem.IsPathRootedShim dir then Path.Combine(dir, file) else file
238+
let startsWithDirectory =
239+
file.Length > dir.Length
240+
&& file.StartsWithOrdinal dir
241+
&& (file[dir.Length] = '\\' || file[dir.Length] = '/')
242+
243+
if startsWithDirectory then file else Path.Combine(dir, file)
238244

239245
if not (FileSystem.IsPathRootedShim file) then
240246
match ccuOfItem g h with

‎tests/FSharp.Compiler.ComponentTests/FSharpChecker/PathMap.fs‎

Lines changed: 70 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@ module FSharpChecker.PathMap
33
open System.IO
44
open System.Threading.Tasks
55
open Xunit
6+
open FSharp.Test
7+
open FSharp.Test.Compiler
68
open FSharp.Test.ProjectGeneration
9+
open FSharp.Test.Utilities
710
open FSharp.Compiler.CodeAnalysis
811
open FSharp.Compiler.EditorServices
12+
open FSharp.Compiler.Symbols
913
open FSharp.Compiler.Text
1014

1115
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``
5761
Assert.Equal(library.GetFilePath "Library", libraryFunction.Symbol.DeclarationLocation.Value.FileName)
5862
}
5963

60-
/// A build that maps its source paths maps the directory it compiled in as well, so the file name of a
61-
/// range and that directory both reach the same root on their own. Joining them names the directory twice.
64+
/// Compiles the library from its own directory, as a build does: the assembly records that directory as the one it
65+
/// was compiled in.
66+
let private emitLibrary (root: string) (pathMap: string list) =
67+
let projectDir = Directory.CreateDirectory(Path.Combine(root, "src", "Domain")).FullName
68+
File.WriteAllText(Path.Combine(projectDir, "Library.fs"), "module Library\n\nlet f x = x + 1\n")
69+
70+
let result =
71+
runFscProcessIn projectDir [
72+
"--target:library"
73+
yield! CompilerAssert.DefaultProjectOptions(TargetFramework.Current).OtherOptions
74+
yield! pathMap
75+
"-o:Library.dll"
76+
"Library.fs"
77+
]
78+
79+
Assert.True((result.ExitCode = 0), $"fsc exited with {result.ExitCode}:\n{result.StdOut}\n{result.StdErr}")
80+
projectDir
81+
82+
/// Read back through the path map, a declaration names the file the library was compiled from, whatever the map.
83+
/// The compiler writes each name under a path map with the mapped directory it compiled in at its start; a #line
84+
/// name, which compilers before F# 10 wrote instead, is relative to that directory.
6285
[<Theory>]
63-
[<InlineData(false)>]
64-
[<InlineData(true)>]
65-
let ``a declaration in a project with a path map is named from the root once`` (useTransparentCompiler: bool) : Task =
86+
[<InlineData("")>]
87+
[<InlineData(".")>]
88+
[<InlineData("/_/")>]
89+
let ``a declaration in a referenced assembly names the file it was compiled from`` (mappedRoot: string) : Task =
6690
task {
67-
let checker = FSharpChecker.Create(useTransparentCompiler = useTransparentCompiler)
68-
let library = SyntheticProject.Create("MappedLibrary", sourceFile "Library" [])
69-
let root = Path.GetDirectoryName library.ProjectDir
91+
let root = TestFramework.createTemporaryDirectory().FullName
92+
let pathMap = if mappedRoot = "" then [] else [ $"--pathmap:{root}={mappedRoot}" ]
93+
let projectDir = emitLibrary root pathMap
7094

71-
let library =
72-
{ library with
73-
OtherOptions = [ $"--pathmap:{root}=.{Path.DirectorySeparatorChar}" ] }
95+
let usageLine = "let _ = Library.f 1"
96+
let source = $"module Consumer\n{usageLine}\n"
97+
let consumer = Path.Combine(root, "Consumer.fs")
98+
File.WriteAllText(consumer, source)
7499

75-
let app =
76-
{ SyntheticProject.Create("MappedApp", sourceFile "App" [ "Library" ]) with
77-
DependsOn = [ library ] }
100+
let options =
101+
let defaults = CompilerAssert.DefaultProjectOptions(TargetFramework.Current)
78102

79-
do! checkWith checker library
80-
do! checkWith checker app
81-
82-
let appFile = app.GetFilePath "App"
83-
let appLines = File.ReadAllLines appFile
103+
{ defaults with
104+
SourceFiles = [| consumer |]
105+
OtherOptions = [| yield! defaults.OtherOptions; $"""-r:{Path.Combine(projectDir, "Library.dll")}""" |] }
84106

85-
let! _, answer =
86-
checker.ParseAndCheckFileInProject(
87-
appFile,
88-
0,
89-
SourceText.ofString (File.ReadAllText appFile),
90-
app.GetProjectOptions checker
91-
)
107+
let! _, answer = FSharpChecker.Create().ParseAndCheckFileInProject(consumer, 0, SourceText.ofString source, options)
92108

93109
let checkResults =
94110
match answer with
95111
| FSharpCheckFileAnswer.Succeeded checkResults -> checkResults
96112
| FSharpCheckFileAnswer.Aborted -> failwith "the check was aborted"
97113

98-
let usage =
99-
checkResults.GetAllUsesOfAllSymbolsInFile()
100-
|> Seq.find (fun symbolUse -> symbolUse.Symbol.FullName = $"{library.Name}.ModuleLibrary.f")
114+
let endOfName = usageLine.Length - " 1".Length
115+
let names = [ "Library"; "f" ]
101116

102117
let declaration =
103-
checkResults.GetDeclarationLocation(
104-
usage.Range.EndLine,
105-
usage.Range.EndColumn,
106-
appLines[usage.Range.EndLine - 1],
107-
[ "ModuleLibrary"; "f" ]
108-
)
109-
110-
match declaration with
111-
| FindDeclResult.DeclFound range ->
112-
Assert.Equal(library.GetFilePath "Library", Path.GetFullPath(Path.Combine(root, range.FileName)))
113-
| result -> failwith $"expected the declaration of ModuleLibrary.f, got %A{result}"
118+
match checkResults.GetDeclarationLocation(2, endOfName, usageLine, names) with
119+
| FindDeclResult.DeclFound range -> range
120+
| result -> failwith $"expected the declaration of Library.f, got %A{result}"
121+
122+
// Since F# 10 the compiler writes the real file for a #line directive, so the name an older one wrote is
123+
// resolved against this assembly's recorded directory directly
124+
let fromLineDirective =
125+
let symbol = checkResults.GetSymbolUseAtLocation(2, endOfName, usageLine, names).Value.Symbol
126+
let range = Range.mkRange "Original.fs" declaration.Start declaration.End
127+
SymbolHelpers.fileNameOfItem symbol.SymbolEnv.g None range symbol.Item
128+
129+
// A reader that knows the map turns a name back into the file it stands for, reading a rooted name as the
130+
// full path GetDeclarationLocation gives
131+
let compiledFrom (fileName: string) =
132+
let asRead (path: string) =
133+
if Path.IsPathRooted path then Path.GetFullPath path else path
134+
135+
if mappedRoot = "" then
136+
Path.GetFullPath fileName
137+
else
138+
let fileName, mappedRoot = asRead fileName, asRead mappedRoot
139+
Assert.StartsWith(mappedRoot, fileName)
140+
Path.GetFullPath(Path.Combine(root, fileName.Substring(mappedRoot.Length).TrimStart('/', '\\')))
141+
142+
Assert.Equal(Path.Combine(projectDir, "Library.fs"), compiledFrom declaration.FileName)
143+
Assert.Equal(Path.Combine(projectDir, "Original.fs"), compiledFrom fromLineDirective)
114144
}

‎tests/FSharp.Test.Utilities/Compiler.fs‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2552,24 +2552,28 @@ $ code --diff {outFile} {expectedFile}
25522552
type ProcessResult = { ExitCode: int; StdOut: string; StdErr: string }
25532553

25542554
/// Run an F# tool (FSI or FSC) as a subprocess. Shared helper for runFsiProcess / runFscProcess.
2555-
let private runToolProcess (toolPath: string) (args: string list) : ProcessResult =
2555+
let private runToolProcess (toolPath: string) (workingDirectory: string) (args: string list) : ProcessResult =
25562556
#if NETCOREAPP
25572557
let exe = TestFramework.initialConfig.DotNetExe
25582558
let arguments = toolPath + " " + (args |> String.concat " ")
25592559
#else
25602560
let exe = toolPath
25612561
let arguments = args |> String.concat " "
25622562
#endif
2563-
let exitCode, stdout, stderr = Commands.executeProcess exe arguments (Directory.GetCurrentDirectory())
2563+
let exitCode, stdout, stderr = Commands.executeProcess exe arguments workingDirectory
25642564
{ ExitCode = exitCode; StdOut = stdout; StdErr = stderr }
25652565

25662566
/// Run FSI as a subprocess with the given arguments. For CLI-level tests only (--help, exit codes, etc.).
25672567
let runFsiProcess (args: string list) : ProcessResult =
2568-
runToolProcess TestFramework.initialConfig.FSI args
2568+
runToolProcess TestFramework.initialConfig.FSI (Directory.GetCurrentDirectory()) args
2569+
2570+
/// Run FSC as a subprocess in the given directory, which the compiled assembly records as the one it was compiled in.
2571+
let runFscProcessIn (workingDirectory: string) (args: string list) : ProcessResult =
2572+
runToolProcess TestFramework.initialConfig.FSC workingDirectory args
25692573

25702574
/// Run FSC as a subprocess with the given arguments. For CLI-level tests only (missing files, exit codes, etc.).
25712575
let runFscProcess (args: string list) : ProcessResult =
2572-
runToolProcess TestFramework.initialConfig.FSC args
2576+
runFscProcessIn (Directory.GetCurrentDirectory()) args
25732577

25742578
/// Compile-and-run a compilation unit that depends on a FSharp.Core attribute
25752579
/// which may not yet be shipped in the SDK's NuGet package.

0 commit comments

Comments
 (0)