Skip to content

Match a path-mapped file name against the solution, not the current directory - #20519

Open
xperiandri wants to merge 12 commits into
dotnet:mainfrom
xperiandri:fix/pathmap-relative-document-names
Open

xperiandri wants to merge 12 commits into
dotnet:mainfrom
xperiandri:fix/pathmap-relative-document-names

Conversation

@xperiandri

@xperiandri xperiandri commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Go To Definition, Find All References and Rename on a symbol from an assembly built with a path map (DeterministicSourcePaths or <PathMap>) opened generated metadata instead of source. The assembly records file names relative to an unrecorded root, and the editor resolved them against the process's current directory. Relative names are now matched against the solution's document paths; builds without a path map are unaffected.

Editor half of the fix; #20518 and #20470 cover the compiler and project options. Stacked on #20470.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

✅ Release notes checked


✅ Found changes and release notes in following paths:

Change path Release notes path Description
`vsintegration/src` docs/release-notes/.VisualStudio/18.vNext.md

xperiandri added a commit to xperiandri/fsharp that referenced this pull request Sep 11, 2026
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the ⚠️ Affects-Design-Time Tooling check: PR touches type providers or dependency manager label Sep 11, 2026
@github-actions

This comment has been minimized.

Comment thread vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs Outdated
Comment thread vsintegration/src/FSharp.Editor/Common/CodeAnalysisExtensions.fs Outdated
Comment thread vsintegration/src/FSharp.Editor/Common/CodeAnalysisExtensions.fs Outdated
@T-Gro
T-Gro self-requested a review September 11, 2026 12:59
@T-Gro T-Gro added the AI-reviewed PR reviewed by AI review council label Sep 11, 2026
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Sep 11, 2026
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the fix/pathmap-relative-document-names branch from 1482407 to 8075288 Compare September 11, 2026 16:16

@T-Gro T-Gro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖🕵️ Please shorten the description using this guidance. Focus on the problem and why the change is needed, in simplified technical English. Leave the implementation inventory to the Files tab and retain necessary caveats.

@github-project-automation github-project-automation Bot moved this from New to In Progress in F# Compiler and Tooling Sep 15, 2026
@T-Gro T-Gro added the vsintegration-only Changes only Visual Studio integration, plus optional docs, release notes, or tests label Sep 21, 2026
@github-actions

This comment has been minimized.

xperiandri added a commit to xperiandri/fsharp that referenced this pull request Sep 26, 2026
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 26, 2026 01:00
@xperiandri
xperiandri force-pushed the fix/pathmap-relative-document-names branch from 8f8c79f to 86ff6f3 Compare September 26, 2026 01:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved moderate issues remain in path handling and Go To Definition/Find All References lookup paths.

Review effort: Lite
Findings: 3 Medium severity · 1 Low severity

Open (4)
What changed in this PR

Updates F# editor navigation to resolve path-mapped source filenames against solution documents.

Changes:

  • Adds path-aware filename matching for navigation and symbol resolution.
  • Removes path-map options from IDE project options.
  • Adds multi-project tests, helpers, and release notes.
File Reviewed changes
vsintegration/​tests/​FSharp.Editor.Tests/​PathMapNavigationTests.fs Adds path-map navigation coverage; current test does not exercise relative declaration ranges.
vsintegration/​tests/​FSharp.Editor.Tests/​Helpers/​RoslynHelpers.fs Adds multi-target helpers; the new helpers currently have no test call sites.
vsintegration/​tests/​FSharp.Editor.Tests/​FSharp.Editor.Tests.fsproj Includes the new test sources.
vsintegration/​src/​FSharp.Editor/​Navigation/​GoToDefinition.fs Adds path-aware declaration comparison; earlier rooted-path lookups still bypass the new matcher.
vsintegration/​src/​FSharp.Editor/​LanguageService/​Symbols.fs Uses solution-aware file resolution; Find All References still uses the older declaration lookup.
vsintegration/​src/​FSharp.Editor/​LanguageService/​FSharpProjectOptionsManager.fs Filters path-map options from IDE project options.
vsintegration/​src/​FSharp.Editor/​Common/​CodeAnalysisExtensions.fs Adds relative filename matching; deterministic virtual roots and remaining Find All References lookup paths need correction.
docs/​release-notes/​.VisualStudio/​18.vNext.md Documents the fix; contains a duplicate path-map entry.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread vsintegration/src/FSharp.Editor/Common/CodeAnalysisExtensions.fs Outdated
Comment thread vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs Outdated
Comment thread vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs Outdated
Comment thread docs/release-notes/.VisualStudio/18.vNext.md Outdated
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Tooling Safety Check — Affects-Design-Time
Affects-Design-Time: Changes IDE integration executed during design-time.

