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 57727d1b0dc3e3..67590653f595ac 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfoContext.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfoContext.cs @@ -141,7 +141,7 @@ private static void CheckNullabilityAttributes(NullabilityInfo nullability, ILis else if ((attribute.AttributeType.Name == "MaybeNullAttribute" || attribute.AttributeType.Name == "MaybeNullWhenAttribute") && codeAnalysisReadState == NullabilityState.Unknown && - !nullability.Type.IsValueType) + !IsValueTypeOrValueTypeByRef(nullability.Type)) { codeAnalysisReadState = NullabilityState.Nullable; } @@ -151,7 +151,7 @@ private static void CheckNullabilityAttributes(NullabilityInfo nullability, ILis } else if (attribute.AttributeType.Name == "AllowNullAttribute" && codeAnalysisWriteState == NullabilityState.Unknown && - !nullability.Type.IsValueType) + !IsValueTypeOrValueTypeByRef(nullability.Type)) { codeAnalysisWriteState = NullabilityState.Nullable; } @@ -327,7 +327,7 @@ private NullabilityInfo GetNullabilityInfo(MemberInfo memberInfo, Type type, Nul int index = 0; NullabilityInfo nullability = GetNullabilityInfo(memberInfo, type, parser, ref index); - if (!type.IsValueType && nullability.ReadState != NullabilityState.Unknown) + if (nullability.ReadState != NullabilityState.Unknown) { TryLoadGenericMetaTypeNullability(memberInfo, nullability); } @@ -340,19 +340,22 @@ private NullabilityInfo GetNullabilityInfo(MemberInfo memberInfo, Type type, Nul NullabilityState state = NullabilityState.Unknown; NullabilityInfo? elementState = null; NullabilityInfo[] genericArgumentsState = Array.Empty(); - Type? underlyingType = type; + Type underlyingType = type; - if (type.IsValueType) + if (underlyingType.IsByRef || underlyingType.IsPointer) { - underlyingType = Nullable.GetUnderlyingType(type); + underlyingType = underlyingType.GetElementType()!; + } - if (underlyingType != null) + if (underlyingType.IsValueType) + { + if (Nullable.GetUnderlyingType(underlyingType) is { } nullableUnderlyingType) { + underlyingType = nullableUnderlyingType; state = NullabilityState.Nullable; } else { - underlyingType = type; state = NullabilityState.NotNull; } @@ -369,9 +372,9 @@ private NullabilityInfo GetNullabilityInfo(MemberInfo memberInfo, Type type, Nul state = contextState; } - if (type.IsArray) + if (underlyingType.IsArray) { - elementState = GetNullabilityInfo(memberInfo, type.GetElementType()!, parser, ref index); + elementState = GetNullabilityInfo(memberInfo, underlyingType.GetElementType()!, parser, ref index); } } @@ -468,6 +471,12 @@ private void CheckGenericParameters(NullabilityInfo nullability, MemberInfo meta { CheckGenericParameters(elementNullability, metaMember, metaType.GetElementType()!, reflectedType); } + // We could also follow this branch for metaType.IsPointer, but since pointers must be unmanaged this + // will be a no-op regardless + else if (metaType.IsByRef) + { + CheckGenericParameters(nullability, metaMember, metaType.GetElementType()!, reflectedType); + } } } @@ -482,6 +491,11 @@ private bool TryUpdateGenericParameterNullability(NullabilityInfo nullability, T return true; } + if (IsValueTypeOrValueTypeByRef(nullability.Type)) + { + return true; + } + var state = NullabilityState.Unknown; if (CreateParser(genericParameter.GetCustomAttributesData()).ParseNullableState(0, ref state)) { @@ -549,9 +563,10 @@ static int CountNullabilityStates(Type type) } return count; } - if (underlyingType.IsArray) + + if (underlyingType.HasElementType) { - return 1 + CountNullabilityStates(underlyingType.GetElementType()!); + return (underlyingType.IsArray ? 1 : 0) + CountNullabilityStates(underlyingType.GetElementType()!); } return type.IsValueType ? 0 : 1; @@ -560,7 +575,7 @@ static int CountNullabilityStates(Type type) private bool TryPopulateNullabilityInfo(NullabilityInfo nullability, NullableAttributeStateParser parser, ref int index) { - bool isValueType = nullability.Type.IsValueType; + bool isValueType = IsValueTypeOrValueTypeByRef(nullability.Type); if (!isValueType) { var state = NullabilityState.Unknown; @@ -606,6 +621,9 @@ private static NullabilityState TranslateByte(byte b) => _ => NullabilityState.Unknown }; + private static bool IsValueTypeOrValueTypeByRef(Type type) => + type.IsValueType || ((type.IsByRef || type.IsPointer) && type.GetElementType()!.IsValueType); + private readonly struct NullableAttributeStateParser { private static readonly object UnknownByte = (byte)0; diff --git a/src/libraries/System.Runtime/tests/System/Reflection/NullabilityInfoContextTests.cs b/src/libraries/System.Runtime/tests/System/Reflection/NullabilityInfoContextTests.cs index 89f2fe49c9c499..aa0e9c0d464939 100644 --- a/src/libraries/System.Runtime/tests/System/Reflection/NullabilityInfoContextTests.cs +++ b/src/libraries/System.Runtime/tests/System/Reflection/NullabilityInfoContextTests.cs @@ -670,6 +670,139 @@ public void MethodParametersTest(string methodName, NullabilityState stringState Assert.Null(dictionaryNullability.ElementType); } + public static IEnumerable MethodByRefParametersTestData() => new[] + { + new object[] { -1, NullabilityState.Nullable }, // ref readonly int? + new object[] { 0, NullabilityState.Nullable }, // out int? a + new object[] { 1, NullabilityState.NotNull }, // out string b + new object[] { 2, NullabilityState.Nullable }, // ref object? c + new object[] { 3, NullabilityState.NotNull }, // ref Type d + new object[] { 4, NullabilityState.Nullable }, // in decimal? e + new object[] { 5, NullabilityState.NotNull }, // in ICloneable f + }; + + [Theory] + [SkipOnMono("Nullability attributes trimmed on Mono")] + [MemberData(nameof(MethodByRefParametersTestData))] + public void MethodByRefParametersTest(int parameterIndex, NullabilityState state) + { + MethodInfo method = typeof(TypeWithNotNullContext).GetMethod(nameof(TypeWithNotNullContext.MethodWithByRefs))!; + ParameterInfo parameter = parameterIndex == -1 ? method.ReturnParameter : method.GetParameters()[parameterIndex]; + NullabilityInfo info = nullabilityContext.Create(parameter); + + Assert.Equal(parameter.ParameterType, info.Type); + Assert.Equal(state, info.ReadState); + Assert.Equal(state, info.WriteState); + } + + public static IEnumerable MethodGenericByRefParametersTestData() => new[] +{ + // ref Tuple + new object?[] { -1, NullabilityState.NotNull, NullabilityState.Nullable, NullabilityState.NotNull, NullabilityState.Nullable }, + // out KeyValuePair a + new object?[] { 0, NullabilityState.NotNull, NullabilityState.Nullable, NullabilityState.NotNull, null }, + // ref Tuple? b + new object?[] { 1, NullabilityState.Nullable, NullabilityState.Nullable, NullabilityState.NotNull, null }, + // in KeyValuePair? c + new object?[] { 2, NullabilityState.Nullable, NullabilityState.NotNull, NullabilityState.Nullable, null }, + }; + + [Theory] + [SkipOnMono("Nullability attributes trimmed on Mono")] + [MemberData(nameof(MethodGenericByRefParametersTestData))] + public void MethodGenericByRefParametersTest( + int parameterIndex, + NullabilityState state, + NullabilityState genericArgument0State, + NullabilityState genericArgument1State, + NullabilityState? genericArgument2State) + { + MethodInfo method = typeof(TypeWithNotNullContext).GetMethod(nameof(TypeWithNotNullContext.MethodWithGenericByRefs))!; + ParameterInfo parameter = parameterIndex == -1 ? method.ReturnParameter : method.GetParameters()[parameterIndex]; + NullabilityInfo info = nullabilityContext.Create(parameter); + Assert.Equal(parameter.ParameterType, info.Type); + Assert.Equal(state, info.ReadState); + Assert.Equal(state, info.WriteState); + + NullabilityState?[] genericArgumentStates = new[] { genericArgument0State, genericArgument1State, genericArgument2State }; + for (var i = 0; i < genericArgumentStates.Length; ++i) + { + if (genericArgumentStates[i] is null) + { + Assert.Equal(i, info.GenericTypeArguments.Length); + break; + } + + Assert.Equal(genericArgumentStates[i], info.GenericTypeArguments[i].ReadState); + Assert.Equal(genericArgumentStates[i], info.GenericTypeArguments[i].WriteState); + } + } + + [Fact] + [SkipOnMono("Nullability attributes trimmed on Mono")] + public void ConstrainedGenericMethodWithByRefTest() + { + // ref T ConstrainedGenericMethodWithByRef(out T[] array) where T : class? + MethodInfo method = typeof(TypeWithNotNullContext).GetMethod(nameof(TypeWithNotNullContext.ConstrainedGenericMethodWithByRef))! + .MakeGenericMethod(typeof(string)); + + NullabilityInfo info = nullabilityContext.Create(method.ReturnParameter); + Assert.Equal(method.ReturnType, info.Type); + Assert.Equal(NullabilityState.Nullable, info.ReadState); + Assert.Equal(NullabilityState.Nullable, info.WriteState); + + info = nullabilityContext.Create(method.GetParameters()[0]); + Assert.Equal(typeof(string[]).MakeByRefType(), info.Type); + Assert.Equal(NullabilityState.NotNull, info.ReadState); + Assert.Equal(NullabilityState.NotNull, info.WriteState); + Assert.Equal(typeof(string), info.ElementType!.Type); + Assert.Equal(NullabilityState.Nullable, info.ElementType.ReadState); + Assert.Equal(NullabilityState.Nullable, info.ElementType.WriteState); + } + + [Fact] + [SkipOnMono("Nullability attributes trimmed on Mono")] + public void MethodWithPointersTest() + { + // MethodWithPointers(int* a, int?* b) + MethodInfo method = typeof(TypeWithNotNullContext).GetMethod(nameof(TypeWithNotNullContext.MethodWithPointers))!; + ParameterInfo[] parameters = method.GetParameters(); + + NullabilityInfo info = nullabilityContext.Create(parameters[0]); + Assert.Equal(parameters[0].ParameterType, info.Type); + Assert.Equal(NullabilityState.NotNull, info.ReadState); + Assert.Equal(NullabilityState.NotNull, info.WriteState); + + info = nullabilityContext.Create(parameters[1]); + Assert.Equal(parameters[1].ParameterType, info.Type); + Assert.Equal(NullabilityState.Nullable, info.ReadState); + Assert.Equal(NullabilityState.Nullable, info.WriteState); + } + + [Fact] + [SkipOnMono("Nullability attributes trimmed on Mono")] + public unsafe void GenericMethodWithPointersTest() + { + // Dummy call to MakeGenericMethod to make the code generated ahead of time + if (string.Empty.Length > 0) + new TypeWithNotNullContext().GenericMethodWithPointers(null, null); + + // GenericMethodWithPointers(T* a, T?* b) + MethodInfo method = typeof(TypeWithNotNullContext).GetMethod(nameof(TypeWithNotNullContext.GenericMethodWithPointers))! + .MakeGenericMethod(typeof(float)); + ParameterInfo[] parameters = method.GetParameters(); + + NullabilityInfo info = nullabilityContext.Create(parameters[0]); + Assert.Equal(parameters[0].ParameterType, info.Type); + Assert.Equal(NullabilityState.NotNull, info.ReadState); + Assert.Equal(NullabilityState.NotNull, info.WriteState); + + info = nullabilityContext.Create(parameters[1]); + Assert.Equal(parameters[1].ParameterType, info.Type); + Assert.Equal(NullabilityState.Nullable, info.ReadState); + Assert.Equal(NullabilityState.Nullable, info.WriteState); + } + public static IEnumerable MethodGenericParametersTestData() { yield return new object[] { "MethodParametersUnknown", NullabilityState.Unknown, NullabilityState.Unknown, NullabilityState.Unknown, NullabilityState.Unknown }; @@ -692,6 +825,34 @@ public void MethodGenericParametersTest(string methodName, NullabilityState para Assert.Equal(dictValue, dictionaryNullability.GenericTypeArguments[1].ReadState); } + [Fact] + [SkipOnMono("Nullability attributes trimmed on Mono")] + public void PropertyWithByRefGenericParametersTest() + { + // ref T? this[in T a, in List b] { get; } + PropertyInfo property = typeof(GenericTest).GetProperty("Item", typeof(string).MakeByRefType())!; + + NullabilityInfo info = nullabilityContext.Create(property); + Assert.Equal(typeof(string).MakeByRefType(), info.Type); + Assert.Equal(NullabilityState.Nullable, info.ReadState); + Assert.Equal(NullabilityState.Unknown, info.WriteState); + + info = nullabilityContext.Create(property.GetIndexParameters()[0]); + Assert.Equal(typeof(string).MakeByRefType(), info.Type); + // in T a is nullable because T is not constrained + Assert.Equal(NullabilityState.Nullable, info.ReadState); + Assert.Equal(NullabilityState.Nullable, info.WriteState); + + info = nullabilityContext.Create(property.GetIndexParameters()[1]); + Assert.Equal(typeof(List).MakeByRefType(), info.Type); + Assert.Equal(NullabilityState.NotNull, info.ReadState); + Assert.Equal(NullabilityState.NotNull, info.WriteState); + info = Assert.Single(info.GenericTypeArguments); + Assert.Equal(typeof(string), info.Type); + Assert.Equal(NullabilityState.Nullable, info.ReadState); + Assert.Equal(NullabilityState.Nullable, info.WriteState); + } + public static IEnumerable StringTypeTestData() { yield return new object[] { "Format", NullabilityState.NotNull, NullabilityState.Nullable, NullabilityState.Nullable, new Type[] { typeof(string), typeof(object), typeof(object) } }; @@ -946,7 +1107,7 @@ public static IEnumerable RefReturnData() yield return new object[] { "RefReturnNotNullable", NullabilityState.NotNull, NullabilityState.NotNull, NullabilityState.Nullable, NullabilityState.NotNull }; // [return: NotNull]public ref string? RefReturnNotNull([NotNull] ref string? id) yield return new object[] { "RefReturnNotNull", NullabilityState.NotNull, NullabilityState.Nullable, NullabilityState.NotNull, NullabilityState.Nullable }; - // publiic ref string? RefReturnNullable([AllowNull] ref string id) + // public ref string? RefReturnNullable([AllowNull] ref string id) yield return new object[] { "RefReturnNullable", NullabilityState.Nullable, NullabilityState.Nullable, NullabilityState.NotNull, NullabilityState.Nullable }; } @@ -1090,12 +1251,18 @@ public void TestNestedGenericInheritanceWithMultipleParameters() Assert.Equal(NullabilityState.NotNull, item3Info.ElementType.WriteState); } - [Fact] - [SkipOnMono("Nullability attributes trimmed on Mono")] - public void TestNullabilityInfoCreationOnPropertiesWithNestedGenericTypeArguments() + public static IEnumerable TestNullabilityInfoCreationOnPropertiesWithNestedGenericTypeArgumentsData() => new[] { - Type type = typeof(TypeWithPropertiesNestingItsGenericTypeArgument); + new object[] { typeof(TypeWithPropertiesNestingItsGenericTypeArgument), NullabilityState.NotNull }, + new object[] { typeof(TypeWithPropertiesNestingItsGenericTypeArgument), NullabilityState.Nullable }, + new object[] { typeof(TypeWithPropertiesNestingItsGenericTypeArgument), NullabilityState.Nullable }, + }; + [Theory] + [SkipOnMono("Nullability attributes trimmed on Mono")] + [MemberData(nameof(TestNullabilityInfoCreationOnPropertiesWithNestedGenericTypeArgumentsData))] + public void TestNullabilityInfoCreationOnPropertiesWithNestedGenericTypeArguments(Type type, NullabilityState expectedGenericArgumentNullability) + { NullabilityInfo shallow1Info = nullabilityContext.Create(type.GetProperty("Shallow1")!); NullabilityInfo deep1Info = nullabilityContext.Create(type.GetProperty("Deep1")!); NullabilityInfo deep2Info = nullabilityContext.Create(type.GetProperty("Deep2")!); @@ -1103,51 +1270,51 @@ public void TestNullabilityInfoCreationOnPropertiesWithNestedGenericTypeArgument NullabilityInfo deep4Info = nullabilityContext.Create(type.GetProperty("Deep4")!); NullabilityInfo deep5Info = nullabilityContext.Create(type.GetProperty("Deep5")!); - //public Tuple? Shallow1 { get; set; } + // public Tuple? Shallow1 { get; set; } NullabilityInfo info = shallow1Info; Assert.Equal(1, info.GenericTypeArguments.Length); - Assert.Equal(NullabilityState.Nullable, info.GenericTypeArguments[0].ReadState); + Assert.Equal(expectedGenericArgumentNullability, info.GenericTypeArguments[0].ReadState); - //public Tuple>? Deep1 { get; set; } + // public Tuple>? Deep1 { get; set; } info = deep1Info; Assert.Equal(1, info.GenericTypeArguments.Length); Assert.Equal(1, info.GenericTypeArguments[0].GenericTypeArguments.Length); Assert.Equal(NullabilityState.NotNull, info.GenericTypeArguments[0].ReadState); - Assert.Equal(NullabilityState.Nullable, info.GenericTypeArguments[0].GenericTypeArguments[0].ReadState); + Assert.Equal(expectedGenericArgumentNullability, info.GenericTypeArguments[0].GenericTypeArguments[0].ReadState); - //public Tuple, int>? Deep2 { get; set; } + // public Tuple, int>? Deep2 { get; set; } info = deep2Info; Assert.Equal(2, info.GenericTypeArguments.Length); Assert.Equal(1, info.GenericTypeArguments[0].GenericTypeArguments.Length); Assert.Equal(NullabilityState.NotNull, info.GenericTypeArguments[0].ReadState); Assert.Equal(NullabilityState.NotNull, info.GenericTypeArguments[1].ReadState); - Assert.Equal(NullabilityState.Nullable, info.GenericTypeArguments[0].GenericTypeArguments[0].ReadState); + Assert.Equal(expectedGenericArgumentNullability, info.GenericTypeArguments[0].GenericTypeArguments[0].ReadState); - //public Tuple>? Deep3 { get; set; } + // public Tuple>? Deep3 { get; set; } info = deep3Info; Assert.Equal(2, info.GenericTypeArguments.Length); Assert.Equal(1, info.GenericTypeArguments[1].GenericTypeArguments.Length); Assert.Equal(NullabilityState.Nullable, info.GenericTypeArguments[0].ReadState); Assert.Equal(NullabilityState.NotNull, info.GenericTypeArguments[1].ReadState); - Assert.Equal(NullabilityState.Nullable, info.GenericTypeArguments[1].GenericTypeArguments[0].ReadState); + Assert.Equal(expectedGenericArgumentNullability, info.GenericTypeArguments[1].GenericTypeArguments[0].ReadState); - //public Tuple>? Deep4 { get; set; } + // public Tuple>? Deep4 { get; set; } info = deep4Info; Assert.Equal(3, info.GenericTypeArguments.Length); Assert.Equal(1, info.GenericTypeArguments[2].GenericTypeArguments.Length); Assert.Equal(NullabilityState.NotNull, info.GenericTypeArguments[0].ReadState); Assert.Equal(NullabilityState.Nullable, info.GenericTypeArguments[1].ReadState); Assert.Equal(NullabilityState.NotNull, info.GenericTypeArguments[2].ReadState); - Assert.Equal(NullabilityState.Nullable, info.GenericTypeArguments[2].GenericTypeArguments[0].ReadState); + Assert.Equal(expectedGenericArgumentNullability, info.GenericTypeArguments[2].GenericTypeArguments[0].ReadState); - //public Tuple?>? Deep5 { get; set; } + // public Tuple?>? Deep5 { get; set; } info = deep5Info; Assert.Equal(3, info.GenericTypeArguments.Length); Assert.Equal(2, info.GenericTypeArguments[2].GenericTypeArguments.Length); Assert.Equal(NullabilityState.NotNull, info.GenericTypeArguments[0].ReadState); Assert.Equal(NullabilityState.NotNull, info.GenericTypeArguments[1].ReadState); Assert.Equal(NullabilityState.Nullable, info.GenericTypeArguments[2].ReadState); - Assert.Equal(NullabilityState.Nullable, info.GenericTypeArguments[2].GenericTypeArguments[0].ReadState); + Assert.Equal(expectedGenericArgumentNullability, info.GenericTypeArguments[2].GenericTypeArguments[0].ReadState); Assert.Equal(NullabilityState.NotNull, info.GenericTypeArguments[2].GenericTypeArguments[1].ReadState); } } @@ -1298,6 +1465,17 @@ public void MethodNullNonNullNonNon(string? s, IDictionary dict public void MethodNonNullNonNullNotNull(string s, [NotNull] IDictionary? dict) { dict = new Dictionary(); } public void MethodNullNonNullNullNon(string? s, IDictionary dict) { } public void MethodAllowNullNonNonNonNull([AllowNull] string s, IDictionary? dict) { } + + public ref readonly int? MethodWithByRefs(out int? a, out string b, ref object? c, ref Type d, in decimal? e, in ICloneable f) => + throw new NotImplementedException(); + public ref Tuple MethodWithGenericByRefs( + out KeyValuePair a, + ref Tuple? b, + in KeyValuePair? c) => + throw new NotImplementedException(); + public ref T ConstrainedGenericMethodWithByRef(out T[] array) where T : class? => throw new NotImplementedException(); + public unsafe void MethodWithPointers(int* a, int?* b) { } + public unsafe void GenericMethodWithPointers(T* a, T?* b) where T : unmanaged { } } public struct GenericStruct { } @@ -1353,6 +1531,8 @@ public void MethodParametersUnknown(T s, IDictionary dict) { } public List? MethodNullListNonNullGeneric() => null; public void MethodArgsNullGenericNullDictValueGeneric(T? s, IDictionary? dict) { } public void MethodArgsGenericDictValueNullGeneric(T s, IDictionary dict) { } + + public ref T? this[in T a, in List b] => throw new NotImplementedException(); } internal class GenericTestConstrainedNotNull where T : notnull