diff --git a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs index 9b8f2d0268c7a0..088587f84ed69d 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs @@ -3591,6 +3591,31 @@ public override GenericParameterAttributes GenericParameterAttributes #endregion #region Generics + + public override unsafe Type? GetNullableUnderlyingType() + { + TypeHandle th = GetNativeTypeHandle(); + if (!th.IsTypeDesc) + { + MethodTable* pMT = th.AsMethodTable(); + if (pMT->IsNullable) + { + // The open generic Nullable<> is also classified as Nullable, and a constructed + // Nullable instantiated over a generic variable holds a TypeDesc (not a + // MethodTable*) in InstantiationArg0(). Fall back to managed reflection in + // those cases. + if (pMT->ContainsGenericVariables) + { + return GetGenericArguments()[0]; + } + RuntimeType result = RuntimeTypeHandle.GetRuntimeTypeFromHandle((IntPtr)pMT->InstantiationArg0()); + GC.KeepAlive(this); + return result; + } + } + return null; + } + internal RuntimeType[] GetGenericArgumentsInternal() { return GetRootElementType().TypeHandle.GetInstantiationInternal(); diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/NativeFormat/NativeFormatRuntimeNamedTypeInfo.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/NativeFormat/NativeFormatRuntimeNamedTypeInfo.cs index 2a0fc6f176cfbf..7284481f7bc4f9 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/NativeFormat/NativeFormatRuntimeNamedTypeInfo.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/NativeFormat/NativeFormatRuntimeNamedTypeInfo.cs @@ -199,6 +199,11 @@ public sealed override string Name protected sealed override IEnumerable TrueCustomAttributes => RuntimeCustomAttributeData.GetCustomAttributes(_reader, _typeDefinition.CustomAttributes); + public sealed override Type? GetNullableUnderlyingType() + { + return (this.ToType() == typeof(Nullable<>)) ? RuntimeGenericTypeParameters[0].ToType() : null; + } + internal sealed override RuntimeTypeInfo[] RuntimeGenericTypeParameters { get diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeConstructedGenericTypeInfo.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeConstructedGenericTypeInfo.cs index 4eaf089573fb1e..8794d70d9d223a 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeConstructedGenericTypeInfo.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeConstructedGenericTypeInfo.cs @@ -85,6 +85,11 @@ public sealed override Type GetGenericTypeDefinition() return GenericTypeDefinitionTypeInfo.ToType(); } + public sealed override Type? GetNullableUnderlyingType() => + GenericTypeDefinitionTypeInfo.ToType() == typeof(Nullable<>) + ? _key.GenericTypeArguments[0].ToType() + : null; + public sealed override Guid GUID { get diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeTypeInfo.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeTypeInfo.cs index 7d99042d8467e2..46b620c9cfc26b 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeTypeInfo.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeTypeInfo.cs @@ -399,6 +399,8 @@ public virtual Type GetGenericTypeDefinition() throw new InvalidOperationException(SR.InvalidOperation_NotGenericType); } + public virtual Type? GetNullableUnderlyingType() => null; + public Type MakeArrayType() { // Do not implement this as a call to MakeArrayType(1) - they are not interchangeable. MakeArrayType() returns a diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs index a0285f166c0eca..97a534a6d567d1 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs @@ -122,6 +122,19 @@ public override Type GetEnumUnderlyingType() return Enum.InternalGetUnderlyingType(this); } + public override Type? GetNullableUnderlyingType() + { + MethodTable* pEEType = _pUnderlyingEEType; + if (pEEType != null) + { + if (!pEEType->IsNullable) + return null; + if (!pEEType->IsGenericTypeDefinition) + return GetTypeFromMethodTable(pEEType->NullableType); + } + return GetRuntimeTypeInfo().GetNullableUnderlyingType(); + } + public override bool IsEnumDefined(object value) { ArgumentNullException.ThrowIfNull(value); diff --git a/src/libraries/Common/tests/System/ModifiedTypeTests.cs b/src/libraries/Common/tests/System/ModifiedTypeTests.cs index 0175ddc892fa7e..2e13c3b8242f60 100644 --- a/src/libraries/Common/tests/System/ModifiedTypeTests.cs +++ b/src/libraries/Common/tests/System/ModifiedTypeTests.cs @@ -867,5 +867,36 @@ public static delegate* delegate* // ret > _fcnPtr_complex; } + [Fact] + public static unsafe void GetNullableUnderlyingType_ModifiedType() + { + FieldInfo fi = typeof(NullableModifiedTypeHolder).Project().GetField(nameof(NullableModifiedTypeHolder._fcnPtr_NullableReturn), Bindings); + + // The function pointer's return type is Nullable. Pulling the modified return type + // produces a ModifiedType wrapping Nullable, which exercises the override. + Type modifiedNullable = fi.GetModifiedFieldType().GetFunctionPointerReturnType(); + Assert.True(IsModifiedType(modifiedNullable)); + Assert.Equal(typeof(int?).Project(), modifiedNullable.UnderlyingSystemType); + + Type modifiedUnderlying = modifiedNullable.GetNullableUnderlyingType(); + Assert.NotNull(modifiedUnderlying); + Assert.True(IsModifiedType(modifiedUnderlying)); + Assert.Equal(typeof(int).Project(), modifiedUnderlying.UnderlyingSystemType); + Assert.Same(modifiedNullable.GetGenericArguments()[0], modifiedUnderlying); + } + + [Fact] + public static unsafe void GetNullableUnderlyingType_ModifiedType_NonNullable_ReturnsNull() + { + FieldInfo fi = typeof(ModifiedTypeHolder).Project().GetField(nameof(ModifiedTypeHolder._volatileInt), Bindings); + Type modifiedInt = fi.GetModifiedFieldType(); + Assert.True(IsModifiedType(modifiedInt)); + Assert.Null(modifiedInt.GetNullableUnderlyingType()); + } + + public unsafe class NullableModifiedTypeHolder + { + public static volatile delegate* _fcnPtr_NullableReturn; + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Nullable.cs b/src/libraries/System.Private.CoreLib/src/System/Nullable.cs index 2eb6e5002968a2..758af84f896c29 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Nullable.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Nullable.cs @@ -104,16 +104,11 @@ public static bool Equals(T? n1, T? n2) where T : struct { ArgumentNullException.ThrowIfNull(nullableType); - if (nullableType.IsGenericType && !nullableType.IsGenericTypeDefinition) - { - // Instantiated generic type only - Type genericType = nullableType.GetGenericTypeDefinition(); - if (ReferenceEquals(genericType, typeof(Nullable<>))) - { - return nullableType.GetGenericArguments()[0]; - } - } - return null; + // COMPAT: Returns null for generic type definition + if (nullableType.IsGenericTypeDefinition) + return null; + + return nullableType.GetNullableUnderlyingType(); } /// diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/EnumBuilder.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/EnumBuilder.cs index b8b250fb8507b3..d25baebf74282b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/EnumBuilder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/EnumBuilder.cs @@ -11,6 +11,9 @@ protected EnumBuilder() { } + // An EnumBuilder represents an enum being built; it cannot itself be a Nullable. + public override Type? GetNullableUnderlyingType() => null; + public FieldBuilder UnderlyingField => UnderlyingFieldCore; diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/GenericTypeParameterBuilder.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/GenericTypeParameterBuilder.cs index dfc38ba67ab088..a82f2a10dfdcfc 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/GenericTypeParameterBuilder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/GenericTypeParameterBuilder.cs @@ -11,6 +11,9 @@ protected GenericTypeParameterBuilder() { } + // A generic type parameter is not a Nullable instantiation. + public override Type? GetNullableUnderlyingType() => null; + public void SetCustomAttribute(ConstructorInfo con, byte[] binaryAttribute) => SetCustomAttributeCore(con, binaryAttribute); protected abstract void SetCustomAttributeCore(ConstructorInfo con, ReadOnlySpan binaryAttribute); diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/SymbolType.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/SymbolType.cs index a17e82b464c725..b154dac0d48b1d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/SymbolType.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/SymbolType.cs @@ -261,6 +261,8 @@ internal void SetFormat(string format, int curIndex, int length) public override bool IsSZArray => _rank <= 1 && _isSzArray; + public override Type? GetNullableUnderlyingType() => null; + public override Type MakePointerType() { return FormCompoundType(_format + "*", _baseType, 0)!; diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilder.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilder.cs index b1a0c16f804bd4..a782db50ffbcba 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilder.cs @@ -12,6 +12,8 @@ protected TypeBuilder() { } + public override Type? GetNullableUnderlyingType() => null; + public const int UnspecifiedTypeSize = 0; public PackingSize PackingSize diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs index 6bdbc966825e93..b7167fde85e323 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs @@ -249,6 +249,7 @@ public override bool ContainsGenericParameters } public override MethodBase? DeclaringMethod => null; public override Type GetGenericTypeDefinition() { return _genericType; } + public override Type? GetNullableUnderlyingType() => _genericType.GetNullableUnderlyingType() is not null ? _typeArguments[0] : null; [RequiresUnreferencedCode("If some of the generic arguments are annotated (either with DynamicallyAccessedMembersAttribute, or generic constraints), trimming can't validate that the requirements of those annotations are met.")] public override Type MakeGenericType(params Type[] inst) { throw new InvalidOperationException(SR.Format(SR.Arg_NotGenericTypeDefinition, this)); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.cs index 89d5401a50b1bc..f4ed19e487a555 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.cs @@ -78,6 +78,7 @@ public override Type[] GetOptionalCustomModifiers() public override bool ContainsGenericParameters => _unmodifiedType.ContainsGenericParameters; public override Type GetGenericTypeDefinition() => _unmodifiedType.GetGenericTypeDefinition(); public override bool IsGenericType => _unmodifiedType.IsGenericType; + public override Type? GetNullableUnderlyingType() => _unmodifiedType.GetNullableUnderlyingType() is not null ? GetGenericArguments()[0] : null; [DynamicallyAccessedMembers(InvokeMemberMembers)] public override object? InvokeMember(string name, BindingFlags invokeAttr, Binder? binder, object? target, diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs index 152a4b6cef8867..67be6b60a67cb8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs @@ -57,6 +57,7 @@ public sealed override bool ContainsGenericParameters internal sealed override SignatureType? ElementType => null; public sealed override int GetArrayRank() => throw new ArgumentException(SR.Argument_HasToBeArrayClass); public sealed override Type GetGenericTypeDefinition() => _genericTypeDefinition; + public sealed override Type? GetNullableUnderlyingType() => _genericTypeDefinition.GetNullableUnderlyingType() is not null ? _genericTypeArguments[0] : null; public sealed override Type[] GetGenericArguments() => GenericTypeArguments; public sealed override Type[] GenericTypeArguments => (Type[])(_genericTypeArguments.Clone()); public sealed override int GenericParameterPosition => throw new InvalidOperationException(SR.Arg_NotGenericParameter); diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.cs index 40d5a9ab977b02..c61466732cf47c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.cs @@ -42,6 +42,7 @@ internal SignatureModifiedType(Type baseType, Type[] requiredCustomModifiers, Ty public override Type[] GenericTypeArguments => _unmodifiedType.GenericTypeArguments; public override int GenericParameterPosition => _unmodifiedType.GenericParameterPosition; internal override SignatureType? ElementType => HasElementType ? new SignatureModifiedType(_unmodifiedType.GetElementType()!, [], []) : null; + public override Type? GetNullableUnderlyingType() => _unmodifiedType.GetNullableUnderlyingType(); public override string Name => _unmodifiedType.Name; public override string? Namespace => _unmodifiedType.Namespace; public override bool IsEnum => _unmodifiedType.IsEnum; diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs index a29ac0441f48d7..8995f99c2dbb86 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs @@ -18,6 +18,9 @@ internal abstract class SignatureType : Type { public sealed override bool IsSignatureType => true; + // The base implementation does not expose Nullable behavior; subclasses override when appropriate. + public override Type? GetNullableUnderlyingType() => null; + // Type flavor predicates public abstract override bool IsTypeDefinition { get; } protected abstract override bool HasElementTypeImpl(); diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.cs index d0ab8d07203f38..f657eaa93d3361 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.cs @@ -152,6 +152,7 @@ public TypeDelegator([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes. protected override bool IsValueTypeImpl() => typeImpl.IsValueType; protected override bool IsCOMObjectImpl() => typeImpl.IsCOMObject; public override bool IsByRefLike => typeImpl.IsByRefLike; + public override Type? GetNullableUnderlyingType() => typeImpl.GetNullableUnderlyingType(); public override bool IsConstructedGenericType => typeImpl.IsConstructedGenericType; public override bool IsCollectible => typeImpl.IsCollectible; diff --git a/src/libraries/System.Private.CoreLib/src/System/Type.cs b/src/libraries/System.Private.CoreLib/src/System/Type.cs index 5a80ac4a18553a..60bf2923b2b6f0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Type.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Type.cs @@ -602,6 +602,18 @@ protected virtual TypeCode GetTypeCodeImpl() public virtual bool IsInstanceOfType([NotNullWhen(true)] object? o) => o != null && IsAssignableFrom(o.GetType()); public virtual bool IsEquivalentTo([NotNullWhen(true)] Type? other) => this == other; + /// + /// Returns the underlying type argument of a type. + /// + /// + /// The type argument of the type if the current type represents + /// the generic type definition or a constructed ; + /// otherwise, . When the current type is the generic type definition + /// (for example, typeof(Nullable<>)), the returned type is the generic type + /// parameter T. + /// + public virtual Type? GetNullableUnderlyingType() => throw new NotSupportedException(SR.NotSupported_SubclassOverride); + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2085:UnrecognizedReflectionPattern", Justification = "The single instance field on enum types is never trimmed")] [Intrinsic] diff --git a/src/libraries/System.Reflection.Emit/ref/System.Reflection.Emit.cs b/src/libraries/System.Reflection.Emit/ref/System.Reflection.Emit.cs index bf5807a9cef73d..afef1b519c5ba1 100644 --- a/src/libraries/System.Reflection.Emit/ref/System.Reflection.Emit.cs +++ b/src/libraries/System.Reflection.Emit/ref/System.Reflection.Emit.cs @@ -162,6 +162,7 @@ protected EnumBuilder() { } public override System.Type? GetNestedType(string name, System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicNestedTypes | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicNestedTypes)] public override System.Type[] GetNestedTypes(System.Reflection.BindingFlags bindingAttr) { throw null; } + public override System.Type? GetNullableUnderlyingType() { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] public override System.Reflection.PropertyInfo[] GetProperties(System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] @@ -293,6 +294,7 @@ protected GenericTypeParameterBuilder() { } public override System.Type GetNestedType(string name, System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicNestedTypes | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicNestedTypes)] public override System.Type[] GetNestedTypes(System.Reflection.BindingFlags bindingAttr) { throw null; } + public override System.Type? GetNullableUnderlyingType() { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] public override System.Reflection.PropertyInfo[] GetProperties(System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] @@ -657,6 +659,7 @@ public void DefineMethodOverride(System.Reflection.MethodInfo methodInfoBody, Sy public override System.Type? GetNestedType(string name, System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicNestedTypes | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicNestedTypes)] public override System.Type[] GetNestedTypes(System.Reflection.BindingFlags bindingAttr) { throw null; } + public override System.Type? GetNullableUnderlyingType() { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] public override System.Reflection.PropertyInfo[] GetProperties(System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] diff --git a/src/libraries/System.Reflection.Emit/tests/System.Reflection.Emit.Tests.csproj b/src/libraries/System.Reflection.Emit/tests/System.Reflection.Emit.Tests.csproj index 64f0b179faaebb..f944d2e9e00cc2 100644 --- a/src/libraries/System.Reflection.Emit/tests/System.Reflection.Emit.Tests.csproj +++ b/src/libraries/System.Reflection.Emit/tests/System.Reflection.Emit.Tests.csproj @@ -117,6 +117,7 @@ + diff --git a/src/libraries/System.Reflection.Emit/tests/TypeBuilder/TypeBuilderGetNullableUnderlyingType.cs b/src/libraries/System.Reflection.Emit/tests/TypeBuilder/TypeBuilderGetNullableUnderlyingType.cs new file mode 100644 index 00000000000000..bed425a32e1ce9 --- /dev/null +++ b/src/libraries/System.Reflection.Emit/tests/TypeBuilder/TypeBuilderGetNullableUnderlyingType.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Reflection.Emit.Tests +{ + public class TypeBuilderGetNullableUnderlyingType + { + [Fact] + public void TypeBuilder_ReturnsNull() + { + TypeBuilder tb = Helpers.DynamicType(TypeAttributes.Public); + Assert.Null(tb.GetNullableUnderlyingType()); + } + + [Fact] + public void EnumBuilder_ReturnsNull() + { + EnumBuilder eb = Helpers.DynamicEnum(TypeAttributes.Public, typeof(int)); + Assert.Null(eb.GetNullableUnderlyingType()); + } + + [Fact] + public void GenericTypeParameterBuilder_ReturnsNull() + { + TypeBuilder tb = Helpers.DynamicType(TypeAttributes.Public); + GenericTypeParameterBuilder[] gps = tb.DefineGenericParameters("T"); + Assert.Null(gps[0].GetNullableUnderlyingType()); + } + + [Fact] + public void TypeBuilderInstantiation_ReturnsNull() + { + // A TypeBuilderInstantiation is produced when MakeGenericType is called on a + // generic TypeBuilder. The open generic definition is a TypeBuilder (never + // typeof(Nullable<>)), so the override always returns null in practice. + TypeBuilder tb = Helpers.DynamicType(TypeAttributes.Public); + tb.DefineGenericParameters("T"); + Type instantiation = tb.MakeGenericType(typeof(int)); + Assert.Null(instantiation.GetNullableUnderlyingType()); + } + + [Fact] + public void SymbolType_Array_ReturnsNull() + { + TypeBuilder tb = Helpers.DynamicType(TypeAttributes.Public); + Type arrayType = tb.MakeArrayType(); + Assert.Null(arrayType.GetNullableUnderlyingType()); + Assert.Null(Nullable.GetUnderlyingType(arrayType)); + } + + [Fact] + public void SymbolType_MultiDimArray_ReturnsNull() + { + TypeBuilder tb = Helpers.DynamicType(TypeAttributes.Public); + Type arrayType = tb.MakeArrayType(2); + Assert.Null(arrayType.GetNullableUnderlyingType()); + Assert.Null(Nullable.GetUnderlyingType(arrayType)); + } + + [Fact] + public void SymbolType_Pointer_ReturnsNull() + { + TypeBuilder tb = Helpers.DynamicType(TypeAttributes.Public); + Type pointerType = tb.MakePointerType(); + Assert.Null(pointerType.GetNullableUnderlyingType()); + Assert.Null(Nullable.GetUnderlyingType(pointerType)); + } + + [Fact] + public void SymbolType_ByRef_ReturnsNull() + { + TypeBuilder tb = Helpers.DynamicType(TypeAttributes.Public); + Type byRefType = tb.MakeByRefType(); + Assert.Null(byRefType.GetNullableUnderlyingType()); + Assert.Null(Nullable.GetUnderlyingType(byRefType)); + } + } +} diff --git a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoModifiedType.cs b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoModifiedType.cs index 1ec881fd27a9a3..0cf1eaf4f7f2ef 100644 --- a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoModifiedType.cs +++ b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoModifiedType.cs @@ -168,6 +168,10 @@ public override IEnumerable DeclaredNestedTypes public override Type GetGenericTypeDefinition() => throw new NotSupportedException(SR.NotSupported_ModifiedType); +#if NET11_0_OR_GREATER + public override Type? GetNullableUnderlyingType() => _unmodifiedType.GetNullableUnderlyingType() is not null ? GetGenericArguments()[0] : null; +#endif + // Generic parameters are supported. internal override RoType[] GetGenericTypeParametersNoCopy() => _unmodifiedType.GetGenericTypeParametersNoCopy(); internal override RoType[] GetGenericTypeArgumentsNoCopy() => _unmodifiedType.GetGenericTypeArgumentsNoCopy(); diff --git a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs index a5b0738d246f10..3ee62bfd62d42c 100644 --- a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs +++ b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs @@ -327,6 +327,26 @@ public sealed override Type MakeArrayType(int rank) private RoType? _lazyUnderlyingEnumType; public sealed override Array GetEnumValues() => throw new InvalidOperationException(SR.Arg_InvalidOperation_Reflection); + // Nullable methods +#if NET11_0_OR_GREATER + public override Type? GetNullableUnderlyingType() + { + if (IsGenericType) + { + RoType? nullableOfT = Loader.TryGetCoreType(CoreType.NullableT); + if (nullableOfT is not null && GetGenericTypeDefinition() == nullableOfT) + { + // Use GetGenericArguments() to cover both constructed Nullable + // (returns T) and the generic type definition Nullable<> + // (returns the generic type parameter). + return GetGenericArguments()[0]; + } + } + + return null; + } +#endif + #if NET [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2085:UnrecognizedReflectionPattern", Justification = "Enum Types are not trimmed.")] diff --git a/src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csproj b/src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csproj index 64669968feebf2..33e1e4ce93627d 100644 --- a/src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csproj +++ b/src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csproj @@ -62,6 +62,7 @@ + diff --git a/src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.cs b/src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.cs new file mode 100644 index 00000000000000..72b0bdf87f562a --- /dev/null +++ b/src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET + +using Xunit; + +namespace System.Reflection.Tests +{ + public static partial class TypeTests + { + [Fact] + public static void GetNullableUnderlyingType_MetadataLoadContext_NullableInt_ReturnsUnderlyingType() + { + string coreAssemblyPath = TestUtils.GetPathToCoreAssembly(); + var resolver = new PathAssemblyResolver([coreAssemblyPath]); + using var mlc = new MetadataLoadContext(resolver, TestUtils.GetNameOfCoreAssembly()); + + Assembly coreAssembly = mlc.LoadFromAssemblyPath(coreAssemblyPath); + Type intType = coreAssembly.GetType("System.Int32", throwOnError: true)!; + Type nullableIntType = coreAssembly.GetType("System.Nullable`1", throwOnError: true)!.MakeGenericType(intType); + + Type? underlying = Nullable.GetUnderlyingType(nullableIntType); + Assert.NotNull(underlying); + Assert.Equal("System.Int32", underlying.FullName); + Assert.Same(intType, underlying); + Assert.NotSame(typeof(int), underlying); + + Type? underlyingDirect = nullableIntType.GetNullableUnderlyingType(); + Assert.NotNull(underlyingDirect); + Assert.Equal("System.Int32", underlyingDirect.FullName); + Assert.Same(intType, underlyingDirect); + Assert.NotSame(typeof(int), underlyingDirect); + } + + [Fact] + public static void GetNullableUnderlyingType_MetadataLoadContext_NonNullableTypes_ReturnsNull() + { + string coreAssemblyPath = TestUtils.GetPathToCoreAssembly(); + var resolver = new PathAssemblyResolver([coreAssemblyPath]); + using var mlc = new MetadataLoadContext(resolver, TestUtils.GetNameOfCoreAssembly()); + + Assembly coreAssembly = mlc.LoadFromAssemblyPath(coreAssemblyPath); + Type intType = coreAssembly.GetType("System.Int32", throwOnError: true)!; + Type stringType = coreAssembly.GetType("System.String", throwOnError: true)!; + + Assert.Null(Nullable.GetUnderlyingType(intType)); + Assert.Null(Nullable.GetUnderlyingType(stringType)); + + Assert.Null(intType.GetNullableUnderlyingType()); + Assert.Null(stringType.GetNullableUnderlyingType()); + } + + [Fact] + public static void GetNullableUnderlyingType_MetadataLoadContext_OpenNullable() + { + string coreAssemblyPath = TestUtils.GetPathToCoreAssembly(); + var resolver = new PathAssemblyResolver([coreAssemblyPath]); + using var mlc = new MetadataLoadContext(resolver, TestUtils.GetNameOfCoreAssembly()); + + Assembly coreAssembly = mlc.LoadFromAssemblyPath(coreAssemblyPath); + Type openNullableType = coreAssembly.GetType("System.Nullable`1", throwOnError: true)!; + + // Nullable.GetUnderlyingType returns null for generic type definitions (COMPAT). + Assert.Null(Nullable.GetUnderlyingType(openNullableType)); + + // Type.GetNullableUnderlyingType returns the generic type parameter T for Nullable<>. + Type? underlying = openNullableType.GetNullableUnderlyingType(); + Assert.NotNull(underlying); + Assert.Same(openNullableType.GetGenericArguments()[0], underlying); + } + } +} + +#endif // NET diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 1e205d855b5d0a..8c517b088c4d76 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -6623,6 +6623,7 @@ protected Type() { } public virtual string? GetEnumName(object value) { throw null; } public virtual string[] GetEnumNames() { throw null; } public virtual System.Type GetEnumUnderlyingType() { throw null; } + public virtual System.Type? GetNullableUnderlyingType() { throw null; } [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("It might not be possible to create an array of the enum type at runtime. Use Enum.GetValues or the GetEnumValuesAsUnderlyingType method instead.")] public virtual System.Array GetEnumValues() { throw null; } public virtual System.Array GetEnumValuesAsUnderlyingType() { throw null; } @@ -13277,6 +13278,7 @@ public TypeDelegator([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers public override System.Type? GetNestedType(string name, System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicNestedTypes | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicNestedTypes)] public override System.Type[] GetNestedTypes(System.Reflection.BindingFlags bindingAttr) { throw null; } + public override System.Type? GetNullableUnderlyingType() { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] public override System.Reflection.PropertyInfo[] GetProperties(System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs index 4f3efc786e4623..94cebad747f2f4 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs @@ -88,6 +88,33 @@ public static void GetUnderlyingType(Type nullableType, Type? expected) Assert.Equal(expected, Nullable.GetUnderlyingType(nullableType)); } + public static IEnumerable GetNullableUnderlyingType_RuntimeType_TestData() + { + yield return new object[] { typeof(int?), typeof(int) }; + yield return new object[] { typeof(int), null }; + yield return new object[] { typeof(G), null }; + // Nullable<> (generic type definition) is nullable; returns the generic type parameter T. + yield return new object[] { typeof(Nullable<>), typeof(Nullable<>).GetGenericArguments()[0] }; + } + + [Theory] + [MemberData(nameof(GetNullableUnderlyingType_RuntimeType_TestData))] + public static void GetNullableUnderlyingType_RuntimeType(Type type, Type? expected) + { + Assert.Equal(expected, type.GetNullableUnderlyingType()); + } + + [Fact] + public static void GetNullableUnderlyingType_NullableOverForeignGenericParameter() + { + // Nullable instantiated over the generic parameter of another type. + Type genericParam = typeof(GStruct<>).GetGenericArguments()[0]; + Type nullableOverParam = typeof(Nullable<>).MakeGenericType(genericParam); + + Assert.Same(genericParam, nullableOverParam.GetNullableUnderlyingType()); + Assert.Same(genericParam, Nullable.GetUnderlyingType(nullableOverParam)); + } + [Fact] public static void GetUnderlyingType_NullType_ThrowsArgumentNullException() { @@ -222,5 +249,7 @@ private struct MutatingStruct } public class G { } + + public struct GStruct where T : struct { } } } diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Reflection/SignatureTypes.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Reflection/SignatureTypes.cs index 870982a7949d05..ba8f8d5584eef7 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Reflection/SignatureTypes.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Reflection/SignatureTypes.cs @@ -934,5 +934,37 @@ private static void TestSignatureTypeInvariants(Type type) Assert.Throws(() => type.GenericParameterPosition); } } + + [Fact] + public static void GetNullableUnderlyingType_SignatureConstructedGenericType_Nullable_ReturnsTypeArgument() + { + Type sig = Type.MakeGenericSignatureType(typeof(Nullable<>), typeof(int)); + Assert.True(sig.IsSignatureType); + Assert.Equal(typeof(int), sig.GetNullableUnderlyingType()); + } + + [Fact] + public static void GetNullableUnderlyingType_SignatureConstructedGenericType_NonNullable_ReturnsNull() + { + Type sig = Type.MakeGenericSignatureType(typeof(List<>), typeof(int)); + Assert.True(sig.IsSignatureType); + Assert.Null(sig.GetNullableUnderlyingType()); + } + + [Fact] + public static void GetNullableUnderlyingType_SignatureModifiedType_Nullable_DelegatesToUnmodifiedType() + { + Type sig = Type.MakeModifiedSignatureType(typeof(int?), null, null); + Assert.True(sig.IsSignatureType); + Assert.Equal(typeof(int), sig.GetNullableUnderlyingType()); + } + + [Fact] + public static void GetNullableUnderlyingType_SignatureModifiedType_NonNullable_ReturnsNull() + { + Type sig = Type.MakeModifiedSignatureType(typeof(int), null, null); + Assert.True(sig.IsSignatureType); + Assert.Null(sig.GetNullableUnderlyingType()); + } } } diff --git a/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs b/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs index f442f500bcd5b4..5ab601dceb9809 100644 --- a/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs @@ -1396,6 +1396,18 @@ public override GenericParameterAttributes GenericParameterAttributes #region Generics + public override Type? GetNullableUnderlyingType() + { + if (IsGenericType) + { + Type genericType = GetGenericTypeDefinition(); + if (ReferenceEquals(genericType, typeof(Nullable<>))) + return GetGenericArguments()[0]; + } + + return null; + } + internal RuntimeType[] GetGenericArgumentsInternal() { RuntimeType[]? res = null; @@ -2466,7 +2478,7 @@ public override string? FullName public sealed override bool HasSameMetadataDefinitionAs(MemberInfo other) => HasSameMetadataDefinitionAsCore(other); - internal bool IsNullableOfT => Nullable.GetUnderlyingType(this) != null; + internal bool IsNullableOfT => GetNullableUnderlyingType() is not null; public override bool IsSZArray {