Generated by PR Tooling Safety Check · gpt56 1.7M · ◷

@github-actions github-actions Bot added the AI-Tooling-Check-Scanned-Clean Tooling check: diff analyzed, no interesting infrastructure files label Sep 28, 2026
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Sep 30, 2026
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the fix/pathmap-relative-document-names branch from c9a267c to c9f0917 Compare September 30, 2026 23:05

@T-Gro T-Gro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 🕵️ Re-reviewed all recent changes as of c9f0917; 1 finding

// maps its source paths leaves that name relative, so it is the solution's own paths it
// is matched against, not the workspace's index of them.
if
originDocument.Project.Solution.GetDocumentIdsWithFSharpFileName targetRange.FileName

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖🕵️ [P2] F12 now opens Unrelated.other instead of metadata when an unrelated assembly's document matches the mapped suffix.

// C:\package\Library.fs; build from C:\package:
// fsc --target:library --pathmap:C:\package=. -o:ExternalLibrary.dll Library.fs
module ExternalLibrary
let value = 42

// Unrelated\Library.fs in the solution; ExternalLibrary's source is absent.
module Unrelated
let other = 99

// Consumer\App.fs references ExternalLibrary.dll.
module Consumer
let result = ExternalLibrary.value // F12 jumps to Unrelated.other

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e0fa3ea: a mapped name is only a candidate now. It reaches a document only in the projects that compile the symbol's assembly, and only if they hold exactly one file ending that way; otherwise F12 keeps the external result. Tests: goto definition stays external when only an unrelated file ends as the mapped name does, which runs your repro against a DLL built with --pathmap, and a mapped name reaches the one file of the declaring assembly that ends with it.

@abonie

abonie commented Oct 1, 2026

Copy link
Copy Markdown
Member

#20462 adds origin/current-solution fallback, while #20519 adds mapped-name matching within a supplied solution; resolving their overlapping callers by choosing either implementation loses the other behavior.

Could we consolidate resolution so it preserves origin-buffer precedence, falls back to the current solution, and requires evidence that a candidate belongs to the declaring assembly and intended signature/implementation location rather than treating a filename suffix as source identity?

That contract needs to hold through navigation, declaration reporting, and Rename—including compatible snapshot ownership for replacement edits—with metadata fallback retained when source resolution fails; a mapped DLL with .fsi/.fs, an unrelated same-suffix file, and a stale origin snapshot would provide useful combined coverage.

@abonie

abonie commented Oct 1, 2026

Copy link
Copy Markdown
Member

A relative filename such as .\Scratch\..\Lib\Library.fs no longer resolves to the workspace’s Lib\Library.fs: suffix matching retains the literal Scratch\.. segments, whereas the previous full-path normalization resolved this case when the process directory was the repository root.

Could we lexically normalize . and resolvable .. segments before constructing the matching suffix, without introducing dependence on the process directory again?

Unresolved leading .. segments need explicit handling rather than silent removal, with regression coverage for equivalent normalized paths and names that cannot be resolved without a known root.

@abonie

abonie commented Oct 1, 2026

Copy link
Copy Markdown
Member

With a binary reference to a path-mapped ExternalLibrary.dll, no library source loaded, and an unrelated Library.fs in the solution, F12 opens the unrelated file and Find All References reports its declaration where the previous code treated the symbol as external.

