diff --git a/ILSpy.Tests/Metadata/FlagsSchemaInfererTests.cs b/ILSpy.Tests/Metadata/FlagsSchemaInfererTests.cs index 57f7ff715..0589889c9 100644 --- a/ILSpy.Tests/Metadata/FlagsSchemaInfererTests.cs +++ b/ILSpy.Tests/Metadata/FlagsSchemaInfererTests.cs @@ -112,6 +112,18 @@ public class FlagsSchemaInfererTests schema.IndependentFlags.Select(f => f.Name).Should().NotContain("LayoutMask"); } + [Test] + public void TypeAttributes_Includes_The_Type_Forwarder_Bit() + { + // 0x00200000 is the ECMA-335 Forwarder bit (II.23.1.15), set on type-forwarder + // ExportedType rows. System.Reflection.TypeAttributes has no member for it, so the + // enum field walk alone would leave forwarders unfilterable in the flags filter. + var schema = FlagsSchemaInferer.For(typeof(TypeAttributes)); + + schema.IndependentFlags.Should().ContainSingle(f => f.Name == "IsTypeForwarder") + .Which.Bit.Should().Be(0x00200000u); + } + [Test] public void MethodAttributes_Has_Member_Access_Mutex_Plus_Independent_Modifiers() { diff --git a/ILSpy/Metadata/FlagsSchemaInferer.cs b/ILSpy/Metadata/FlagsSchemaInferer.cs index c90b09339..5c0bbc50c 100644 --- a/ILSpy/Metadata/FlagsSchemaInferer.cs +++ b/ILSpy/Metadata/FlagsSchemaInferer.cs @@ -36,6 +36,16 @@ namespace ILSpy.Metadata.Filters { static readonly ConcurrentDictionary cache = new(); + /// + /// Bits that exist in ECMA-335 but have no member in the runtime's enum, so the + /// field walk cannot surface them. Appended to the inferred independent flags. + /// + static readonly Dictionary extraFlags = new() { + // ECMA-335 II.23.1.15 Forwarder: set on ExportedType rows that forward the + // type to another assembly. + [typeof(TypeAttributes)] = [new IndependentFlag("IsTypeForwarder", 0x00200000)], + }; + public static FlagsSchema For(Type enumType) { ArgumentNullException.ThrowIfNull(enumType); @@ -90,6 +100,9 @@ namespace ILSpy.Metadata.Filters independentFlags.Add(new IndependentFlag(name, value)); } + if (extraFlags.TryGetValue(enumType, out var extras)) + independentFlags.AddRange(extras); + // Build the final mutex groups. Drop empty buckets — a *Mask field whose // values weren't named in the enum (or were aliased away) doesn't earn a // dropdown section. Each group always leads with a synthesised "(none)" entry