Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>.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);
Comment on lines +243 to +250
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not correct - we're losing type information that the end user might want to interrogate. The user can do this:

var v = reader.GetCustomAttribute(attr).DecodeValue(new CustomAttributeTypeProvider());
_ = v.FixedArguments[0].Type;

And then the .Type obtained by the user would be the .Type we populate here. However, we're losing type information about generic arguments.

We don't have means to provide type information about generic arguments because the interface through which we communicate type information (ICustomAttributeTypeProvider<T>) only deals with SZArrays. No other constructed types are expressible.

I think we need new APIs (several) on ICustomAttributeTypeProvider. We could in theory default-implement these to throw BadImageFormat.

Alternative option is to construct a string representation of the type and use the existing string-based API on ICustomAttributeTypeProvider. However, not all types have string representation (function pointers, modifiers), so we'd still be losing type information.

info.TypeCode = _provider.IsSystemType(info.Type) ? SerializationTypeCode.Type : (SerializationTypeCode)_provider.GetUnderlyingEnumType(info.Type);
break;

default:
throw new BadImageFormatException();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> 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<bool>]
[GenericAttribute<string>("Hello")]
[GenericAttribute<int>(12)]
Expand Down Expand Up @@ -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<int>.E))]
public class HasGenericEnumAttribute { }

public class GenericClassForEnum<T>
{
public enum E : int { Value1 = 0, Value2 = 1 }
}

[AttributeUsage(AttributeTargets.All)]
public class GenericEnumAttribute : Attribute
{
public GenericEnumAttribute(GenericClassForEnum<int>.E e) { }
}
#endif

// no arguments
Expand Down Expand Up @@ -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))
Expand Down
Loading