diff --git a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/CustomAttributeDecoder.cs b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/CustomAttributeDecoder.cs index 45deeee7853047..574fbd28d9c5db 100644 --- a/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/CustomAttributeDecoder.cs +++ b/src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/Ecma335/CustomAttributeDecoder.cs @@ -231,6 +231,26 @@ private ArgumentTypeInfo DecodeFixedArgumentType(ref BlobReader signatureReader, return DecodeFixedArgumentType(ref genericContextReader, default, isElementType); + case SignatureTypeCode.GenericTypeInstance: + // Generic type instantiation. The parameter is a concrete generic type (e.g. GenericClass.E) + // and is only allowed to be System.Type or Enum. + int kind = signatureReader.ReadCompressedInteger(); + if (kind != (int)SignatureTypeKind.Class && kind != (int)SignatureTypeKind.ValueType) + { + throw new BadImageFormatException(); + } + + EntityHandle genericHandle = signatureReader.ReadTypeHandle(); + int genericArgumentCount = signatureReader.ReadCompressedInteger(); + for (int i = 0; i < genericArgumentCount; i++) + { + SkipType(ref signatureReader); + } + + info.Type = GetTypeFromHandle(genericHandle); + info.TypeCode = _provider.IsSystemType(info.Type) ? SerializationTypeCode.Type : (SerializationTypeCode)_provider.GetUnderlyingEnumType(info.Type); + break; + default: throw new BadImageFormatException(); } diff --git a/src/libraries/System.Reflection.Metadata/tests/Metadata/Decoding/CustomAttributeDecoderTests.cs b/src/libraries/System.Reflection.Metadata/tests/Metadata/Decoding/CustomAttributeDecoderTests.cs index b697f7fadab38d..39ad004854cabd 100644 --- a/src/libraries/System.Reflection.Metadata/tests/Metadata/Decoding/CustomAttributeDecoderTests.cs +++ b/src/libraries/System.Reflection.Metadata/tests/Metadata/Decoding/CustomAttributeDecoderTests.cs @@ -299,6 +299,66 @@ public void TestCustomAttributeDecoderGenericArray() } } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.HasAssemblyFiles))] + public void TestCustomAttributeDecoderGenericEnum() + { + Type type = typeof(HasGenericEnumAttribute); + using (FileStream stream = File.OpenRead(AssemblyPathHelper.GetAssemblyLocation(type.GetTypeInfo().Assembly))) + using (PEReader peReader = new PEReader(stream)) + { + MetadataReader reader = peReader.GetMetadataReader(); + CustomAttributeTypeProvider provider = new CustomAttributeTypeProvider(); + TypeDefinitionHandle typeDefHandle = TestMetadataResolver.FindTestType(reader, type); + + bool found = false; + foreach (CustomAttributeHandle attributeHandle in reader.GetCustomAttributes(typeDefHandle)) + { + CustomAttribute attribute = reader.GetCustomAttribute(attributeHandle); + + // Skip attributes that aren't GenericEnumAttribute + if (!IsGenericEnumAttribute(reader, attribute)) + continue; + + CustomAttributeValue value = attribute.DecodeValue(provider); + + Assert.Equal(1, value.FixedArguments.Length); + // For enum types, the Type should be the enum's name in IL assembly notation + // The enum E is nested in GenericClassForEnum`1, which is nested in the test class + Assert.Contains("GenericClassForEnum", value.FixedArguments[0].Type); + Assert.EndsWith("/E", value.FixedArguments[0].Type); + Assert.Equal(0, value.FixedArguments[0].Value); + Assert.Empty(value.NamedArguments); + + found = true; + break; + } + + Assert.True(found, "GenericEnumAttribute was not found on HasGenericEnumAttribute"); + } + } + + private static bool IsGenericEnumAttribute(MetadataReader reader, CustomAttribute attribute) + { + if (attribute.Constructor.Kind == HandleKind.MemberReference) + { + MemberReference memberRef = reader.GetMemberReference((MemberReferenceHandle)attribute.Constructor); + if (memberRef.Parent.Kind == HandleKind.TypeReference) + { + TypeReference typeRef = reader.GetTypeReference((TypeReferenceHandle)memberRef.Parent); + string typeName = reader.GetString(typeRef.Name); + return typeName == "GenericEnumAttribute"; + } + } + else if (attribute.Constructor.Kind == HandleKind.MethodDefinition) + { + MethodDefinition methodDef = reader.GetMethodDefinition((MethodDefinitionHandle)attribute.Constructor); + TypeDefinition typeDef = reader.GetTypeDefinition(methodDef.GetDeclaringType()); + string typeName = reader.GetString(typeDef.Name); + return typeName == "GenericEnumAttribute"; + } + return false; + } + [GenericAttribute] [GenericAttribute("Hello")] [GenericAttribute(12)] @@ -341,6 +401,21 @@ public GenericAttribute2(K key, V value) { } public V Value { get; set; } public K[] ArrayProperty { get; set; } } + + // Test for generic type instantiation in custom attributes (nested enum in generic type) + [GenericEnumAttribute(default(GenericClassForEnum.E))] + public class HasGenericEnumAttribute { } + + public class GenericClassForEnum + { + public enum E : int { Value1 = 0, Value2 = 1 } + } + + [AttributeUsage(AttributeTargets.All)] + public class GenericEnumAttribute : Attribute + { + public GenericEnumAttribute(GenericClassForEnum.E e) { } + } #endif // no arguments @@ -689,6 +764,15 @@ public string GetTypeFromSerializedName(string name) public PrimitiveTypeCode GetUnderlyingEnumType(string type) { +#if NET && !TARGET_BROWSER + // Handle generic type instances with nested enums (e.g., "GenericClassForEnum`1/E") + // These appear when decoding generic type instantiations in custom attribute signatures + if (type.Contains("GenericClassForEnum") && type.Contains("/E")) + { + return PrimitiveTypeCode.Int32; + } +#endif + Type runtimeType = Type.GetType(type.Replace('/', '+')); // '/' vs '+' is only difference between ilasm and reflection notation for fixed set below. if (runtimeType == typeof(SByteEnum))