Skip to content

Fix NullReferenceException in Entity on unlinked placeholder entities - #20274

Open
xperiandri wants to merge 4 commits into
dotnet:mainfrom
xperiandri:fix-NullReferenceException-breaks-IDE-syntax-coloring-on-unlinked-placeholder-entities
Open

Fix NullReferenceException in Entity on unlinked placeholder entities#20274
xperiandri wants to merge 4 commits into
dotnet:mainfrom
xperiandri:fix-NullReferenceException-breaks-IDE-syntax-coloring-on-unlinked-placeholder-entities

Conversation

@xperiandri

Copy link
Copy Markdown
Contributor

Summary

Fixes #20269 by making typed-tree remapping and classification resilient to placeholder/unlinked entities created while the file is in a broken mid-edit state. Instead of allowing a single null placeholder to crash the entire IDE classification pipeline, the compiler now treats absent module-type augmentation data as null and skips the remap/guard path safely.

Changes

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

❗ Release notes required

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

@xperiandri,

Caution

No release notes found for the changed paths (see table below).

Please make sure to add an entry with an informative description of the change as well as link to this pull request, issue and language suggestion if applicable. Release notes for this repository are based on Keep A Changelog format.

The following format is recommended for this repository:

`* . (PR #XXXXX)`

See examples in the files, listed in the table below or in th full documentation at https://fsharp.github.io/fsharp-compiler-docs/release-notes/About.html.

If you believe that release notes are not necessary for this PR, please add NO_RELEASE_NOTES label to the pull request.

Change path Release notes path Description
`vsintegration/src` docs/release-notes/.VisualStudio/18.vNext.md No release notes found or release notes format is not correct

