Skip to content

Place the added open from the syntax tree, not from a line of text - #20500

Open
xperiandri wants to merge 3 commits into
dotnet:mainfrom
xperiandri:fix/add-open-placement
Open

Place the added open from the syntax tree, not from a line of text#20500
xperiandri wants to merge 3 commits into
dotnet:mainfrom
xperiandri:fix/add-open-placement

Conversation

@xperiandri

Copy link
Copy Markdown
Contributor

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 the open landed above the module declaration.
  • A header with no blank line under it took the open below the first declaration, leaving the identifier as unresolved as before.
  • namespace Ns with no blank line under it had the open spliced between a type header and its first member.

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. ParsedInput.AdjustInsertionPoint keeps only the cosmetic step of stepping over the blank line under a header.

The editor's two insertion paths (the code fix and the completion commit) now share one implementation, which also follows the file's own line endings instead of the host's.

Tests

OpenDeclarationInsertionTests gains cases for the attribute-on-the-header-line and no-blank-line shapes; AddOpenOnTopOnTests/AddOpenOnTopOffTests gain the end-to-end equivalents. One existing expectation moved by a line: the open now sits next to the code rather than in the gap under the header.

🤖 Generated with Claude Code

@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 (#20500) found, please consider adding it
`vsintegration/src` docs/release-notes/.VisualStudio/18.vNext.md No current pull request URL (#20500) found, please consider adding it

@xperiandri

Copy link
Copy Markdown
Contributor Author

What this looked like before, and what it does now

Three shapes, each with the open the fix produced. The diagnostics below are from fsc on the "before" files, not from memory.

1. An attribute on the module's own line

[<AutoOpen>] module Ns.Helpers

let write () = Console.WriteLine "hi"

line.StartsWith "module" is false for that line, so the file read as an implicit module and the open went to the top of it — above the module declaration, where it cannot go:

open System                              // ← before: above the module declaration

[<AutoOpen>] module Ns.Helpers

let write () = Console.WriteLine "hi"
[<AutoOpen>] module Ns.Helpers

open System                              // ← after: inside the module

let write () = Console.WriteLine "hi"

2. No blank line under the header

module Ns.Helpers
let write () = Console.WriteLine "hi"

The arithmetic added two lines to reach "past the blank line under the header". With no blank line there, that landed past the first declaration, so the open came after the use of the name it was supposed to resolve — the error the fix was invoked on stayed exactly where it was:

module Ns.Helpers
let write () = Console.WriteLine "hi"    // ← before: still FS0039, the open is below it
open System
case2.fs(2,16): error FS0039: The value, namespace, type or module 'Console' is not defined.
module Ns.Helpers

open System                              // ← after

let write () = Console.WriteLine "hi"

3. namespace with no blank line under it

namespace Ns
type Reader() =
    member _.Read() = Console.ReadLine()

Same arithmetic, and here the two lines landed between the type header and its first member, splitting the type in half:

namespace Ns
type Reader() =
open System                              // ← before: inside the type definition

    member _.Read() = Console.ReadLine()
case3.fs(2,6): error FS0547: A type definition requires one or more members or other declarations.
case3.fs(5,5): error FS0010: Unexpected keyword 'member' in implementation file.
namespace Ns

open System                              // ← after

type Reader() =
    member _.Read() = Console.ReadLine()

Why the shapes above are the whole set

The three are not a list of spellings that were special-cased. InsertionContext.Pos now comes from the module's or namespace's leading keyword as the trivia records it, the identifier it introduces, and the = of a nested module — so what precedes the keyword on its line, and what follows the header, stop mattering. What is left in AdjustInsertionPoint is one cosmetic step: if the line under the header is blank, step over it, so the open joins the code rather than sitting in the gap.

@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 completion and code-fix logic executes during design-time analysis.

Generated by PR Tooling Safety Check · gpt56 2.6M ·

@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.

| SynModuleOrNamespaceLeadingKeyword.Module keyword
| SynModuleOrNamespaceLeadingKeyword.Namespace keyword -> headerEndLine keyword ident None
// An implicit module has no header, so its first declaration opens the scope.
| SynModuleOrNamespaceLeadingKeyword.None -> range.StartLine - 1

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.

🤖 🕵️ Adding open System.Text puts it inside the block comment, leaving StringBuilder unresolved (FS0039).

(* header
*) let x = StringBuilder()

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.

Correct, and thank you — verified with fsc rather than by reading:

t_new.fs(6,12): error FS0039: The value or constructor 'StringBuilder' is not defined.

Worth adding that main is no better here, it just fails on the neighbouring shape. Where the comment closes on its own line it wrote the open inside it:

(* header
open System.Text

*)
let x = StringBuilder()

So this is one defect — the change is applied at the start of the anchor's line without regard for what is already on that line — which this PR happened to fix in one shape and inherit in the other.

Fixed in 21f852d. Whatever precedes the insertion point on its line is trivia the declaration follows, so the line is broken at the declaration instead:

(* header
*)
   open System.Text

   let x = StringBuilder()

The declaration keeps its column on purpose — moved to the start of the line it would be offside of the open that the implicit module now begins with. The whitespace between *) and the declaration is taken into the change rather than left behind as trailing whitespace.

Covered by Fixes FS0039 for missing opens - declaration shares its line with the end of a comment.

@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:30
xperiandri and others added 3 commits September 10, 2026 15:56
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>
The formatting-check CI job flagged AddOpenCodeFixProvider.fs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
@xperiandri
xperiandri force-pushed the fix/add-open-placement branch from 21f852d to ca89ff8 Compare September 10, 2026 13:56
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