[Generator] Add support for nullability. Part 1. (#17384)

This is the first part of a 2 or more changes that will allow the APIs
to use ? as a way to mark a nullable field, parameter, property and
return method.

The way it works follows the documentation from the runtime that can be
found here:
https://github.com/dotnet/roslyn/blob/main/docs/features/nullable-metadata.md

The tldr; of the documentation is as follows:

1. If we are looking at a value type, the compiler converts int? into
Nullable<int>. This was already considered in the old code.
2. If we are inspecting a generic type we have to look for the
information in two different attributes added by the compiler: -
NullableAttribute: Each type reference in metadata may have an
associated NullableAttribute with a byte[] where each byte represents
nullability: 0 for oblivious, 1 for not annotated, and 2 for annotated.
- NullableContextAttribute is valid in metadata on type and method
declarations. The byte value represents the implicit NullableAttribute
value for type reference within that scope that do not have an explicit
NullableAttribute and would not otherwise be represented by an empty
byte[].

The API is very similar to that provided by dotnet6 but it allows us to
support classic. The move to the dotnet6 API should not be hard and
probably can be done by an IDE.

Once this API lands we will have to find the old usages of the check for
nullability and combine both. This new API should fix several issues we
have with nullability and nullability + generics.

---------

Co-authored-by: GitHub Actions Autoformatter <github-actions-autoformatter@xamarin.com>
This commit is contained in:
Manuel de la Pena 2023-01-30 10:34:24 -05:00 коммит произвёл GitHub
Родитель 00490d9da2
Коммит c201466d78
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
6 изменённых файлов: 554 добавлений и 0 удалений

Просмотреть файл

@ -60,6 +60,9 @@
<Compile Include="$(RepositoryPath)\tools\common\Execution.cs">
<Link>Execution.cs</Link>
</Compile>
<Compile Include="..\generator-nullability-info-context.cs">
<Link>src\generator-nullability-info-context.cs</Link>
</Compile>
<Compile Include="..\Resources.Designer.cs">
<DependentUpon>..\Resources.resx</DependentUpon>
</Compile>

Просмотреть файл

@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
#nullable enable
namespace bgen {
// extension shared between NET and !NET make our lives a little easier
public static class NullabilityInfoExtensions {
public static bool IsNullable (this NullabilityInfo self)
=> self.ReadState == NullabilityState.Nullable;
}
#if !NET
// from: https://github.com/dotnet/roslyn/blob/main/docs/features/nullable-metadata.md
public enum NullabilityState : byte {
Unknown = 0,
NotNull = 1,
Nullable = 2,
}
public class NullabilityInfo {
public NullabilityState ReadState { get; set; } = NullabilityState.NotNull;
public Type? Type { get; set; }
public NullabilityInfo? ElementType { get; set; }
public NullabilityInfo []? GenericTypeArguments { get; set; }
}
// helper class that allows to check the custom attrs in a method,property or field
// in order for the generator to support ? in the bindings definition. This class should be
// replaced by NullabilityInfoContext from the dotnet6 in the future. The API can be maintained (hopefully).
public class NullabilityInfoContext {
static readonly string NullableAttributeName = "System.Runtime.CompilerServices.NullableAttribute";
static readonly string NullableContextAttributeName = "System.Runtime.CompilerServices.NullableContextAttribute";
public NullabilityInfoContext () { }
public NullabilityInfo Create (PropertyInfo propertyInfo)
=> Create (propertyInfo.PropertyType, propertyInfo.DeclaringType, propertyInfo.CustomAttributes);
public NullabilityInfo Create (FieldInfo fieldInfo)
=> Create (fieldInfo.FieldType, fieldInfo.DeclaringType, fieldInfo.CustomAttributes);
public NullabilityInfo Create (ParameterInfo parameterInfo)
=> Create (parameterInfo.ParameterType, parameterInfo.Member, parameterInfo.CustomAttributes);
static NullabilityInfo Create (Type memberType, MemberInfo? declaringType,
IEnumerable<CustomAttributeData>? customAttributes, int depth = 0)
{
var info = new NullabilityInfo { Type = memberType };
if (memberType.IsByRef) {
var e = memberType.GetElementType ();
info = Create (e, declaringType, customAttributes);
// override the returned type because it is returning e and not ref e
info.Type = memberType;
return info;
}
if (memberType.IsArray) {
info.ElementType = Create (memberType.GetElementType ()!, declaringType, customAttributes, depth + 1);
}
if (memberType.IsGenericType) {
// we need to get the nullability type of each of the generics, we use an array to make sure
// order is kept
var genericArguments = memberType.GetGenericArguments ();
info.GenericTypeArguments = new NullabilityInfo [genericArguments.Length];
for (int i = 0; i < genericArguments.Length; i++) {
info.GenericTypeArguments [i] = Create (genericArguments [i], declaringType,
customAttributes, depth + (i + 1)); // the depth can be complicated
}
}
// there are two possible cases we have to take care of:
//
// 1. ValueType: If we are dealing with a value type the compiler converts it to Nullable<ValueType>
// 2. ReferenceType: Ret types do not use an interface, but a custom attr is used in the signature
if (memberType.IsValueType) {
var nullableType = Nullable.GetUnderlyingType (memberType);
info.ReadState = nullableType != null ? NullabilityState.Nullable : NullabilityState.NotNull;
info.Type = memberType;
return info;
}
// at this point, we have to use the attributes (metadata) added by the compiler to decide if we have a
// nullable type or not from a ref type. From https://github.com/dotnet/roslyn/blob/main/docs/features/nullable-metadata.md
//
// The byte[] is constructed as follows:
// Reference type: the nullability (0, 1, or 2), followed by the representation of the type arguments in order including containing types
// Nullable value type: the representation of the type argument only
// Non-generic value type: skipped
// Generic value type: 0, followed by the representation of the type arguments in order including containing types
// Array: the nullability (0, 1, or 2), followed by the representation of the element type
// Tuple: the representation of the underlying constructed type
// Type parameter reference: the nullability (0, 1, or 2, with 0 for unconstrained type parameter)
// interesting case when we have constrains from a generic method
if ((customAttributes?.Count () ?? 0) == 0 && memberType.CustomAttributes is not null)
customAttributes = memberType.CustomAttributes;
var nullable = customAttributes?.FirstOrDefault (
x => x.AttributeType.FullName == NullableAttributeName);
var flag = NullabilityState.Unknown;
if (nullable is not null && nullable.ConstructorArguments.Count == 1) {
var attributeArgument = nullable.ConstructorArguments [0];
if (attributeArgument.ArgumentType == typeof (byte [])) {
var args = (ReadOnlyCollection<CustomAttributeTypedArgument>) attributeArgument.Value!;
if (args.Count > 0 && args [depth].ArgumentType == typeof (byte)) {
flag = (NullabilityState) args [depth].Value;
}
} else if (attributeArgument.ArgumentType == typeof (byte)) {
flag = (NullabilityState) attributeArgument.Value!;
}
info.Type = memberType;
info.ReadState = flag;
return info;
}
// we are using the context of the declaring type to decide if the type is nullable
for (var type = declaringType; type is not null; type = type.DeclaringType) {
var context = type.CustomAttributes
.FirstOrDefault (x => x.AttributeType.FullName == NullableContextAttributeName);
if (context is null ||
context.ConstructorArguments.Count != 1 ||
context.ConstructorArguments [0].ArgumentType != typeof (byte))
continue;
if (NullabilityState.Nullable == (NullabilityState) context.ConstructorArguments [0].Value!) {
info.Type = memberType;
info.ReadState = NullabilityState.Nullable;
return info;
}
}
// we need to consider the generic constrains
if (!memberType.IsGenericParameter)
return info;
// if we do have a custom null atr in any of them, use it
if (memberType.GenericParameterAttributes.HasFlag (GenericParameterAttributes.NotNullableValueTypeConstraint))
info.ReadState = NullabilityState.NotNull;
return info;
}
}
#endif
}

Просмотреть файл

@ -128,6 +128,7 @@
<Compile Include="..\tools\common\Execution.cs">
<Link>Execution.cs</Link>
</Compile>
<Compile Include="generator-nullability-info-context.cs" />
<Compile Include="Resources.Designer.cs">
<DependentUpon>Resources.resx</DependentUpon>
</Compile>

Просмотреть файл

@ -43,6 +43,9 @@
<Compile Include="..\common\Profile.cs">
<Link>Profile.cs</Link>
</Compile>
<Compile Include="..\generator\NullabilityContextTests.cs">
<Link>NullabilityContextTests.cs</Link>
</Compile>
<Compile Include="..\mtouch\Cache.cs">
<Link>Cache.cs</Link>
</Compile>

Просмотреть файл

@ -0,0 +1,390 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using bgen;
using NUnit.Framework;
#nullable enable
namespace GeneratorTests {
[TestFixture]
[Parallelizable (ParallelScope.None)] // dotnet implementation is not thread safe..
public class NullabilityContextTests {
interface IInterfaceTest { }
class ObjectTest : IInterfaceTest {
public int NotNullableValueTypeField = 0;
public string NotNullableRefTypeField = string.Empty;
public int? NullableValueTypeField = 0;
public string? NullableRefTypeField = string.Empty;
public int NotNullableValueTypeProperty { get; set; }
public string NotNullableRefProperty { get; set; } = string.Empty;
public int? NullableValueTypeProperty { get; set; }
public string? NullableRefProperty { get; set; }
public int? this [int i] {
get => i;
set { }
}
public string? this [string? i] {
get => i;
set { }
}
public string this [double? i] {
get => string.Empty;
set { }
}
public void NotNullableValueTypeParameter (int myParam) { }
public void NotNullableOutValueTypeParameter (out int myParam) { myParam = 1; }
public void NotNullableRefValueTypeParameter (ref int myParam) { myParam = 1; }
public void NotNullableRefParameter (string myParam) { }
public void NotNullableOutRefParameter (out string myParam) { myParam = string.Empty; }
public void NotNullableRefRefParameter (ref string myParam) { myParam = string.Empty; }
public void NullableValueTypeParameter (int? myParam) { }
public void NullableOutValueTypeParameter (out int? myParam) { myParam = null; }
public void NullableRefValueTypeParameter (ref int? myParam) { myParam = null; }
public void NullableRefTypeParameter (string? myParam) { }
public void NullableOutRefTypeParameter (out string? myParam) { myParam = null; }
public void NullableRefRefTypeParameter (out string? myParam) { myParam = null; }
public int NotNullableValueTypeReturn () => 0;
public string NotNullableRefTypeReturn () => string.Empty;
public int? NullableValueTypeReturn () => null;
public string? NullableRefTypeReturn () => null;
// array return type tests
public int [] NotNullableValueTypeArray () => Array.Empty<int> ();
public int? [] NotNullableNullableValueTypeArray () => Array.Empty<int?> ();
public int []? NullableValueTypeArray () => null;
public int? []? NullableNullableValueTypeArray () => null;
public string [] NotNullableRefTypeArray () => Array.Empty<string> ();
public string? [] NotNullableNullableRefTypeArray () => Array.Empty<string?> ();
public string []? NullableRefTypeArray () => null;
public string? []? NullableNullableRefTypeArray () => null;
public int [] [] NotNullableNestedArrayNotNullableValueType () => Array.Empty<int []> ();
public int? [] [] NotNullableNestedArrayNullableValueType () => Array.Empty<int? []> ();
public int? []? [] NotNullableNestedNullableArrayNullableValueType () => Array.Empty<int? []?> ();
public int? []? []? NullableNestedNullableArrayNullableValueType () => null;
public string [] [] NotNullableNestedArrayNotNullableRefType () => Array.Empty<string []> ();
public string? [] [] NotNullableNestedArrayNullableRefType () => Array.Empty<string? []> ();
public string? []? [] NotNullableNestedNullableArrayNullableRefType () => Array.Empty<string? []?> ();
public string? []? []? NullableNestedNullableArrayNullableRefType () => null;
public List<int> NotNullableValueTypeList () => new ();
public List<int?> NullableValueTypeList () => new ();
public List<int?>? NullableListNullableValueType () => null;
public List<string> NotNullableRefTypeList () => new ();
public List<string?> NullableRefTypeList () => new ();
public List<string?>? NullableListNullableRefType () => null;
public List<HashSet<int>> NestedGenericNotNullableValueType () => new ();
public List<HashSet<int?>> NestedGenericNullableValueType () => new ();
public List<HashSet<int?>?> NestedNullableGenericNullableValueType () => new ();
public List<HashSet<int?>?>? NullableNestedNullableGenericNullableValueType () => new ();
public List<HashSet<string>> NestedGenericNotNullableRefType () => new ();
public List<HashSet<string?>> NestedGenericNullableRefType () => new ();
public List<HashSet<string?>?> NestedNullableGenericNullableRefType () => new ();
public List<HashSet<string?>?>? NullableNestedNullableGenericNullableRefType () => new ();
public Dictionary<string, int> DictionaryStringInt () => new ();
public Dictionary<string, int?> DictionaryStringNullableInt () => new ();
public Dictionary<string?, int?> DictionaryNullableStringNullableInt () => new ();
public Dictionary<string?, int?>? NullableDictionaryNullableStringNullableInt () => new ();
public T GenericNotNullConstrain<T> () where T : notnull => default!;
public T? GenericNullableClassConstrain<T> () where T : class? => default;
public T? GenericNullableRefTypeConstrain<T> () where T : ObjectTest => null;
public T GenericNotNullableRefTypeConstrain<T> () where T : ObjectTest => default!;
public T? GenericNullableInterface<T> () where T : IInterfaceTest => default;
public void GenericNotNullParameterConstrain<T> (T param) where T : notnull { }
public void GenericNullableClassParameterConstrain<T> (T param) where T : class? { }
public void GenericNullableRefTypeParameterConstrain<T> (T? param) where T : ObjectTest { }
public void GenericNullableInterfaceParameterConstrain<T> (T? param) where T : IInterfaceTest { }
}
public static PropertyInfo GetIndexer (Type type, params Type [] arguments) => type.GetProperties ().First (
x => x.GetIndexParameters ().Select (y => y.ParameterType).SequenceEqual (arguments));
Type testType = typeof (object);
NullabilityInfoContext context = new ();
[SetUp]
public void SetUp ()
{
testType = typeof (ObjectTest);
context = new ();
}
[TestCase ("NotNullableValueTypeField", typeof (int))]
[TestCase ("NotNullableRefTypeField", typeof (string))]
public void NotNullableFieldTest (string fieldName, Type expectedType)
{
var fieldInfo = testType.GetField (fieldName);
Assert.NotNull (fieldInfo, "fieldInfo != null");
var info = context.Create (fieldInfo);
Assert.False (info.IsNullable (), "isNullable");
Assert.AreEqual (expectedType, info.Type);
}
[TestCase ("NullableValueTypeField", typeof (Nullable<int>))]
[TestCase ("NullableRefTypeField", typeof (string))]
public void NullableFieldTest (string fieldName, Type expectedType)
{
var fieldInfo = testType.GetField (fieldName);
Assert.NotNull (fieldInfo);
var info = context.Create (fieldInfo);
Assert.True (info.IsNullable (), "isNullable");
Assert.AreEqual (expectedType, info.Type);
}
[TestCase ("NotNullableValueTypeProperty", typeof (int))]
[TestCase ("NotNullableRefProperty", typeof (string))]
public void NotNullablePropertyTest (string propertyName, Type expectedType)
{
var propertyInfo = testType.GetProperty (propertyName);
Assert.NotNull (propertyInfo, "propertyInto != null");
var info = context.Create (propertyInfo);
Assert.False (info.IsNullable (), "isNullable");
Assert.AreEqual (expectedType, info.Type);
}
// public int? this [int i] {
[TestCase (typeof (int), typeof (Nullable<int>), true, typeof (int), false)]
// public string? this [string? i]
[TestCase (typeof (string), typeof (string), true, typeof (string), true)]
// public string this [double? i] {
[TestCase (typeof (double?), typeof (string), false, typeof (Nullable<double>), true)]
public void IndexersTests (Type indexersTypes, Type resultType, bool isNullable, Type indexParameter, bool isIndexParameterNull)
{
var propertyInfo = GetIndexer (testType, indexersTypes);
Assert.NotNull (propertyInfo, "propertyInto != null");
var info = context.Create (propertyInfo.GetMethod.ReturnParameter);
Assert.AreEqual (isNullable, info.IsNullable (), "info.IsNullable");
Assert.AreEqual (resultType, info.Type, "info.Type");
var parameters = propertyInfo.SetMethod.GetParameters ();
var paramInfo = context.Create (parameters [0]);
Assert.AreEqual (isIndexParameterNull, paramInfo.IsNullable (), "paramInfo.IsNullable");
Assert.AreEqual (indexParameter, paramInfo.Type, "paramInfo.Type");
}
[TestCase ("NullableValueTypeProperty", typeof (Nullable<int>))]
[TestCase ("NullableRefProperty", typeof (string))]
public void NullablePropertyTest (string propertyName, Type expectedType)
{
var propertyInfo = testType.GetProperty (propertyName);
Assert.NotNull (propertyInfo, "propertyInfo != null");
var info = context.Create (propertyInfo);
Assert.True (info.IsNullable (), "isNullable");
Assert.AreEqual (expectedType, info.Type);
}
[TestCase ("NotNullableValueTypeParameter", typeof (int))]
[TestCase ("NotNullableRefParameter", typeof (string))]
public void NotNullableParameterTest (string methodName, Type expectedType)
{
var memberInfo = testType.GetMethod (methodName);
Assert.NotNull (memberInfo, "memberInfo != null");
var paramInfo = memberInfo.GetParameters () [0];
var info = context.Create (paramInfo);
Assert.False (info.IsNullable (), "isNullable");
Assert.AreEqual (expectedType, info.Type);
}
[TestCase ("NotNullableOutValueTypeParameter", typeof (int))]
[TestCase ("NotNullableRefValueTypeParameter", typeof (int))]
[TestCase ("NotNullableOutRefParameter", typeof (string))]
[TestCase ("NotNullableRefRefParameter", typeof (string))]
public void NotNullableRefParameterTest (string methodName, Type expectedType)
{
var memberInfo = testType.GetMethod (methodName);
Assert.NotNull (memberInfo, "memberInfo != null");
var paramInfo = memberInfo.GetParameters () [0];
var info = context.Create (paramInfo);
Assert.False (info.IsNullable (), "isNullable");
Assert.AreEqual (expectedType.MakeByRefType (), info.Type);
}
[TestCase ("NullableValueTypeParameter", typeof (Nullable<int>))]
[TestCase ("NullableRefTypeParameter", typeof (string))]
public void NullableParameterTest (string methdName, Type expectedType)
{
var memberInfo = testType.GetMethod (methdName);
Assert.NotNull (memberInfo, "memberInfo != null");
var paramInfo = memberInfo.GetParameters () [0];
var info = context.Create (paramInfo);
Assert.True (info.IsNullable (), "isNullable");
Assert.AreEqual (expectedType, info.Type);
}
[TestCase ("NullableOutValueTypeParameter", typeof (Nullable<int>))]
[TestCase ("NullableRefValueTypeParameter", typeof (Nullable<int>))]
[TestCase ("NullableOutRefTypeParameter", typeof (string))]
[TestCase ("NullableRefRefTypeParameter", typeof (string))]
public void NullableRefParameterTest (string methdName, Type expectedType)
{
var memberInfo = testType.GetMethod (methdName);
Assert.NotNull (memberInfo, "memberInfo != null");
var paramInfo = memberInfo.GetParameters () [0];
var info = context.Create (paramInfo);
Assert.True (info.IsNullable (), "isNullable");
Assert.AreEqual (expectedType.MakeByRefType (), info.Type);
}
[TestCase ("NotNullableValueTypeReturn", typeof (int))]
[TestCase ("NotNullableRefTypeReturn", typeof (string))]
public void NotNullableReturnTypeTest (string methodName, Type expectedType)
{
var memberInfo = testType.GetMethod (methodName);
Assert.NotNull (memberInfo, "memberInfo != null");
var info = context.Create (memberInfo.ReturnParameter);
Assert.IsFalse (info.IsNullable (), "isNullable");
Assert.AreEqual (expectedType, info.Type);
}
[TestCase ("NullableValueTypeReturn", typeof (Nullable<int>))]
[TestCase ("NullableRefTypeReturn", typeof (string))]
public void NullableReturnTypeTest (string methodName, Type expectedType)
{
var memberInfo = testType.GetMethod (methodName);
Assert.NotNull (memberInfo, "memberInfo != null");
var info = context.Create (memberInfo.ReturnParameter);
Assert.IsTrue (info.IsNullable (), "isNullable");
Assert.AreEqual (expectedType, info.Type);
}
[TestCase ("NotNullableValueTypeArray", typeof (int []), false, typeof (int), false)]
[TestCase ("NotNullableNullableValueTypeArray", typeof (int []), false, typeof (Nullable<int>), true)]
[TestCase ("NullableValueTypeArray", typeof (int []), true, typeof (int), false)]
[TestCase ("NullableNullableValueTypeArray", typeof (int []), true, typeof (Nullable<int>), true)]
[TestCase ("NotNullableRefTypeArray", typeof (string []), false, typeof (string), false)]
[TestCase ("NotNullableNullableRefTypeArray", typeof (string []), false, typeof (string), true)]
public void ReturnTypeArrayTests (string methodName, Type? expectedType, bool arrayIsNullable, Type expectedElementType, bool elementTypeIsNullable)
{
var memberInfo = testType.GetMethod (methodName);
Assert.NotNull (memberInfo, "memberInfo != null");
var info = context.Create (memberInfo.ReturnParameter);
Assert.AreEqual (arrayIsNullable, info.IsNullable (), "info.IsNullable");
Assert.AreEqual (expectedElementType, info.ElementType!.Type, "info.ElementType.Type");
Assert.AreEqual (elementTypeIsNullable, info.ElementType.IsNullable (), "info.ElementTyps.IsNullable");
}
[TestCase ("NotNullableNestedArrayNotNullableValueType", typeof (int [] []), false, false, typeof (int), false)]
[TestCase ("NotNullableNestedArrayNullableValueType", typeof (int? [] []), false, false, typeof (Nullable<int>), true)]
[TestCase ("NotNullableNestedNullableArrayNullableValueType", typeof (int? [] []), false, true, typeof (Nullable<int>), true)]
[TestCase ("NullableNestedNullableArrayNullableValueType", typeof (int? [] []), true, true, typeof (Nullable<int>), true)]
[TestCase ("NotNullableNestedArrayNotNullableRefType", typeof (string [] []), false, false, typeof (string), false)]
[TestCase ("NotNullableNestedArrayNullableRefType", typeof (string? [] []), false, false, typeof (string), true)]
[TestCase ("NotNullableNestedNullableArrayNullableRefType", typeof (string? [] []), false, true, typeof (string), true)]
[TestCase ("NullableNestedNullableArrayNullableRefType", typeof (string? [] []), true, true, typeof (string), true)]
public void ReturnNestedArrayTests (string methodName, Type? expectedType, bool isArrayNullable,
bool isNestedArrayNullable, Type nestedArrayType, bool isNestedArrayElementNullable)
{
var memberInfo = testType.GetMethod (methodName);
Assert.NotNull (memberInfo, "memberInfo != null");
var info = context.Create (memberInfo.ReturnParameter);
Assert.AreEqual (isArrayNullable, info.IsNullable (), "isArrayNullable");
Assert.AreEqual (isNestedArrayNullable, info.ElementType!.IsNullable (), "isNestedArrayNullable");
Assert.AreEqual (nestedArrayType, info.ElementType!.ElementType!.Type, "nestedArrayType");
Assert.AreEqual (isNestedArrayElementNullable, info.ElementType.ElementType.IsNullable (), "isNestedArrayElementNullable");
}
[TestCase ("NotNullableValueTypeList", typeof (List<int>), false, typeof (int), false)]
[TestCase ("NullableValueTypeList", typeof (List<Nullable<int>>), false, typeof (Nullable<int>), true)]
[TestCase ("NullableListNullableValueType", typeof (List<Nullable<int>>), true, typeof (Nullable<int>), true)]
[TestCase ("NotNullableRefTypeList", typeof (List<string>), false, typeof (string), false)]
[TestCase ("NullableRefTypeList", typeof (List<string>), false, typeof (string), true)]
[TestCase ("NullableListNullableRefType", typeof (List<string>), true, typeof (string), true)]
public void ReturnSimpleGenericType (string methodName, Type? expectedType, bool isGenericTypeNullable,
Type? genericParameterType, bool isGenericParameterNull)
{
var memberInfo = testType.GetMethod (methodName);
Assert.NotNull (memberInfo, "memberInfo != null");
var info = context.Create (memberInfo.ReturnParameter);
Assert.AreEqual (expectedType, info.Type, "info.Type");
Assert.AreEqual (isGenericTypeNullable, info.IsNullable (), "info.IsNullable");
Assert.AreEqual (genericParameterType, info.GenericTypeArguments! [0].Type, "genericParameterType");
Assert.AreEqual (isGenericParameterNull, info.GenericTypeArguments [0].IsNullable (), "isGenericParameterNull");
}
[TestCase ("NestedGenericNotNullableValueType", typeof (List<HashSet<int>>), false, typeof (HashSet<int>), false, typeof (int), false)]
[TestCase ("NestedGenericNullableValueType", typeof (List<HashSet<Nullable<int>>>), false, typeof (HashSet<Nullable<int>>), false, typeof (Nullable<int>), true)]
[TestCase ("NestedNullableGenericNullableValueType", typeof (List<HashSet<Nullable<int>>>), false, typeof (HashSet<Nullable<int>>), true, typeof (Nullable<int>), true)]
[TestCase ("NullableNestedNullableGenericNullableValueType", typeof (List<HashSet<Nullable<int>>>), true, typeof (HashSet<Nullable<int>>), true, typeof (Nullable<int>), true)]
[TestCase ("NestedGenericNotNullableRefType", typeof (List<HashSet<string>>), false, typeof (HashSet<string>), false, typeof (string), false)]
[TestCase ("NestedGenericNullableRefType", typeof (List<HashSet<string>>), false, typeof (HashSet<string>), false, typeof (string), true)]
[TestCase ("NestedNullableGenericNullableRefType", typeof (List<HashSet<string>>), false, typeof (HashSet<string>), true, typeof (string), true)]
[TestCase ("NullableNestedNullableGenericNullableRefType", typeof (List<HashSet<string>>), true, typeof (HashSet<string>), true, typeof (string), true)]
public void ReturnNestedGeneric (string methodName, Type? expectedType, bool isGenericNullable,
Type? nestedGenericType, bool isNestedGenericNullable, Type? nestedGenericArgumentType,
bool isNullableNestedGenericArgument)
{
var memberInfo = testType.GetMethod (methodName);
Assert.NotNull (memberInfo, "memberInfo != null");
var info = context.Create (memberInfo.ReturnParameter);
Assert.AreEqual (expectedType, info.Type, "info.Type");
Assert.AreEqual (isGenericNullable, info.IsNullable (), "info.IsNullable");
Assert.AreEqual (nestedGenericType, info.GenericTypeArguments! [0].Type, "nestedGenericType");
Assert.AreEqual (isNestedGenericNullable, info.GenericTypeArguments [0].IsNullable (), "isNestedGenericNullable");
Assert.AreEqual (nestedGenericArgumentType, info.GenericTypeArguments [0].GenericTypeArguments! [0].Type, "nestedGenericArgumentType");
Assert.AreEqual (isNullableNestedGenericArgument, info.GenericTypeArguments [0].GenericTypeArguments! [0].IsNullable (), "isNullableNestedGenericArgument");
}
[TestCase ("DictionaryStringInt", typeof (Dictionary<string, int>), false, typeof (string), false, typeof (int), false)]
[TestCase ("DictionaryStringNullableInt", typeof (Dictionary<string, Nullable<int>>), false, typeof (string), false, typeof (Nullable<int>), true)]
[TestCase ("DictionaryNullableStringNullableInt", typeof (Dictionary<string, Nullable<int>>), false, typeof (string), true, typeof (Nullable<int>), true)]
[TestCase ("NullableDictionaryNullableStringNullableInt", typeof (Dictionary<string, Nullable<int>>), true, typeof (string), true, typeof (Nullable<int>), true)]
public void ReturnDictionary (string methodName, Type? dictionaryType, bool isDictionaryNullable, Type? keyType,
bool isKeyNullable, Type? valueType, bool isValueNullable)
{
var memberInfo = testType.GetMethod (methodName);
Assert.NotNull (memberInfo, "memberInfo != null");
var info = context.Create (memberInfo.ReturnParameter);
Assert.AreEqual (dictionaryType, info.Type, "info.Type");
Assert.AreEqual (isDictionaryNullable, info.IsNullable (), "info.IsNullable");
Assert.AreEqual (keyType, info.GenericTypeArguments! [0].Type, "keyType");
Assert.AreEqual (isKeyNullable, info.GenericTypeArguments [0].IsNullable (), "isKeyNullable");
Assert.AreEqual (valueType, info.GenericTypeArguments [1].Type, "valueType");
Assert.AreEqual (isValueNullable, info.GenericTypeArguments [1].IsNullable (), "isValueNullable");
}
[TestCase ("GenericNotNullConstrain", false)]
[TestCase ("GenericNullableClassConstrain", true)]
[TestCase ("GenericNullableRefTypeConstrain", true)]
[TestCase ("GenericNullableInterface", true)] // limitation in the lang ? is ignore
public void GenericReturnConstrainTests (string methodName, bool returnTypeIsNull)
{
var memberInfo = testType.GetMethod (methodName);
Assert.NotNull (memberInfo, "memberInfo != null");
var info = context.Create (memberInfo.ReturnParameter);
Assert.AreEqual (returnTypeIsNull, info.IsNullable ());
}
[TestCase ("GenericNotNullParameterConstrain", false)]
[TestCase ("GenericNullableClassParameterConstrain", true)]
[TestCase ("GenericNullableRefTypeParameterConstrain", true)]
[TestCase ("GenericNullableInterfaceParameterConstrain", true)] // limitation in the lang ? is ignore
public void GenericParamConstrainTests (string methodName, bool returnTypeIsNull)
{
var memberInfo = testType.GetMethod (methodName);
Assert.NotNull (memberInfo, "memberInfo != null");
var info = context.Create (memberInfo.GetParameters () [0]);
Assert.AreEqual (returnTypeIsNull, info.IsNullable ());
}
}
}

Просмотреть файл

@ -63,6 +63,7 @@
<Compile Include="..\common\BinLog.cs">
<Link>BinLog.cs</Link>
</Compile>
<Compile Include="NullabilityContextTests.cs" />
</ItemGroup>
<ItemGroup>
<None Include="tests\is-direct-binding.cs" />