diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfoContext.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfoContext.cs index 203bafd4d66c7..c7e3ef85a9f58 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfoContext.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfoContext.cs @@ -347,19 +347,23 @@ private NullabilityInfo GetNullabilityInfo(MemberInfo memberInfo, Type type, ILi private NullabilityInfo GetNullabilityInfo(MemberInfo memberInfo, Type type, IList customAttributes, int index) { NullabilityState state = NullabilityState.Unknown; + NullabilityInfo? elementState = null; + NullabilityInfo[] genericArgumentsState = Array.Empty(); + Type? underlyingType = type; if (type.IsValueType) { - if (Nullable.GetUnderlyingType(type) != null) + underlyingType = Nullable.GetUnderlyingType(type); + + if (underlyingType != null) { state = NullabilityState.Nullable; } else { + underlyingType = type; state = NullabilityState.NotNull; } - - return new NullabilityInfo(type, state, state, null, Array.Empty()); } else { @@ -368,32 +372,36 @@ private NullabilityInfo GetNullabilityInfo(MemberInfo memberInfo, Type type, ILi state = GetNullableContext(memberInfo); } - NullabilityInfo? elementState = null; - NullabilityInfo[]? genericArgumentsState = null; - if (type.IsArray) { elementState = GetNullabilityInfo(memberInfo, type.GetElementType()!, customAttributes, index + 1); } - else if (type.IsGenericType) - { - Type[] genericArguments = type.GetGenericArguments(); - genericArgumentsState = new NullabilityInfo[genericArguments.Length]; + } - for (int i = 0; i < genericArguments.Length; i++) + if (underlyingType.IsGenericType) + { + Type[] genericArguments = underlyingType.GetGenericArguments(); + genericArgumentsState = new NullabilityInfo[genericArguments.Length]; + + for (int i = 0, offset = 0; i < genericArguments.Length; i++) + { + if (!genericArguments[i].IsValueType) { - genericArgumentsState[i] = GetNullabilityInfo(memberInfo, genericArguments[i], customAttributes, i + 1); + offset++; } - } - NullabilityInfo nullability = new NullabilityInfo(type, state, state, elementState, genericArgumentsState ?? Array.Empty()); - if (state != NullabilityState.Unknown) - { - TryLoadGenericMetaTypeNullability(memberInfo, nullability); + genericArgumentsState[i] = GetNullabilityInfo(memberInfo, genericArguments[i], customAttributes, offset); } + } - return nullability; + NullabilityInfo nullability = new NullabilityInfo(type, state, state, elementState, genericArgumentsState); + + if (!type.IsValueType && state != NullabilityState.Unknown) + { + TryLoadGenericMetaTypeNullability(memberInfo, nullability); } + + return nullability; } private static bool ParseNullableState(IList customAttributes, int index, ref NullabilityState state) diff --git a/src/libraries/System.Runtime/tests/System/Reflection/NullabilityInfoContextTests.cs b/src/libraries/System.Runtime/tests/System/Reflection/NullabilityInfoContextTests.cs index 1651b1817a4ab..a1ec3c648137c 100644 --- a/src/libraries/System.Runtime/tests/System/Reflection/NullabilityInfoContextTests.cs +++ b/src/libraries/System.Runtime/tests/System/Reflection/NullabilityInfoContextTests.cs @@ -480,8 +480,8 @@ public void GenericListTest() Assert.Equal(NullabilityState.Nullable, nullability.WriteState); Assert.Equal(typeof(string), nullability.Type); - Type lisNontNull = typeof(List); - MethodInfo addNotNull = lisNontNull.GetMethod("Add")!; + Type listNotNull = typeof(List); + MethodInfo addNotNull = listNotNull.GetMethod("Add")!; nullability = nullabilityContext.Create(addNotNull.GetParameters()[0]); Assert.Equal(NullabilityState.Nullable, nullability.ReadState); Assert.Equal(typeof(string), nullability.Type); @@ -812,6 +812,34 @@ public void RefReturnTestTest(string methodName, NullabilityState retReadState, Assert.Equal(paramReadState, paramNullability.ReadState); Assert.Equal(paramWriteState, paramNullability.WriteState); } + + public static IEnumerable ValueTupleTestData() + { + // public (int, string) UnknownValueTuple; [0] + yield return new object[] { "UnknownValueTuple", NullabilityState.NotNull, NullabilityState.Unknown, NullabilityState.NotNull }; + // public (string?, object) NullNonNonValueTuple; [0, 2, 1] + yield return new object[] { "Null_Non_Non_ValueTuple", NullabilityState.Nullable, NullabilityState.NotNull, NullabilityState.NotNull }; + // public (string?, object)? Null_Non_Null_ValueTuple; [0, 2, 1] + yield return new object[] { "Null_Non_Null_ValueTuple", NullabilityState.Nullable, NullabilityState.NotNull, NullabilityState.Nullable }; + // public (int, int?)? Non_Null_Null_ValueTuple; [0] + yield return new object[] { "Non_Null_Null_ValueTuple", NullabilityState.NotNull, NullabilityState.Nullable, NullabilityState.Nullable }; + // public (int, string) Non_Non_Non_ValueTuple; [0, 1] + yield return new object[] { "Non_Non_Non_ValueTuple", NullabilityState.NotNull, NullabilityState.NotNull, NullabilityState.NotNull }; + // public (int, string?) Non_Null_Non_ValueTuple; [0, 2] + yield return new object[] { "Non_Null_Non_ValueTuple", NullabilityState.NotNull, NullabilityState.Nullable, NullabilityState.NotNull }; + } + + [Theory] + [MemberData(nameof(ValueTupleTestData))] + [SkipOnMono("Nullability attributes trimmed on Mono")] + public void TestValueTupleGenericTypeParameters(string fieldName, NullabilityState param1, NullabilityState param2, NullabilityState fieldState) + { + var tupleInfo = nullabilityContext.Create(testType.GetField(fieldName)!); + + Assert.Equal(fieldState, tupleInfo.ReadState); + Assert.Equal(param1, tupleInfo.GenericTypeArguments[0].ReadState); + Assert.Equal(param2, tupleInfo.GenericTypeArguments[1].ReadState); + } } #pragma warning disable CS0649, CS0067, CS0414 @@ -865,6 +893,7 @@ public class TypeWithNotNullContext protected Tuple PropertyTupleUnknown { get; set; } protected internal IDictionary PropertyDictionaryUnknown { get; set; } + public (int, string) UnknownValueTuple; internal TypeWithNotNullContext FieldUnknown; public int FieldValueTypeUnknown; @@ -915,6 +944,11 @@ public void MethodParametersUnknown(string s, IDictionary dict) private IDictionary? PropertyDictionaryNonNonNonNull { get; set; } public IDictionary PropertyDictionaryNonNonNonNon { get; set; } = null!; + public (string?, object) Null_Non_Non_ValueTuple; + public (string?, object)? Null_Non_Null_ValueTuple; + public (int, int?)? Non_Null_Null_ValueTuple; + public (int, string) Non_Non_Non_ValueTuple; + public (int, string?) Non_Null_Non_ValueTuple; private const string? FieldNullable = null; protected static NullabilityInfoContextTests FieldNonNullable = null!; public static double FieldValueTypeNotNull; @@ -969,14 +1003,14 @@ public void MethodParametersUnknown(T s, IDictionary dict) { } public Tuple? PropertyTupleNullNullNullNull { get; set; } public Tuple PropertyTupleNonNullNonNon { get; set; } = null!; Tuple? PropertyTupleNullNonNullNull { get; set; } - public Tuple? PropertyTupleNonNullNonNull { get; set; } - public Tuple PropertyTupleNonNonNonNon { get; set; } = null!; + public Tuple? PropertyTupleNonNullNonNull { get; set; } + public Tuple PropertyTupleNonNonNonNon { get; set; } = null!; private IDictionary PropertyDictionaryNullNullNullNon { get; set; } = null!; static IDictionary? PropertyDictionaryNonNullNonNull { get; set; } public static IDictionary? PropertyDictionaryNullNonNonNull { get; set; } public IDictionary PropertyDictionaryNonNullNonNon { get; set; } = null!; protected IDictionary? PropertyDictionaryNonNonNonNull { get; set; } - public IDictionary PropertyDictionaryNonNonNonNon { get; set; } = null!; + public IDictionary PropertyDictionaryNonNonNonNon { get; set; } = null!; static T? FieldNullable = default; public T FieldNonNullable = default!;