✅ 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 (#20274) found, please consider adding it

@xperiandri xperiandri changed the title Fix NRE on unlinked placeholder entities and add release notes Fix NullReferenceException in Entity on unlinked placeholder entities Aug 17, 2026
@github-actions github-actions Bot added the AI-Tooling-Check-Scanned-Clean Tooling check: diff analyzed, no interesting infrastructure files label Aug 17, 2026

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.

@T-Gro this is another fix (I can move to another PR if you want)

Comment thread src/Compiler/TypedTree/TypedTree.fs

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

Thanks for digging into this — resilient partial classification during mid-edit is a real, worthwhile goal, and the guarded remapTyconAug / entity_modul_type remap in TypedTreeOps.Remapping.fs are the right shape. A few blocking items before this can go in:

It doesn't build. Marking entity_modul_type and entity_tycon_tcaug as | null in TypedTree.fsi propagates nullability to consumers that weren't updated, and nullness warnings are errors here (outside Proto). A local -c Release build fails with FS3261 at:

  • TypedTreeOps.Transforms.fs(991)d.entity_modul_type.Value is the same unguarded deref you fixed in Remapping.fs, missed here.
  • TypedTreePickle.fs(2818)p_maybe_lazy p_modul_typ x.entity_modul_type passes a nullable to a non-nullable target.
  • TypedTreePickle.fs(2837–2852) — eight TyconAugmentation | null incompatibilities in the tcaug pickle/unpickle path.

That's why every Build_And_Test_* leg is red. If you make these fields nullable you have to sweep all consumers, including the pickler.

~1360 lines of the TypedTree.fs diff are trailing-whitespace churn — only ~20 lines are real change. This trips CheckCodeFormatting, buries the actual fix, and will conflict with everything. Please revert the whitespace-only edits (and the lone one in lib.fs) so the diff is just the guards.

Scope of the type change. These fields were already effectively null for NewUnlinked() placeholders (Unchecked.defaultof<_>), so | null documents an existing latent invariant — but flipping two core Entity fields to nullable forces null-checks everywhere and is a big hammer for an IDE-resilience fix. Worth considering whether the dangling unlinked entity reaching the classification path is itself the bug to fix, rather than making the field type nullable tree-wide.

NewUnlinked() value changes are unnecessary and risky. Switching the placeholder to entity_logical_name = "<unknown>", entity_tycon_repr = TNoRepr, entity_typars = NotLazy [] changes observable pre-Link state on the metadata-unpickling hot path. IsLinked keys off entity_attribs (still defaultof), so linkage detection survives, but none of this is needed for the NRE fix — suggest reverting to Unchecked.defaultof<_>.

No regression test. The linked issue has a clean repro (break a type decl mid-edit); please encode it so this can't silently come back.

lib.fs WeakMap.TryAdd behind #if NETSTANDARD2_0 is correct and a nice cleanup, but it's unrelated to the NRE — fine to keep, just calling it out.

One more: the issue body references files/functions not in this diff (vsintegration/src/FSharp.Editor/*, NewModified*, FreeVars, a NullRef-TypedTree-Classification-Fix.md). Reconciling the description with the actual change will help reviewers follow along.

@github-project-automation github-project-automation Bot moved this from New to In Progress in F# Compiler and Tooling Aug 17, 2026
@T-Gro T-Gro added the AI-reviewed PR reviewed by AI review council label Aug 17, 2026
@T-Gro
T-Gro self-requested a review August 17, 2026 08:41
@xperiandri
xperiandri force-pushed the fix-NullReferenceException-breaks-IDE-syntax-coloring-on-unlinked-placeholder-entities branch from d9c55d0 to 026148d Compare August 17, 2026 11:26

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

One note on the null-fallback design (complements the root-cause point).

Comment thread src/Compiler/TypedTree/TypedTree.fs Outdated
member x.TypeContents = x.entity_tycon_tcaug
member x.TypeContents =
match x.entity_tycon_tcaug with
| null -> TyconAugmentation.Create()

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.

🤖🕵️ On the null branch TypeContents returns a fresh TyconAugmentation.Create() each access, but callers mutate the result in place (tcaug_super <-, tcaug_abstract <-, tcaug_closed <-). If an unlinked placeholder ever reaches such a path, those writes land on a throwaway object and are silently lost — turning a fail-fast NRE into silent typed-tree corruption.

| null -> TyconAugmentation.Create()

ModuleOrNamespaceType (line 884) has the same shape. A shared sentinel, or keeping unlinked entities out of these paths (the root-cause fix), is safer.

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.

So do you think we must save TyconAugmentation.Create() on empty Entity creation?

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 the latest commit

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.

Also, should we then initialize these objects on empty entity creation instead of doing that lazily?

@xperiandri
xperiandri force-pushed the fix-NullReferenceException-breaks-IDE-syntax-coloring-on-unlinked-placeholder-entities branch 2 times, most recently from a9fc80b to 24f2fc9 Compare August 19, 2026 22:13
@github-actions github-actions Bot added ⚠️ Affects-Bootstrap Tooling check: PR touches compiler bootstrap chain ⚠️ Affects-Build-Infra Tooling check: PR touches build infrastructure ⚠️ Scope-Review-Needed Tooling check: PR scope exceeds title/description labels Aug 19, 2026
@github-actions

This comment has been minimized.

xperiandri added a commit to xperiandri/fsharp that referenced this pull request Aug 20, 2026
Always return initialized value for `Entity.entity_modul_type` and `Entity.entity_tycon_tcaug`.
Comment thread FSharpBuild.Directory.Build.props
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Aug 20, 2026
Always return initialized value for `Entity.entity_modul_type` and `Entity.entity_tycon_tcaug`.
@xperiandri
xperiandri force-pushed the fix-NullReferenceException-breaks-IDE-syntax-coloring-on-unlinked-placeholder-entities branch from c3d9be5 to 4a4d62e Compare August 20, 2026 00:56
@xperiandri
xperiandri requested a review from T-Gro August 20, 2026 08:46
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Aug 20, 2026
Always return initialized value for `Entity.entity_modul_type` and `Entity.entity_tycon_tcaug`.
@xperiandri
xperiandri force-pushed the fix-NullReferenceException-breaks-IDE-syntax-coloring-on-unlinked-placeholder-entities branch from 4a4d62e to ba36e07 Compare August 20, 2026 08:51
T-Gro added a commit that referenced this pull request Aug 20, 2026
The previous run was SIGKILL'd by the OOM-killer mid-suite
(0 real test failures; 229 tests never ran). Empty commit to re-run
the pipeline. Same exit-137 flake also hit unrelated PRs #20274 and
#20235 at the same time.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Aug 20, 2026
Always return initialized value for `Entity.entity_modul_type` and `Entity.entity_tycon_tcaug`.
@xperiandri
xperiandri force-pushed the fix-NullReferenceException-breaks-IDE-syntax-coloring-on-unlinked-placeholder-entities branch from ba36e07 to 3f31b99 Compare August 20, 2026 12:57
@github-actions

This comment has been minimized.

@github-actions github-actions Bot added the ⚠️ Affects-Compiler-Output Tooling check: PR touches IL emission or codegen label Aug 20, 2026
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Aug 21, 2026
Always return initialized value for `Entity.entity_modul_type` and `Entity.entity_tycon_tcaug`.
@xperiandri
xperiandri force-pushed the fix-NullReferenceException-breaks-IDE-syntax-coloring-on-unlinked-placeholder-entities branch from 45d3fb2 to c199150 Compare August 21, 2026 14:36
@xperiandri

Copy link
Copy Markdown
Contributor Author

@T-Gro is my approach correct now?

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

Can you pls revert the whitespace only changes and only pick the essentials of the fix?

#if NETSTANDARD2_0
try table.Add(key, value) with | :? ArgumentException -> ()
#else
table.TryAdd(key, value) |> ignore

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 drop the #if NETSTANDARD2_0 / #else here and keep the single try table.Add(key, value) with :? ArgumentException -> () form — it compiles on every TFM. FCS ships netstandard2.0 only, so the #else table.TryAdd branch never ships. Don't move it to a separate PR either; a net-current-only branch would be rejected for the same reason.

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.

@T-Gro is it a correct statement?

@xperiandri
xperiandri force-pushed the fix-NullReferenceException-breaks-IDE-syntax-coloring-on-unlinked-placeholder-entities branch from c199150 to 9acab97 Compare August 24, 2026 14:55
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Aug 24, 2026
Always return initialized value for `Entity.entity_modul_type` and `Entity.entity_tycon_tcaug`.
@xperiandri
xperiandri requested a review from T-Gro August 24, 2026 14:57
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Tooling Safety Check — Affects-Bootstrap, Affects-Compiler-Output, Scope-Review-Needed
Affects-Bootstrap: modifies TypedTreePickle.fs (serialization format)
Affects-Compiler-Output: changes TypedTree representation and remapping
Scope-Review-Needed: touches pickle, remapping, transforms, lib.fs, vsintegration beyond stated NullRef fix

Generated by PR Tooling Safety Check · opus46 4.2M ·

* Used safe `ModuleOrNamespaceType` property instead of `entity_modul_type`.
* Used safe `TypeContents` property instead of `entity_tycon_tcaug`.
* Optimized `TextViewEventsHandler` memory usage with `voption`
Always return initialized value for `Entity.entity_modul_type` and `Entity.entity_tycon_tcaug`.
@xperiandri
xperiandri force-pushed the fix-NullReferenceException-breaks-IDE-syntax-coloring-on-unlinked-placeholder-entities branch from 9acab97 to 6be833e Compare August 26, 2026 19:54
Comment on lines -1112 to +1125
{ entity_typars = Unchecked.defaultof<_>
static member NewUnlinked() : Entity =
{ entity_typars = LazyWithContext.NotLazy []
entity_flags = Unchecked.defaultof<_>
entity_stamp = Unchecked.defaultof<_>
entity_logical_name = Unchecked.defaultof<_>
entity_range = Unchecked.defaultof<_>
entity_logical_name = "<unknown>"

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.

It's important that this does absolute minimum of allocations, Entity is a hot path object.
The string constant is a risk of incorrect data (as opposed to a NRE).

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.

Should it be handled on the property level as the other nullable fields?
or would you like me just make it nullable and handle on usage?

let allFieldsText =
fields
|> Array.map (fun f -> f.LogicalName)
|> Seq.map _.LogicalName

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.

Why?

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.

There is no need to materialize transformation as the String.concat accepts seq. So why would we allocate an array?

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.

Can we close this?

// does not work correctly (they may get incorrectly relinked to a default member)
|> List.filter (fun (isExplicitImpl, _) -> not isExplicitImpl)
|> List.map (fun (_, vref) -> vref.LogicalName, vref)),
|> Seq.filter (fun (isExplicitImpl, _) -> not isExplicitImpl)

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.

Why?

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.

Same here there is no need to materialize the list twice. One lazy sequence materialized at the end

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.

Can we close this?

member x.ModuleOrNamespaceType = x.entity_modul_type.Force()
member x.ModuleOrNamespaceType =
match x.entity_modul_type with
| null -> x.entity_modul_type <- MaybeLazy.Strict (Construct.NewEmptyModuleOrNamespaceType ModuleOrType)

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.

What are the consequences and what is the added value of adding a NewEmptyModuleOrNamespaceType here?

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.

The main point was to prevent the NullReferenceException

@xperiandri
xperiandri requested a review from T-Gro August 27, 2026 11:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚠️ Affects-Bootstrap Tooling check: PR touches compiler bootstrap chain ⚠️ Affects-Build-Infra Tooling check: PR touches build infrastructure ⚠️ Affects-Compiler-Output Tooling check: PR touches IL emission or codegen AI-reviewed PR reviewed by AI review council AI-Tooling-Check-Scanned-Clean Tooling check: diff analyzed, no interesting infrastructure files ⚠️ Scope-Review-Needed Tooling check: PR scope exceeds title/description

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

NullReferenceException breaks IDE syntax coloring on unlinked placeholder entities (e.g. broken code mid-edit)

2 participants