From 3b0939d99cfe39ecff34f02bc06d5ae9d138f429 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sat, 2 Aug 2025 19:46:18 +0200 Subject: [PATCH] Add ExtensionInfo: mapping of extension members to extension implementations and more. --- .../TypeSystem/TypeSystemLoaderTests.cs | 18 ++ .../TypeSystem/TypeSystemTestCase.cs | 20 ++ .../ICSharpCode.Decompiler.csproj | 1 + .../TypeSystem/ExtensionInfo.cs | 176 ++++++++++++++++++ .../TypeSystem/ITypeDefinition.cs | 1 + .../Implementation/MetadataTypeDefinition.cs | 17 ++ .../Implementation/MinimalCorlib.cs | 1 + 7 files changed, 234 insertions(+) create mode 100644 ICSharpCode.Decompiler/TypeSystem/ExtensionInfo.cs diff --git a/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemLoaderTests.cs b/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemLoaderTests.cs index e689ffc7f..9eb82c286 100644 --- a/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemLoaderTests.cs +++ b/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemLoaderTests.cs @@ -1993,5 +1993,23 @@ namespace ICSharpCode.Decompiler.Tests.TypeSystem Assert.That(@class.HasAttribute(KnownAttribute.SpecialName)); Assert.That(@struct.HasAttribute(KnownAttribute.SpecialName)); } + + [Test] + public void ExtensionEverything() + { + var extensionEverything = GetTypeDefinition(typeof(ExtensionEverything)); + Assert.That(extensionEverything.IsStatic, Is.True, "ExtensionEverything should be static"); + Assert.That(extensionEverything.HasExtensions, Is.True, "ExtensionEverything should have extensions"); + var info = extensionEverything.ExtensionInfo; + Assert.That(info, Is.Not.Null, "ExtensionEverything should have ExtensionInfo"); + foreach (var method in extensionEverything.Methods) + { + Assert.That(method.IsStatic, Is.True, "Method should be static: " + method.Name); + ExtensionMemberInfo? infoOfImpl = info.InfoOfImplementationMember(method); + Assert.That(infoOfImpl, Is.Not.Null, "Method should have implementation info: " + method.Name); + ExtensionMemberInfo? infoOfExtension = info.InfoOfExtensionMember(infoOfImpl.Value.ExtensionMember); + Assert.That(infoOfExtension, Is.EqualTo(infoOfImpl), "Info of extension member should be equal to info of implementation member: " + method.Name); + } + } } } diff --git a/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs b/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs index a526c793f..1636d5fd8 100644 --- a/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs +++ b/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs @@ -753,4 +753,24 @@ namespace ICSharpCode.Decompiler.Tests.TypeSystem [DispId(11)] void StopRouter(); } + + public static class ExtensionEverything + { + extension(int input) + { + public void Method() { } + public void Method(char c) { } + public string AsString => input.ToString(); + public string Test { + get => "Test"; + set { } + } + public static void StaticMethod() { } + public static void StaticMethod(double x) { } + public static string StaticProperty => "StaticProperty"; + public static void GenericMethod(T value) + { + } + } + } } diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index 6d12fb24f..072043f0f 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -155,6 +155,7 @@ + diff --git a/ICSharpCode.Decompiler/TypeSystem/ExtensionInfo.cs b/ICSharpCode.Decompiler/TypeSystem/ExtensionInfo.cs new file mode 100644 index 000000000..f4a080902 --- /dev/null +++ b/ICSharpCode.Decompiler/TypeSystem/ExtensionInfo.cs @@ -0,0 +1,176 @@ +// Copyright (c) 2025 Daniel Grunwald +// +// 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. + +#nullable enable + +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata; + +namespace ICSharpCode.Decompiler.TypeSystem +{ + public class ExtensionInfo + { + readonly Dictionary extensionMemberMap; + readonly Dictionary implementationMemberMap; + + public ExtensionInfo(MetadataModule module, ITypeDefinition extensionContainer) + { + this.extensionMemberMap = new(); + this.implementationMemberMap = new(); + + var metadata = module.MetadataFile.Metadata; + + foreach (var extGroup in extensionContainer.NestedTypes) + { + if (!(extGroup is { Kind: TypeKind.Class, IsSealed: true } + && extGroup.Name.StartsWith("<>E__", System.StringComparison.Ordinal))) + { + continue; + } + + TypeDefinition td = metadata.GetTypeDefinition((TypeDefinitionHandle)extGroup.MetadataToken); + IMethod? marker = null; + bool hasMultipleMarkers = false; + List extensionMethods = []; + + // For easier access to accessors we use SRM + foreach (var h in td.GetMethods()) + { + var method = module.GetDefinition(h); + + if (method.SymbolKind is SymbolKind.Constructor) + continue; + if (method is { Name: "$", IsStatic: true, Parameters.Count: 1 }) + { + if (marker == null) + marker = method; + else + hasMultipleMarkers = true; + continue; + } + + extensionMethods.Add(method); + } + + if (marker == null || hasMultipleMarkers) + continue; + + foreach (var extension in extensionMethods) + { + int expectedTypeParameterCount = extension.TypeParameters.Count + extGroup.TypeParameterCount; + bool hasInstance = !extension.IsStatic; + int parameterOffset = hasInstance ? 1 : 0; + int expectedParameterCount = extension.Parameters.Count + parameterOffset; + TypeParameterSubstitution subst = new TypeParameterSubstitution([], [.. extGroup.TypeArguments, .. extension.TypeArguments]); + + bool IsMatchingImplementation(IMethod impl) + { + if (!impl.IsStatic) + return false; + if (extension.Name != impl.Name) + return false; + if (expectedTypeParameterCount != impl.TypeParameters.Count) + return false; + if (expectedParameterCount != impl.Parameters.Count) + return false; + if (hasInstance) + { + IType ti = impl.Parameters[0].Type.AcceptVisitor(subst); + IType tm = marker.Parameters.Single().Type; + if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(ti, tm)) + return false; + } + for (int i = 0; i < extension.Parameters.Count; i++) + { + IType ti = impl.Parameters[i + parameterOffset].Type.AcceptVisitor(subst); + IType tm = extension.Parameters[i].Type; + if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(ti, tm)) + return false; + } + return NormalizeTypeVisitor.TypeErasure.EquivalentTypes( + impl.ReturnType.AcceptVisitor(subst), + extension.ReturnType + ); + } + + foreach (var impl in extensionContainer.Methods) + { + if (!IsMatchingImplementation(impl)) + continue; + var emi = new ExtensionMemberInfo(marker, extension, impl); + extensionMemberMap[extension] = emi; + implementationMemberMap[impl] = emi; + } + } + } + + } + + public ExtensionMemberInfo? InfoOfExtensionMember(IMethod method) + { + return this.extensionMemberMap.TryGetValue(method, out var value) ? value : null; + } + + public ExtensionMemberInfo? InfoOfImplementationMember(IMethod method) + { + return this.implementationMemberMap.TryGetValue(method, out var value) ? value : null; + } + + public IEnumerable> GetGroups() + { + return this.extensionMemberMap.Values.GroupBy(x => x.ExtensionMarkerMethod); + } + } + + public readonly struct ExtensionMemberInfo(IMethod marker, IMethod extension, IMethod implementation) + { + /// + /// Metadata-only method called '<Extension>$'. Has the C# signature for the extension declaration. + /// + /// extension(ReceiverType name) {} -> void <Extension>$(ReceiverType name) {} + /// + public readonly IMethod ExtensionMarkerMethod = marker; + /// + /// Metadata-only method with a signature as declared in C# within the extension declaration. + /// This could also be an accessor of an extension property. + /// + public readonly IMethod ExtensionMember = extension; + /// + /// The actual implementation method in the outer class. The signature is a concatenation + /// of the extension marker and the extension member's signatures. + /// + public readonly IMethod ImplementationMethod = implementation; + + /// + /// This is the enclosing static class. + /// + public ITypeDefinition ExtensionContainer => ImplementationMethod.DeclaringTypeDefinition!; + + /// + /// This is the compiler-generated class containing the extension members. Has type parameters + /// from the extension declaration with minimal constraints. + /// + public ITypeDefinition ExtensionGroupingType => ExtensionMember.DeclaringTypeDefinition!; + + /// + /// This class holds the type parameters for the extension declaration with full fidelity of C# constraints. + /// + public ITypeDefinition ExtensionMarkerType => ExtensionMarkerMethod.DeclaringTypeDefinition!; + } +} diff --git a/ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs b/ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs index a08fca333..9101162d1 100644 --- a/ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs +++ b/ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs @@ -28,6 +28,7 @@ namespace ICSharpCode.Decompiler.TypeSystem /// public interface ITypeDefinition : ITypeDefinitionOrUnknown, IType, IEntity { + ExtensionInfo? ExtensionInfo { get; } IReadOnlyList NestedTypes { get; } IReadOnlyList Members { get; } diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs index d4b7fff1c..a312bd8a3 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs @@ -41,6 +41,7 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation // eagerly loaded: readonly FullTypeName fullTypeName; readonly TypeAttributes attributes; + public TypeKind Kind { get; } public bool IsByRefLike { get; } public bool IsReadOnly { get; } @@ -143,6 +144,22 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation return $"{MetadataTokens.GetToken(handle):X8} {fullTypeName}"; } + private ExtensionInfo extensionInfo; + + public ExtensionInfo ExtensionInfo { + get { + if (!HasExtensions) + return null; + var extensionInfo = LazyInit.VolatileRead(ref this.extensionInfo); + if (extensionInfo != null) + return extensionInfo; + extensionInfo = new ExtensionInfo(module, this); + if ((module.TypeSystemOptions & TypeSystemOptions.Uncached) != 0) + return extensionInfo; + return LazyInit.GetOrSet(ref this.extensionInfo, extensionInfo); + } + } + ITypeDefinition[] nestedTypes; public IReadOnlyList NestedTypes { diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/MinimalCorlib.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/MinimalCorlib.cs index 802d91636..82f6f79f4 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/MinimalCorlib.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/MinimalCorlib.cs @@ -174,6 +174,7 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation IType IEntity.DeclaringType => null; bool ITypeDefinition.HasExtensions => false; + ExtensionInfo ITypeDefinition.ExtensionInfo => null; bool ITypeDefinition.IsReadOnly => false; TypeKind IType.Kind => typeKind;