Skip to content

Offer every open that resolves the name, and open type where a plain open cannot reach - #20501

Open
xperiandri wants to merge 6 commits into
dotnet:mainfrom
xperiandri:fix/add-open-type
Open

Offer every open that resolves the name, and open type where a plain open cannot reach#20501
xperiandri wants to merge 6 commits into
dotnet:mainfrom
xperiandri:fix/add-open-type

Conversation

@xperiandri

Copy link
Copy Markdown
Contributor

Stacked on #20500 — the first commit here is that PR. Review the last three.

Two defects in what the "open namespace" code fix suggests, both visible on the same example: an unresolved File in File.ReadAllText "x".

A plain open was offered for things it cannot reach

A plain open takes namespaces and F# modules. The fix offered one for every candidate it found, so File was answered with open System.Net.WebRequestMethods and an unresolved WriteLine with open System.Console — code that does not compile, FS0039: The namespace 'WebRequestMethods' is not defined, because those name types, and a type's nested types and static members are only brought into scope by open type.

Nothing in the entity model told the two apart: the namespace to open was the dotted name of the candidate's container, whatever kind of thing that container was. AssemblySymbol now records how far a plain open reaches — the namespace, extended by each enclosing F# module — and InsertionContextEntity how much of the candidate's name the suggested open covers, which the qualification already written at the use site and auto-open modules can both shorten. The fix compares the two and writes open type when the suggestion reaches past what a plain open can.

Only one suggestion was ever offered

The fix computed a suggestion for every namespace and type a name could be resolved from, and then Seq.tryHead threw all but one away — whichever one the assembly crawler happened to reach first. File was answered with System.Net.WebRequestMethods and never with System.IO.

IFSharpMultiCodeFixProvider already exists for providers with more than one suggestion, so the provider implements that instead and registers them all. Following Roslyn's AbstractAddImportCodeFixProvider, which faces the same problem:

  • the list is capped — five opens and three qualifications, against Roslyn's MaxResults of 5 and its fully-qualify's 3 — because a lightbulb is a menu somebody reads;
  • it is ordered with what System holds first (Roslyn's placeSystemNamespaceFirst: true) rather than by the order the crawler walked the assemblies in. Without that, Microsoft.VisualBasic.FileSystem sorts above System.Console for WriteLine.

Tests

AddOpenOnTopOnTests/AddOpenOnTopOffTests now assert the whole list of suggestions rather than one, with cases for a name reachable from two namespaces, a static member reachable from four types, and the qualifications offered after the opens.

🤖 Generated with Claude Code

The "open namespace" code fix and the completion that adds an open both
decided where the declaration goes by recognizing a module header in the
text of a line — `line.StartsWith "module" && not (line.EndsWith "=")` —
followed by arithmetic that assumed a blank line under that header. Every
shape the spelling missed put the open where it does not compile:
`[<AutoOpen>] module Ns.Name` written on one line read as an implicit
module and took the open above the module declaration, and a header with
no blank line under it took the open below the first declaration, leaving
the identifier as unresolved as before.

InsertionContext.Pos now means one thing for every ScopeKind — the first
line inside the scope — taken from the leading keyword the trivia records,
the name it introduces and, for a nested module, the `=`. Attributes and
doc comments sit above that line by construction, so no spelling has to be
recognized. AdjustInsertionPoint keeps only the cosmetic step of stepping
over the blank line under a header, and the editor's two insertion paths
now share one implementation, which also follows the file's own line
endings instead of the host's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

❗ Release notes required

You can open this PR in browser to add release notes: open in github.dev


✅ Found changes and release notes in following paths:

Warning

No PR link found in some release notes, please consider adding it.

Change path Release notes path Description
`src/Compiler` docs/release-notes/.FSharp.Compiler.Service/11.0.100.md No current pull request URL (#20501) found, please consider adding it
`vsintegration/src` docs/release-notes/.VisualStudio/18.vNext.md No current pull request URL (#20501) found, please consider adding it

@xperiandri

xperiandri commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author
image image

@github-actions github-actions Bot added the ⚠️ Affects-Design-Time Tooling check: PR touches type providers or dependency manager label Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

🔍 Tooling Safety Check — Affects-Design-Time
Affects-Design-Time: IDE code-fix logic executes during design-time analysis.

Generated by PR Tooling Safety Check · gpt56 2.6M ·

The formatting-check CI job flagged AddOpenCodeFixProvider.fs.

Co-Authored-By: Claude Sonnet 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.

🤖 🕵️ AI review — verify independently.

|> Seq.toList
|> getSuggestionsAsCodeFixes context sourceText
|> Seq.tryHead))
|> getSuggestionsAsCodeFixes context sourceText))

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] Choosing N.M.FixTarget817 rewrites M.FixTarget817() to M.N.M() (FS0039). Replace the matching long-identifier prefix, not just the diagnostic span.

