diff --git a/ICSharpCode.Decompiler.Tests/IsolatedMethodDecompilationTests.cs b/ICSharpCode.Decompiler.Tests/IsolatedMethodDecompilationTests.cs index 7fbf62a99..0ad3d1207 100644 --- a/ICSharpCode.Decompiler.Tests/IsolatedMethodDecompilationTests.cs +++ b/ICSharpCode.Decompiler.Tests/IsolatedMethodDecompilationTests.cs @@ -69,5 +69,53 @@ namespace ICSharpCode.Decompiler.Tests Assert.That(code, Does.Contain("Number = 42")); Assert.That(code, Does.Contain("Text = \"hello\"")); } + + [Test] + public async Task NestedTypeMemberInClassicExtensionClass() + { + // #3938: [Extension] on the container alone must not make members of an ordinary + // nested type extension members; here the container has no extension blocks at all. + await DecompileNestedGetMethod("ClassicExtensions").ConfigureAwait(false); + } + + [Test] + public async Task NestedTypeMemberInExtensionBlockContainer() + { + // Same as above, but the container also has a real C# 14 extension block whose + // ExtensionInfo must not be attributed to the nested type's members. + await DecompileNestedGetMethod("BlockExtensions").ConfigureAwait(false); + } + + static async Task DecompileNestedGetMethod(string containerName) + { + var csFile = Path.Combine(TestCasePath, "ExtensionContainerNestedType.cs"); + var compilation = await Tester.CompileCSharp(csFile, CompilerOptions.UseRoslynLatest | CompilerOptions.Optimize | CompilerOptions.Library).ConfigureAwait(false); + try + { + var settings = new DecompilerSettings(); + using var file = new FileStream(compilation.PathToAssembly, FileMode.Open, FileAccess.Read); + var module = new PEFile(compilation.PathToAssembly, file, PEStreamOptions.PrefetchEntireImage); + var targetFramework = module.Metadata.DetectTargetFrameworkId(); + var resolver = new UniversalAssemblyResolver(compilation.PathToAssembly, false, targetFramework, null, PEStreamOptions.PrefetchMetadata); + resolver.AddSearchDirectory(Tester.RefAssembliesToolset.GetPath(targetFramework)); + var typeSystem = new DecompilerTypeSystem(module, resolver, settings); + var decompiler = new CSharpDecompiler(typeSystem, settings); + + var getMethod = typeSystem.MainModule.TypeDefinitions + .Single(t => t.Name == containerName) + .NestedTypes.Single(t => t.Name == "Nested") + .Methods.Single(m => m.Name == "Get"); + + var code = decompiler.Decompile(getMethod.MetadataToken).ToString(); + + Assert.That(code, Does.Contain("return 42;")); + // The nested type's method must not be rendered inside an extension block. + Assert.That(code, Does.Not.Contain("extension(")); + } + finally + { + compilation.DeleteTempFiles(); + } + } } } diff --git a/ICSharpCode.Decompiler.Tests/TestCases/IsolatedDecompilation/ExtensionContainerNestedType.cs b/ICSharpCode.Decompiler.Tests/TestCases/IsolatedDecompilation/ExtensionContainerNestedType.cs new file mode 100644 index 000000000..06172c731 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/IsolatedDecompilation/ExtensionContainerNestedType.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; + +namespace ICSharpCode.Decompiler.Tests.TestCases.IsolatedDecompilation +{ + // A static class whose only extension members use the classic "this" parameter syntax: + // the type carries [Extension], but contains no C# 14 extension blocks. + internal static class ClassicExtensions + { + public static int Twice(this int x) + { + return x * 2; + } + + private sealed class Nested + { + public int Get() + { + return 42; + } + } + } + + // A static class that mixes a real C# 14 extension block with an ordinary nested type. + internal static class BlockExtensions + { + extension(List list) + { + public int DoubledCount => list.Count * 2; + } + + private sealed class Nested + { + public int Get() + { + return 42; + } + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index 5eb7e14f3..1ff6a8fdc 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -1299,8 +1299,6 @@ namespace ICSharpCode.Decompiler.CSharp bool first = true; ITypeDefinition? parentTypeDef = null; - ExtensionInfo? parentExtensionInfo = null; - foreach (var entity in definitions) { switch (entity.Kind) @@ -1319,8 +1317,7 @@ namespace ICSharpCode.Decompiler.CSharp break; case HandleKind.MethodDefinition: IMethod method = module.GetDefinition((MethodDefinitionHandle)entity); - parentExtensionInfo = method.ResolveExtensionInfo(); - syntaxTree.Members.Add(DoDecompile(method, decompileRun, new SimpleTypeResolveContext(method), parentExtensionInfo)); + syntaxTree.Members.Add(DoDecompile(method, decompileRun, new SimpleTypeResolveContext(method), method.ResolveExtensionInfo())); if (first) { parentTypeDef = method.DeclaringTypeDefinition; @@ -1337,14 +1334,14 @@ namespace ICSharpCode.Decompiler.CSharp break; case HandleKind.PropertyDefinition: IProperty property = module.GetDefinition((PropertyDefinitionHandle)entity); - parentExtensionInfo = property.ResolveExtensionInfo(); + var propertyExtensionInfo = property.ResolveExtensionInfo(); if (property.IsParameterizedProperty()) { - syntaxTree.Members.AddRange(DecompileParameterizedProperty(property, decompileRun, new SimpleTypeResolveContext(property), parentExtensionInfo)); + syntaxTree.Members.AddRange(DecompileParameterizedProperty(property, decompileRun, new SimpleTypeResolveContext(property), propertyExtensionInfo)); } else { - syntaxTree.Members.Add(DoDecompile(property, decompileRun, new SimpleTypeResolveContext(property), parentExtensionInfo)); + syntaxTree.Members.Add(DoDecompile(property, decompileRun, new SimpleTypeResolveContext(property), propertyExtensionInfo)); } if (first) { diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs index 9fda3fc71..3f09af785 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs @@ -153,12 +153,16 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation if ((module.TypeSystemOptions & TypeSystemOptions.ExtensionMembers) == 0) 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); + if (extensionInfo == null) + { + extensionInfo = new ExtensionInfo(module, this); + if ((module.TypeSystemOptions & TypeSystemOptions.Uncached) == 0) + extensionInfo = LazyInit.GetOrSet(ref this.extensionInfo, extensionInfo); + } + // A static class whose extension methods all use the classic "this" parameter + // syntax carries [Extension] but has no extension blocks; it is not an + // extension container. + return extensionInfo.ExtensionGroups.Count > 0 ? extensionInfo : null; } } diff --git a/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs b/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs index b4a2f359a..5b7168416 100644 --- a/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs +++ b/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs @@ -890,6 +890,11 @@ namespace ICSharpCode.Decompiler.TypeSystem return ns; } + /// + /// Returns the of the container type the member's extension + /// block belongs to, or null if the member is not part of an extension block. Fields and + /// events always return null: extension blocks cannot declare them. + /// public static ExtensionInfo? ResolveExtensionInfo(this IMember member) { if (member is null) @@ -898,7 +903,19 @@ namespace ICSharpCode.Decompiler.TypeSystem } var td = member.DeclaringTypeDefinition; Debug.Assert(td != null, "IMember.DeclaringTypeDefinition should never be null"); - return td.DeclaringTypeDefinition?.ExtensionInfo ?? td.DeclaringTypeDefinition?.DeclaringTypeDefinition?.ExtensionInfo; + var info = td.DeclaringTypeDefinition?.ExtensionInfo ?? td.DeclaringTypeDefinition?.DeclaringTypeDefinition?.ExtensionInfo; + if (info == null) + return null; + // The container's info only applies to members declared in one of its extension + // blocks; members of other nested types must not pick it up. + IMethod? method = member switch { + IMethod m => m, + IProperty p => p.Getter ?? p.Setter, + _ => null + }; + if (method == null || info.InfoOfExtensionMember((IMethod)method.MemberDefinition) == null) + return null; + return info; } } }