Could we treat suffix matches as candidates and validate them against the declaring project/assembly before accepting them for either navigation or declaration reporting, rather than protecting only reference-search scope?

When source identity cannot be established, retain the external/metadata fallback instead of accepting even a sole suffix match; the absent-source case and ambiguous same-suffix candidates should be regression tests.

@abonie

abonie commented Oct 1, 2026

Copy link
Copy Markdown
Member

For a mapped DLL built from Library.fsi and Library.fs, F12 finds the implementation document but checks DeclarationLocation, which points to the signature; the subsequent imported-symbol lookup finds no uses in the independently checked implementation, leaving no definition instead of the previous external result.

Could resolution use the implementation or signature location selected for the request when it matches the validated target document, rather than relying exclusively on DeclarationLocation?

Retain the external fallback when source resolution fails, and cover a mapped binary reference through final navigation while preserving signature-origin preference and declaration-toggle behavior.

xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 2, 2026
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the fix/pathmap-relative-document-names branch from 8d36f95 to e2ee950 Compare October 2, 2026 16:03

@T-Gro T-Gro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 🕵️ Re-reviewed all recent changes as of e2ee950; LGTM

@abonie

abonie commented Oct 5, 2026

Copy link
Copy Markdown
Member

Perhaps merge main into this branch now that #20470 has landed the shared helpers and baseline path-map tests. This PR can then reuse them, leaving only its mapped-name resolution changes and additional tests in the diff.

@xperiandri
xperiandri force-pushed the fix/pathmap-relative-document-names branch from e2ee950 to dd4f107 Compare October 8, 2026 01:11
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 8, 2026
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@T-Gro T-Gro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 🕵️ Re-reviewed all recent changes as of dd4f107; LGTM

xperiandri and others added 11 commits October 10, 2026 23:39
…r tests

Test helpers so far put every synthetic file into one Roslyn project. CreateMultiProjectSolution
creates one project per synthetic project with project references, the way VS wires
project-to-project references; CreateMultiTargetSolution creates one project per target
instance sharing the project path and the document paths, the way VS loads a multi-targeted
project.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An assembly built with a path map names its source files relative to a root it
never records. Resolving such a name with Path.GetFullPath resolved it against
the process's current directory, which is not that root and is not even the
solution's - it is wherever the last component to set it left it - so the
answer differed between sessions and named a file that does not exist.
Navigation then took the symbol for an external one and opened generated
metadata instead of its source.

A name that arrives relative is now matched by its tail against the paths the
solution already holds, anchored on a separator so that it matches whole
directories rather than the tail of one. A rooted name still goes through the
workspace's index, so nothing changes for a build without a map.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Finding the document a range names got the caret to the right file, and then the
search for the declaration inside it compared the range's file name to the
document's path with `=`. Under a path map the first is relative to a root the
assembly never records and the second is absolute, so they never match: the
search fell through to a full check of the file, looking for uses of a symbol
that belongs to another compilation, and came back with nothing.

Both places now go through one rule, `isTheFileAt`, rather than two spellings of
it, so a name a path map left relative is matched by its tail wherever a file is
identified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A name a path map left relative is matched by its tail, so a file of an
unrelated project that ends the same way answers to it.  The search then
restricted itself to that project and never looked at the one that references
the assembly, so a use went missing while an unrelated declaration's uses were
reported instead.  Only a project that compiles the symbol's own assembly may
narrow the search now; a rooted name, which resolves to exactly its document,
is unaffected, and so is a file linked into several projects.