module Review
module M = let marker = ()
module N =
    module M =
        type FixTarget817() = class end
let x = M.FixTarget817()

@T-Gro T-Gro added the AI-reviewed PR reviewed by AI review council label Sep 10, 2026
@T-Gro
T-Gro self-requested a review September 10, 2026 12:33
xperiandri and others added 4 commits September 10, 2026 15:02
The insertion point is a position, and the change was applied at the start
of its line. That is the same thing whenever what precedes the declaration
on that line is whitespace, and something else entirely when it is not:

    (* header
    *) let x = StringBuilder()

Here the scope's first declaration is on the line that closes the comment,
so the open went between `(* header` and `*)`, inside the comment, leaving
the name as unresolved as it was. Whether the old code got this right was
luck of the same kind: it snapped an implicit module to line 1 and so wrote
above the comment here, but wrote inside it when the comment closed on a
line of its own.

What precedes the declaration is trivia that has to stay where it is, so
the line is broken at the declaration instead, and the declaration keeps
its column - moving it to the start of the line would put the open the
implicit module now begins with offside of it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A plain `open` takes namespaces and F# modules. The code fix offered one
for every candidate it found, so an unresolved `File` was answered with
`open System.Net.WebRequestMethods` and an unresolved `WriteLine` with
`open System.Console`, `open System.Diagnostics.Debug` and
`open System.Diagnostics.Trace` — all of them code that does not compile,
because those name types, and a type's nested types and static members are
only brought into scope by `open type`.

Nothing in the entity model told the two apart: the namespace to open was
the dotted name of the candidate's container, whatever kind of thing that
container was. `AssemblySymbol` now records how far a plain open reaches —
the namespace, extended by each enclosing F# module — and
`InsertionContextEntity` how much of the candidate's name the suggested
open covers, which the qualification already written at the use site and
auto-open modules can both shorten. The code fix compares the two and
writes `open type` when the suggestion reaches past what a plain open can.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The fix computed a suggestion for every namespace and type a name could be
resolved from, and then `Seq.tryHead` threw all but one away — whichever
one the assembly crawler happened to reach first. An unresolved `File` was
answered with `System.Net.WebRequestMethods` and never with `System.IO`;
an unresolved `WriteLine` with `System.Diagnostics.Trace` and never with
`System.Console`.

`IFSharpMultiCodeFixProvider` already exists for providers with more than
one suggestion, so the provider implements that instead and registers them
all. Following Roslyn's add-import fix, which faces the same problem: the
list is capped, at five opens and three qualifications, because a lightbulb
is a menu somebody reads, and it is ordered with what `System` holds first
rather than by whatever order the crawler walked the assemblies in --
without that, `Microsoft.VisualBasic.FileSystem` sorts above
`System.Console` for `WriteLine`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tests filtered the suggestions down to the opens, so what the fix
offers for qualifying the name in place went unasserted — including that
three of them is where the list stops.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

2 participants