diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs
index bffde1160..512c9412f 100644
--- a/ILSpy/Languages/CSharpLanguage.cs
+++ b/ILSpy/Languages/CSharpLanguage.cs
@@ -198,6 +198,49 @@ namespace ILSpy.Languages
}
}
+ ///
+ /// Decompiles a C# 14 explicit-extension declaration block back to source. Used by
+ /// when the user activates an extension
+ /// container; the type-level overload is what the node actually calls. The method
+ /// and property overloads exist so individual members can also be decompiled in
+ /// isolation (e.g. when navigated to by analyzer results in a future commit).
+ ///
+ public void DecompileExtension(ITypeDefinition extension, ITextOutput output, DecompilationOptions options)
+ {
+ MetadataFile assembly = extension.ParentModule!.MetadataFile!;
+ CSharpDecompiler decompiler = CreateDecompiler(assembly, options);
+ AddReferenceAssemblyWarningMessage(assembly, output);
+ AddReferenceWarningMessage(assembly, output);
+ WriteCommentLine(output, assembly.FullName);
+ WriteCommentLine(output, TypeToString(extension,
+ ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames | ConversionFlags.SupportExtensionDeclarations));
+ WriteCode(output, options.DecompilerSettings, decompiler.DecompileExtension(extension.MetadataToken), decompiler.TypeSystem);
+ }
+
+ public void DecompileExtension(IMethod extension, ITextOutput output, DecompilationOptions options)
+ {
+ MetadataFile assembly = extension.ParentModule!.MetadataFile!;
+ CSharpDecompiler decompiler = CreateDecompiler(assembly, options);
+ AddReferenceAssemblyWarningMessage(assembly, output);
+ AddReferenceWarningMessage(assembly, output);
+ WriteCommentLine(output, assembly.FullName);
+ WriteCommentLine(output, TypeToString(extension.DeclaringType,
+ ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames | ConversionFlags.SupportExtensionDeclarations));
+ WriteCode(output, options.DecompilerSettings, decompiler.DecompileExtension(extension.MetadataToken), decompiler.TypeSystem);
+ }
+
+ public void DecompileExtension(IProperty extension, ITextOutput output, DecompilationOptions options)
+ {
+ MetadataFile assembly = extension.ParentModule!.MetadataFile!;
+ CSharpDecompiler decompiler = CreateDecompiler(assembly, options);
+ AddReferenceAssemblyWarningMessage(assembly, output);
+ AddReferenceWarningMessage(assembly, output);
+ WriteCommentLine(output, assembly.FullName);
+ WriteCommentLine(output, TypeToString(extension.DeclaringType,
+ ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames | ConversionFlags.SupportExtensionDeclarations));
+ WriteCode(output, options.DecompilerSettings, decompiler.DecompileExtension(extension.MetadataToken), decompiler.TypeSystem);
+ }
+
public override void DecompileEvent(IEvent ev, ITextOutput output, DecompilationOptions options)
{
MetadataFile assembly = ev.ParentModule!.MetadataFile!;
diff --git a/ILSpy/TreeNodes/ExtensionTreeNode.cs b/ILSpy/TreeNodes/ExtensionTreeNode.cs
new file mode 100644
index 000000000..41ee75eb2
--- /dev/null
+++ b/ILSpy/TreeNodes/ExtensionTreeNode.cs
@@ -0,0 +1,124 @@
+// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+
+using ICSharpCode.Decompiler;
+using ICSharpCode.Decompiler.Output;
+using ICSharpCode.Decompiler.TypeSystem;
+
+using ILSpy.AppEnv;
+using ILSpy.Images;
+using ILSpy.Languages;
+
+using SRM = System.Reflection.Metadata;
+
+namespace ILSpy.TreeNodes
+{
+ ///
+ /// Tree-node container for a single C# 14 explicit-extension declaration block
+ /// — the new extension<T>(ReceiverType source) { ... } syntax. Each
+ /// block surfaces under its containing static class as a sibling of the regular
+ /// methods/properties; the block's own members (methods + properties) appear as
+ /// children of this node when expanded.
+ ///
+ internal sealed class ExtensionTreeNode : ILSpyTreeNode
+ {
+ public ExtensionTreeNode(
+ ITypeDefinition typeDefinition,
+ (IMethod Marker, IReadOnlyList TypeParameters) extensionGroup,
+ AssemblyTreeNode parentAssemblyNode)
+ {
+ this.ParentAssemblyNode = parentAssemblyNode ?? throw new ArgumentNullException(nameof(parentAssemblyNode));
+ this.ContainerTypeDefinition = typeDefinition ?? throw new ArgumentNullException(nameof(typeDefinition));
+ this.MarkerMethod = extensionGroup.Marker ?? throw new ArgumentNullException(nameof(extensionGroup.Marker));
+ this.TypeParameters = extensionGroup.TypeParameters ?? throw new ArgumentNullException(nameof(extensionGroup.TypeParameters));
+ this.LazyLoading = true;
+ }
+
+ public ITypeDefinition ContainerTypeDefinition { get; }
+ public IMethod MarkerMethod { get; }
+ public IReadOnlyList TypeParameters { get; }
+ public AssemblyTreeNode ParentAssemblyNode { get; }
+
+ public override object Icon => Images.Images.GetIcon(Images.Images.Class, AccessOverlayIcon.Public, isStatic: false, isExtension: true);
+
+ public override object Text
+ => Language.TypeToString(GetTypeDefinition(), ConversionFlags.SupportExtensionDeclarations);
+
+ public override object NavigationText
+ => Language.TypeToString(GetTypeDefinition(),
+ ConversionFlags.UseFullyQualifiedTypeNames
+ | ConversionFlags.UseFullyQualifiedEntityNames
+ | ConversionFlags.SupportExtensionDeclarations);
+
+ // Avalonia uses the constructor-time marker reference directly. WPF re-resolves
+ // through GetTypeSystemWithCurrentOptionsOrNull so language-version flips refresh
+ // the display string immediately; the Avalonia port doesn't yet expose that helper
+ // (see `extension-methods-tree` follow-ups in the tracker).
+ ITypeDefinition GetTypeDefinition() => MarkerMethod.DeclaringTypeDefinition!;
+
+ protected override void LoadChildren()
+ {
+ var extensionInfo = ContainerTypeDefinition.ExtensionInfo!;
+ var members = extensionInfo.GetMembersOfGroup(MarkerMethod).ToList();
+
+ foreach (var property in members.OfType().OrderBy(p => p.Name, NaturalStringComparer.Instance))
+ this.Children.Add(new PropertyTreeNode(property));
+ foreach (var method in members.OfType().OrderBy(m => m.Name, NaturalStringComparer.Instance))
+ {
+ if (method.MetadataToken.IsNil)
+ continue;
+ this.Children.Add(new MethodTreeNode(method));
+ }
+ }
+
+ public override FilterResult Filter(LanguageSettings settings)
+ {
+ if (Language is not CSharpLanguage)
+ return FilterResult.Hidden;
+
+ var decompilerSettings = TryGetDecompilerSettings();
+ if (decompilerSettings != null && !decompilerSettings.ExtensionMembers)
+ return FilterResult.Hidden;
+
+ return base.Filter(settings);
+ }
+
+ static ICSharpCode.Decompiler.DecompilerSettings? TryGetDecompilerSettings()
+ {
+ try
+ {
+ return AppComposition.Current.GetExport().DecompilerSettings.Clone();
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ public override void Decompile(Language language, ITextOutput output, DecompilationOptions options)
+ {
+ Debug.Assert(language is CSharpLanguage);
+ ((CSharpLanguage)language).DecompileExtension(GetTypeDefinition(), output, options);
+ }
+ }
+}
diff --git a/ILSpy/TreeNodes/TypeTreeNode.cs b/ILSpy/TreeNodes/TypeTreeNode.cs
index b29bc8c7b..d94753353 100644
--- a/ILSpy/TreeNodes/TypeTreeNode.cs
+++ b/ILSpy/TreeNodes/TypeTreeNode.cs
@@ -168,6 +168,20 @@ namespace ILSpy.TreeNodes
Children.Add(new TypeTreeNode((TypeDefinitionHandle)nestedType.MetadataToken, module));
}
+ // C# 14 explicit-extension declaration blocks surface as their own container nodes
+ // — one per (marker, type-params) tuple inside this static class. Filter()-hidden
+ // when the user runs an older C# language version that doesn't recognise them, or
+ // when DecompilerSettings.ExtensionMembers is off.
+ if (typeDef.ExtensionInfo is { } ext)
+ {
+ var parentAssemblyNode = this.Ancestors().OfType().FirstOrDefault();
+ if (parentAssemblyNode != null)
+ {
+ foreach (var group in ext.ExtensionGroups)
+ Children.Add(new ExtensionTreeNode(typeDef, group, parentAssemblyNode));
+ }
+ }
+
// Enums look more useful in declaration order than alphabetical.
var fields = typeDef.Kind == TypeKind.Enum
? typeDef.Fields