From 059a579e272529141454f8057d964a36db8a0186 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 5 Aug 2026 14:37:00 +0200 Subject: [PATCH] Fix #3952: recognize VB-generated names as anonymous types Two predicates disagreed on what a generated name looks like. At the metadata level a '$' in the name counts, so MemberIsHidden treated VB$AnonymousType_0 as an anonymous type and dropped its definition from the output. At the type system level only '<' counted, so none of the anonymous-type translations in CallBuilder and ExpressionBuilder fired. VB assemblies therefore lost the definitions and kept the raw metadata names at every use site, which is not valid C#. Both levels now share one predicate and cannot drift apart again. It keeps the metadata-level behaviour exactly: counting every name that merely contains '<' would newly capture explicit implementations of generic interface members. Assisted-by: Claude:claude-fable-5:Claude Code --- ICSharpCode.Decompiler/NRExtensions.cs | 2 +- ICSharpCode.Decompiler/SRMExtensions.cs | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/ICSharpCode.Decompiler/NRExtensions.cs b/ICSharpCode.Decompiler/NRExtensions.cs index db299ee9a..f3cc16fc5 100644 --- a/ICSharpCode.Decompiler/NRExtensions.cs +++ b/ICSharpCode.Decompiler/NRExtensions.cs @@ -50,7 +50,7 @@ namespace ICSharpCode.Decompiler public static bool HasGeneratedName(this IType type) { - return type.Name.StartsWith("<", StringComparison.Ordinal) || type.Name.Contains("<"); + return SRMExtensions.IsGeneratedName(type.Name); } public static bool IsAnonymousType(this IType type) diff --git a/ICSharpCode.Decompiler/SRMExtensions.cs b/ICSharpCode.Decompiler/SRMExtensions.cs index 2be174f6c..bcd544fb1 100644 --- a/ICSharpCode.Decompiler/SRMExtensions.cs +++ b/ICSharpCode.Decompiler/SRMExtensions.cs @@ -489,9 +489,20 @@ namespace ICSharpCode.Decompiler public static bool IsGeneratedName(this StringHandle handle, MetadataReader metadata) { - return !handle.IsNil - && (metadata.GetString(handle).StartsWith("<", StringComparison.Ordinal) - || metadata.GetString(handle).Contains("$")); + return !handle.IsNil && IsGeneratedName(metadata.GetString(handle)); + } + + /// + /// Detects the mangled names compilers give to entities that have no user-written + /// declaration. The C# compiler prefixes them with '<', the VB compiler separates + /// the parts with '$' (VB$AnonymousType_0, VB$StateMachine_1_Foo). Neither character + /// is legal in a C# or VB identifier. + /// Note that a name may legitimately contain '<' without being generated: explicit + /// implementations of generic interface members are named after the interface. + /// + internal static bool IsGeneratedName(string name) + { + return name.StartsWith("<", StringComparison.Ordinal) || name.Contains("$"); } public static bool HasGeneratedName(this MethodDefinitionHandle handle, MetadataReader metadata)