Skip to content

Fix #3938: nested-type members must not inherit ExtensionInfo - #3942

Merged
siegfriedpammer merged 1 commit into
masterfrom
issue-3938-xdocument-navigable
Jul 31, 2026
Merged

siegfriedpammer merged 1 commit into
masterfrom
issue-3938-xdocument-navigable

Conversation

@siegfriedpammer

Copy link
Copy Markdown
Member

Fixes #3938.

Decompiling a single member of an ordinary nested type inside a static [Extension] class (e.g. System.Xml.XPath.XDocumentExtensions.XDocumentNavigable.CreateNavigator in the .NET 10 BCL) threw InvalidOperationException: Nullable object must have a value with language version C# 14+.

Root cause, in two layers:

  • MetadataTypeDefinition.ExtensionInfo was non-null for every static class carrying [Extension] — including classic this-parameter extension classes with no C# 14 extension blocks at all. The group scan found nothing, so the object was empty, but consumers took non-null to mean "extension container". It now returns null when there are no extension groups. (Empty extension blocks like extension(int) {} still produce marker groups and are unaffected.)
  • ResolveExtensionInfo handed the container's info to members of any nested type, so DecompileBody dereferenced a missing InfoOfExtensionMember mapping. It now returns the info only for members that actually belong to one of the container's extension blocks — which also covers containers that mix real extension blocks with ordinary nested types.

ExtensionInfo only ever models C# 14 extension blocks (its maps are populated solely from the <>E__/<G>$ encodings); classic extension methods are resolved via HasExtensions/IsExtensionMethod, which are unchanged.

Both new tests in IsolatedMethodDecompilationTests (classic-only and mixed-container fixtures) reproduced the reported exception before the fix; the stock .NET 10 System.Xml.XPath.XDocument.dll member from the issue now decompiles cleanly.

🤖 Generated with Claude Code

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

Verdict: LGTM. The fix addresses the root cause rather than the crash site, and comes with targeted regression tests for both failure layers. Detailed notes below; inline suggestions are all minor and non-blocking.

Correctness (traced against all consumers)

  • The crash path is closed at its root. DecompileBody only receives a non-null extensionInfo from the isolated-member entry points -- whole-type decompilation passes null down (CSharpDecompiler.cs:1888/1896) and uses the container info only to skip implementation members. Gating ResolveExtensionInfo on InfoOfExtensionMember membership therefore guarantees the !.Value at CSharpDecompiler.cs:2150 is only reached with a valid mapping.
  • The Debug.Assert(extensionInfo != null) at lines 1398/1410 stays valid: those paths are reached only from extension-member UI nodes, whose members are always in the map.
  • The null-when-empty change fixes a latent second bug for free. The two-level fallback td.DeclaringTypeDefinition?.ExtensionInfo ?? ...DeclaringTypeDefinition?.ExtensionInfo (in ResolveExtensionInfo, RequiredNamespaceCollector.cs:88, CSharpAmbience.cs:248, CSharpDecompiler.cs:1646/1666) previously could stop at a non-null-but-empty info from an intermediate nested type and never fall through to the real container. Now it falls through correctly.
  • The claim about empty extension blocks holds in both encodings: a bare extension(int) {} still yields a marker (<>E__ group with <Extension>$, or a <M>$ marker type), so ExtensionGroups.Count > 0.
  • All other consumers handle the new null (TypeTreeNode.cs:152 pattern-matches, CSharpDecompiler.cs:1700 uses ?. + ?? []; MinimalCorlib/SyntheticWpfModule already returned null, so null was always part of the contract).
  • Removing the loop-carried parentExtensionInfo in Decompile(definitions) is a good cleanliness fix -- the old variable persisted stale values across iterations.
  • Caching still works: the empty ExtensionInfo is stored in the field, so repeated getter calls only repeat the cheap Count > 0 check, not the nested-type scan.

Non-blocking follow-up (outside this diff)

  • MethodTreeNode.cs:54 and PropertyTreeNode.cs:58 still do ResolveExtensionInfo()?.InfoOfExtensionMember(...) themselves, so the membership test now runs twice. Harmless; they could be simplified to a null check on ResolveExtensionInfo() in a follow-up.
  • External ILSpyX/plugin consumers of ITypeDefinition.ExtensionInfo will now see null for classic-only [Extension] classes -- a correct tightening, but worth a mention if release notes track API behavior.

Test coverage, conventions (self-contained comments, headerless fixture matching TestCases/, ASCII-only), performance, and security all check out.

🤖 Generated with Claude Code

Comment thread ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs
Comment thread ICSharpCode.Decompiler.Tests/IsolatedMethodDecompilationTests.cs
Comment thread ICSharpCode.Decompiler.Tests/IsolatedMethodDecompilationTests.cs
The .NET 10 BCL ships static [Extension] classes that contain ordinary
nested types (e.g. XDocumentExtensions.XDocumentNavigable). Decompiling
such a nested type's member in isolation resolved the enclosing
container's ExtensionInfo, and DecompileBody then dereferenced the
missing extension-member mapping. A container without any extension
blocks now reports no ExtensionInfo at all, and ResolveExtensionInfo
applies a container's info only to members that actually belong to one
of its extension blocks.

Assisted-by: Claude:claude-fable-5:Claude Code
@siegfriedpammer
siegfriedpammer force-pushed the issue-3938-xdocument-navigable branch from 62338d9 to fffd2e7 Compare July 31, 2026 10:42
@siegfriedpammer
siegfriedpammer merged commit a1726a9 into master Jul 31, 2026
15 checks passed
@siegfriedpammer
siegfriedpammer deleted the issue-3938-xdocument-navigable branch July 31, 2026 11:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

System.Xml.XPath.XDocumentExtensions+XDocumentNavigable.CreateNavigator fails to decompile.

2 participants