Browse Source

ExtensionTreeNode for C# 14 extension blocks

Each `extension<T>(ReceiverType source) { ... }` block inside a static class
now appears as its own tree node sibling to the regular methods/properties.
Expanding the node lists the block's declared methods + properties as
PropertyTreeNode / MethodTreeNode children sorted by name.

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
53b965e239
  1. 43
      ILSpy/Languages/CSharpLanguage.cs
  2. 124
      ILSpy/TreeNodes/ExtensionTreeNode.cs
  3. 14
      ILSpy/TreeNodes/TypeTreeNode.cs

43
ILSpy/Languages/CSharpLanguage.cs

@ -198,6 +198,49 @@ namespace ILSpy.Languages @@ -198,6 +198,49 @@ namespace ILSpy.Languages
}
}
/// <summary>
/// Decompiles a C# 14 explicit-extension declaration block back to source. Used by
/// <see cref="TreeNodes.ExtensionTreeNode"/> 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).
/// </summary>
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!;

124
ILSpy/TreeNodes/ExtensionTreeNode.cs

@ -0,0 +1,124 @@ @@ -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
{
/// <summary>
/// Tree-node container for a single C# 14 explicit-extension declaration block
/// — the new <c>extension&lt;T&gt;(ReceiverType source) { ... }</c> 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.
/// </summary>
internal sealed class ExtensionTreeNode : ILSpyTreeNode
{
public ExtensionTreeNode(
ITypeDefinition typeDefinition,
(IMethod Marker, IReadOnlyList<ITypeParameter> 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<ITypeParameter> 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<IProperty>().OrderBy(p => p.Name, NaturalStringComparer.Instance))
this.Children.Add(new PropertyTreeNode(property));
foreach (var method in members.OfType<IMethod>().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<global::ILSpy.SettingsService>().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);
}
}
}

14
ILSpy/TreeNodes/TypeTreeNode.cs

@ -168,6 +168,20 @@ namespace ILSpy.TreeNodes @@ -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<AssemblyTreeNode>().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

Loading…
Cancel
Save