Two more things the same lookup got wrong.  A declaration imported from an
assembly can carry a name written on another operating system, and
`Path.IsPathRooted` throws on one where .NET Framework runs, out of a lookup
whose callers expect "no document" for a name it cannot place.  And the scan it
falls back to walks every document of the solution while Find All References
asks for the same name once per project it searches, so the answer is now kept
for the solution - a miss as much as a hit, since a name outside the solution is
asked for just as often.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The scan compares every document of the solution against the name, and the
comparison rebuilt the name each time: split, filtered, joined and interpolated
per document, for a name that does not change between them.  The tail a path
must end with is built once for the lookup now, and each path is read where it
lies.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Go To Definition's `DeclFound` branch and the declaration spans Find All References
reports still asked the workspace's index of the paths on disk for the file a range
names, so a name a path map left relative reached no document there: the definition
was called external and the reference lost its declaration. Both now go through the
resolver that matches such a name against the solution's own paths.

`rangeToNavigableItem` resolved the file again although its caller had already found
the document for that range, so it uses that document.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Linking the note to the PR added a second copy of it instead of editing the first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The multi-project and multi-target helpers now match the ones dotnet#20462 adds,
struct results included, so this branch rebases onto it without reconciling
two copies. A struct tuple cannot be bound by a `let private` pattern, so the
module-level fixture bindings lose the modifier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A struct tuple cannot be bound with `let private` (FS0010: the grammar takes an
access modifier only before a name), so the binding sits in a private AutoOpen
module: the tests here see both names, nothing outside does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the fix/pathmap-relative-document-names branch from dd4f107 to df78b14 Compare October 10, 2026 22:28
A name a path map left relative only ends the way a file's path does, and the
path of an unrelated file can end the same way. Such a name now reaches a
document only for a declaration, in the projects that compile the declaring
assembly, and only when they hold exactly one file ending that way. Otherwise
the declaration stays external, for Go To Definition and for the declaration
Find All References reports alike.

The . and .. segments of a name are resolved in the name itself before it is
matched; a name that climbs above where it starts matches nothing, since what
it denotes depends on the root the assembly never records.

For a mapped assembly built with a signature, Go To Definition from an
implementation file takes the symbol's implementation location instead of
looking only at where it declares itself, and where the mapped source gives no
location the definition is the external one it was before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xperiandri

Copy link
Copy Markdown
Contributor Author

@abonie Addressed in e0fa3ea:

  • Suffix match with the source absent: a suffix match is only a candidate; it is accepted for navigation and for the declaration Find All References reports only in the projects that compile the declaring assembly, and only if they hold exactly one such file, otherwise the symbol stays external (goto definition stays external when only an unrelated file ends as the mapped name does, find all references reports a mapped declaration only in the source of its own assembly, a mapped name reaches the one file of the declaring assembly that ends with it).
  • Normalization: . and resolvable .. segments are resolved in the name itself before the suffix is built, and a name that climbs above where it starts matches nothing (new rows in a relative name denotes the file whose path ends with it).
  • .fsi + .fs: F12 from an implementation file takes the symbol's implementation location, and where the mapped source gives no location the result is the external one (goto definition into a mapped assembly with a signature reaches the file of the origin's kind, goto definition falls back to the assembly when the mapped source gives no location).
  • main: the branch sits on current main, the shared fixture and the baseline path-map tests come from Ignore --pathmap in the IDE's F# project options #20470, and the diff holds only the mapped-name resolution and its tests.

@T-Gro T-Gro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff is still way too big - apply the NoBloat instructions and CodeCompaction skill.

Especially the massive code comments would benefit not just with drastic reduction, but ideally with well named functions/arguments/bindings/types making the intention clear.

If a well named piece of code just tells what the comment says - delete the comment.

  • This diff adds 76 new code comment lines and 6741 new code comment chars.
  • The same path-mapping rationale is repeated across CodeAnalysisExtensions.fs, Symbols.fs, FindUsagesService.fs, and GoToDefinition.fs instead of being expressed once behind a named helper or API.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚠️ Affects-Design-Time Tooling check: PR touches type providers or dependency manager AI-reviewed PR reviewed by AI review council AI-Tooling-Check-Scanned-Clean Tooling check: diff analyzed, no interesting infrastructure files vsintegration-only Changes only Visual Studio integration, plus optional docs, release notes, or tests

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

4 participants