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