Browse Source

Fix #3938: nested-type members must not inherit ExtensionInfo

The .NET 10 BCL ships static [Extension] classes that contain ordinary
nested types (e.g. XDocumentExtensions.XDocumentNavigable). Decompiling
such a nested type's member in isolation resolved the enclosing
container's ExtensionInfo, and DecompileBody then dereferenced the
missing extension-member mapping. A container without any extension
blocks now reports no ExtensionInfo at all, and ResolveExtensionInfo
applies a container's info only to members that actually belong to one
of its extension blocks.

Assisted-by: Claude:claude-fable-5:Claude Code
pull/3943/head
Siegfried Pammer 2 months ago committed by Siegfried Pammer
parent
commit
a1726a9dbf
  1. 48
      ICSharpCode.Decompiler.Tests/IsolatedMethodDecompilationTests.cs
  2. 39
      ICSharpCode.Decompiler.Tests/TestCases/IsolatedDecompilation/ExtensionContainerNestedType.cs
  3. 11
      ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
  4. 16
      ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs
  5. 19
      ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs

48
ICSharpCode.Decompiler.Tests/IsolatedMethodDecompilationTests.cs

@ -69,5 +69,53 @@ namespace ICSharpCode.Decompiler.Tests @@ -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();
}
}
}
}

39
ICSharpCode.Decompiler.Tests/TestCases/IsolatedDecompilation/ExtensionContainerNestedType.cs

@ -0,0 +1,39 @@ @@ -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<int> list)
{
public int DoubledCount => list.Count * 2;
}
private sealed class Nested
{
public int Get()
{
return 42;
}
}
}
}

11
ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs

@ -1299,8 +1299,6 @@ namespace ICSharpCode.Decompiler.CSharp @@ -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 @@ -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 @@ -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)
{

16
ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs

@ -153,12 +153,16 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation @@ -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;
}
}

19
ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs

@ -890,6 +890,11 @@ namespace ICSharpCode.Decompiler.TypeSystem @@ -890,6 +890,11 @@ namespace ICSharpCode.Decompiler.TypeSystem
return ns;
}
/// <summary>
/// Returns the <see cref="ExtensionInfo"/> 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.
/// </summary>
public static ExtensionInfo? ResolveExtensionInfo(this IMember member)
{
if (member is null)
@ -898,7 +903,19 @@ namespace ICSharpCode.Decompiler.TypeSystem @@ -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;
}
}
}

Loading…
Cancel
Save