2
0
mirror of https://github.com/esiur/esiur-dotnet.git synced 2025-05-06 03:32:57 +00:00
esiur-dotnet/Esiur/Data/NullabilityInfoContext.cs
2024-11-10 14:49:53 +03:00

714 lines
29 KiB
C#

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Esiur.Data;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
namespace Esiur.Data
{
/// <summary>
/// Provides APIs for populating nullability information/context from reflection members:
/// <see cref="ParameterInfo"/>, <see cref="FieldInfo"/>, <see cref="PropertyInfo"/> and <see cref="EventInfo"/>.
/// </summary>
public sealed class NullabilityInfoContext
{
private const string CompilerServicesNameSpace = "System.Runtime.CompilerServices";
private readonly Dictionary<Module, NotAnnotatedStatus> _publicOnlyModules = new();
private readonly Dictionary<MemberInfo, NullabilityState> _context = new();
internal static bool IsSupported { get; } =
AppContext.TryGetSwitch("System.Reflection.NullabilityInfoContext.IsSupported", out bool isSupported) ? isSupported : true;
[Flags]
private enum NotAnnotatedStatus
{
None = 0x0, // no restriction, all members annotated
Private = 0x1, // private members not annotated
Internal = 0x2 // internal members not annotated
}
private NullabilityState? GetNullableContext(MemberInfo? memberInfo)
{
while (memberInfo != null)
{
if (_context.TryGetValue(memberInfo, out NullabilityState state))
{
return state;
}
foreach (CustomAttributeData attribute in memberInfo.GetCustomAttributesData())
{
if (attribute.AttributeType.Name == "NullableContextAttribute" &&
attribute.AttributeType.Namespace == CompilerServicesNameSpace &&
attribute.ConstructorArguments.Count == 1)
{
state = TranslateByte(attribute.ConstructorArguments[0].Value);
_context.Add(memberInfo, state);
return state;
}
}
memberInfo = memberInfo.DeclaringType;
}
return null;
}
/// <summary>
/// Populates <see cref="NullabilityInfo" /> for the given <see cref="ParameterInfo" />.
/// If the nullablePublicOnly feature is set for an assembly, like it does in .NET SDK, the private and/or internal member's
/// nullability attributes are omitted, in this case the API will return NullabilityState.Unknown state.
/// </summary>
/// <param name="parameterInfo">The parameter which nullability info gets populated</param>
/// <exception cref="ArgumentNullException">If the parameterInfo parameter is null</exception>
/// <returns><see cref="NullabilityInfo" /></returns>
public NullabilityInfo Create(ParameterInfo parameterInfo)
{
if (parameterInfo == null)
throw new ArgumentNullException();
EnsureIsSupported();
IList<CustomAttributeData> attributes = parameterInfo.GetCustomAttributesData();
NullableAttributeStateParser parser = parameterInfo.Member is MethodBase method && IsPrivateOrInternalMethodAndAnnotationDisabled(method)
? NullableAttributeStateParser.Unknown
: CreateParser(attributes);
NullabilityInfo nullability = GetNullabilityInfo(parameterInfo.Member, parameterInfo.ParameterType, parser);
if (nullability.ReadState != NullabilityState.Unknown)
{
CheckParameterMetadataType(parameterInfo, nullability);
}
CheckNullabilityAttributes(nullability, attributes);
return nullability;
}
private void CheckParameterMetadataType(ParameterInfo parameter, NullabilityInfo nullability)
{
if (parameter.Member is MethodInfo method)
{
MethodInfo metaMethod = GetMethodMetadataDefinition(method);
ParameterInfo? metaParameter = null;
if (string.IsNullOrEmpty(parameter.Name))
{
metaParameter = metaMethod.ReturnParameter;
}
else
{
ParameterInfo[] parameters = metaMethod.GetParameters();
for (int i = 0; i < parameters.Length; i++)
{
if (parameter.Position == i &&
parameter.Name == parameters[i].Name)
{
metaParameter = parameters[i];
break;
}
}
}
if (metaParameter != null)
{
CheckGenericParameters(nullability, metaMethod, metaParameter.ParameterType, parameter.Member.ReflectedType);
}
}
}
private static MethodInfo GetMethodMetadataDefinition(MethodInfo method)
{
if (method.IsGenericMethod && !method.IsGenericMethodDefinition)
{
method = method.GetGenericMethodDefinition();
}
return (MethodInfo)GetMemberMetadataDefinition(method);
}
private static void CheckNullabilityAttributes(NullabilityInfo nullability, IList<CustomAttributeData> attributes)
{
var codeAnalysisReadState = NullabilityState.Unknown;
var codeAnalysisWriteState = NullabilityState.Unknown;
foreach (CustomAttributeData attribute in attributes)
{
if (attribute.AttributeType.Namespace == "System.Diagnostics.CodeAnalysis")
{
if (attribute.AttributeType.Name == "NotNullAttribute")
{
codeAnalysisReadState = NullabilityState.NotNull;
}
else if ((attribute.AttributeType.Name == "MaybeNullAttribute" ||
attribute.AttributeType.Name == "MaybeNullWhenAttribute") &&
codeAnalysisReadState == NullabilityState.Unknown &&
!IsValueTypeOrValueTypeByRef(nullability.Type))
{
codeAnalysisReadState = NullabilityState.Nullable;
}
else if (attribute.AttributeType.Name == "DisallowNullAttribute")
{
codeAnalysisWriteState = NullabilityState.NotNull;
}
else if (attribute.AttributeType.Name == "AllowNullAttribute" &&
codeAnalysisWriteState == NullabilityState.Unknown &&
!IsValueTypeOrValueTypeByRef(nullability.Type))
{
codeAnalysisWriteState = NullabilityState.Nullable;
}
}
}
if (codeAnalysisReadState != NullabilityState.Unknown)
{
nullability.ReadState = codeAnalysisReadState;
}
if (codeAnalysisWriteState != NullabilityState.Unknown)
{
nullability.WriteState = codeAnalysisWriteState;
}
}
/// <summary>
/// Populates <see cref="NullabilityInfo" /> for the given <see cref="PropertyInfo" />.
/// If the nullablePublicOnly feature is set for an assembly, like it does in .NET SDK, the private and/or internal member's
/// nullability attributes are omitted, in this case the API will return NullabilityState.Unknown state.
/// </summary>
/// <param name="propertyInfo">The parameter which nullability info gets populated</param>
/// <exception cref="ArgumentNullException">If the propertyInfo parameter is null</exception>
/// <returns><see cref="NullabilityInfo" /></returns>
public NullabilityInfo Create(PropertyInfo propertyInfo)
{
if (propertyInfo == null)
throw new ArgumentNullException();
EnsureIsSupported();
MethodInfo? getter = propertyInfo.GetGetMethod(true);
MethodInfo? setter = propertyInfo.GetSetMethod(true);
bool annotationsDisabled = (getter == null || IsPrivateOrInternalMethodAndAnnotationDisabled(getter))
&& (setter == null || IsPrivateOrInternalMethodAndAnnotationDisabled(setter));
NullableAttributeStateParser parser = annotationsDisabled ? NullableAttributeStateParser.Unknown : CreateParser(propertyInfo.GetCustomAttributesData());
NullabilityInfo nullability = GetNullabilityInfo(propertyInfo, propertyInfo.PropertyType, parser);
if (getter != null)
{
CheckNullabilityAttributes(nullability, getter.ReturnParameter.GetCustomAttributesData());
}
else
{
nullability.ReadState = NullabilityState.Unknown;
}
if (setter != null)
{
CheckNullabilityAttributes(nullability, setter.GetParameters().Last().GetCustomAttributesData());
}
else
{
nullability.WriteState = NullabilityState.Unknown;
}
return nullability;
}
///// <summary>
///// Populates <see cref="NullabilityInfo" /> for the given <see cref="MethodInfo" />.
///// If the nullablePublicOnly feature is set for an assembly, like it does in .NET SDK, the private and/or internal member's
///// nullability attributes are omitted, in this case the API will return NullabilityState.Unknown state.
///// </summary>
///// <param name="propertyInfo">The parameter which nullability info gets populated</param>
///// <exception cref="ArgumentNullException">If the propertyInfo parameter is null</exception>
///// <returns><see cref="NullabilityInfo" /></returns>
//public NullabilityInfo Create(MethodInfo memberInfo)
//{
// if (memberInfo == null)
// throw new ArgumentNullException();
// EnsureIsSupported();
// bool annotationsDisabled = IsPrivateOrInternalMethodAndAnnotationDisabled(memberInfo);
// NullableAttributeStateParser parser = annotationsDisabled ? NullableAttributeStateParser.Unknown : CreateParser(memberInfo.GetCustomAttributesData());
// NullabilityInfo nullability = GetNullabilityInfo(memberInfo, memberInfo.ReturnType, parser);
// CheckNullabilityAttributes(nullability, memberInfo.ReturnParameter.GetCustomAttributesData());
// return nullability;
//}
private bool IsPrivateOrInternalMethodAndAnnotationDisabled(MethodBase method)
{
if ((method.IsPrivate || method.IsFamilyAndAssembly || method.IsAssembly) &&
IsPublicOnly(method.IsPrivate, method.IsFamilyAndAssembly, method.IsAssembly, method.Module))
{
return true;
}
return false;
}
/// <summary>
/// Populates <see cref="NullabilityInfo" /> for the given <see cref="EventInfo" />.
/// If the nullablePublicOnly feature is set for an assembly, like it does in .NET SDK, the private and/or internal member's
/// nullability attributes are omitted, in this case the API will return NullabilityState.Unknown state.
/// </summary>
/// <param name="eventInfo">The parameter which nullability info gets populated</param>
/// <exception cref="ArgumentNullException">If the eventInfo parameter is null</exception>
/// <returns><see cref="NullabilityInfo" /></returns>
public NullabilityInfo Create(EventInfo eventInfo)
{
if (eventInfo == null)
throw new ArgumentNullException();
EnsureIsSupported();
return GetNullabilityInfo(eventInfo, eventInfo.EventHandlerType!, CreateParser(eventInfo.GetCustomAttributesData()));
}
/// <summary>
/// Populates <see cref="NullabilityInfo" /> for the given <see cref="FieldInfo" />
/// If the nullablePublicOnly feature is set for an assembly, like it does in .NET SDK, the private and/or internal member's
/// nullability attributes are omitted, in this case the API will return NullabilityState.Unknown state.
/// </summary>
/// <param name="fieldInfo">The parameter which nullability info gets populated</param>
/// <exception cref="ArgumentNullException">If the fieldInfo parameter is null</exception>
/// <returns><see cref="NullabilityInfo" /></returns>
public NullabilityInfo Create(FieldInfo fieldInfo)
{
if (fieldInfo == null)
throw new ArgumentNullException();
EnsureIsSupported();
IList<CustomAttributeData> attributes = fieldInfo.GetCustomAttributesData();
NullableAttributeStateParser parser = IsPrivateOrInternalFieldAndAnnotationDisabled(fieldInfo) ? NullableAttributeStateParser.Unknown : CreateParser(attributes);
NullabilityInfo nullability = GetNullabilityInfo(fieldInfo, fieldInfo.FieldType, parser);
CheckNullabilityAttributes(nullability, attributes);
return nullability;
}
private static void EnsureIsSupported()
{
if (!IsSupported)
{
throw new InvalidOperationException();
}
}
private bool IsPrivateOrInternalFieldAndAnnotationDisabled(FieldInfo fieldInfo)
{
if ((fieldInfo.IsPrivate || fieldInfo.IsFamilyAndAssembly || fieldInfo.IsAssembly) &&
IsPublicOnly(fieldInfo.IsPrivate, fieldInfo.IsFamilyAndAssembly, fieldInfo.IsAssembly, fieldInfo.Module))
{
return true;
}
return false;
}
private bool IsPublicOnly(bool isPrivate, bool isFamilyAndAssembly, bool isAssembly, Module module)
{
if (!_publicOnlyModules.TryGetValue(module, out NotAnnotatedStatus value))
{
value = PopulateAnnotationInfo(module.GetCustomAttributesData());
_publicOnlyModules.Add(module, value);
}
if (value == NotAnnotatedStatus.None)
{
return false;
}
if ((isPrivate || isFamilyAndAssembly) && value.HasFlag(NotAnnotatedStatus.Private) ||
isAssembly && value.HasFlag(NotAnnotatedStatus.Internal))
{
return true;
}
return false;
}
private static NotAnnotatedStatus PopulateAnnotationInfo(IList<CustomAttributeData> customAttributes)
{
foreach (CustomAttributeData attribute in customAttributes)
{
if (attribute.AttributeType.Name == "NullablePublicOnlyAttribute" &&
attribute.AttributeType.Namespace == CompilerServicesNameSpace &&
attribute.ConstructorArguments.Count == 1)
{
if (attribute.ConstructorArguments[0].Value is bool boolValue && boolValue)
{
return NotAnnotatedStatus.Internal | NotAnnotatedStatus.Private;
}
else
{
return NotAnnotatedStatus.Private;
}
}
}
return NotAnnotatedStatus.None;
}
private NullabilityInfo GetNullabilityInfo(MemberInfo memberInfo, Type type, NullableAttributeStateParser parser)
{
int index = 0;
NullabilityInfo nullability = GetNullabilityInfo(memberInfo, type, parser, ref index);
if (nullability.ReadState != NullabilityState.Unknown)
{
TryLoadGenericMetaTypeNullability(memberInfo, nullability);
}
return nullability;
}
private NullabilityInfo GetNullabilityInfo(MemberInfo memberInfo, Type type, NullableAttributeStateParser parser, ref int index)
{
NullabilityState state = NullabilityState.Unknown;
NullabilityInfo? elementState = null;
NullabilityInfo[] genericArgumentsState = Array.Empty<NullabilityInfo>();
Type underlyingType = type;
if (underlyingType.IsByRef || underlyingType.IsPointer)
{
underlyingType = underlyingType.GetElementType()!;
}
if (underlyingType.IsValueType)
{
if (Nullable.GetUnderlyingType(underlyingType) is { } nullableUnderlyingType)
{
underlyingType = nullableUnderlyingType;
state = NullabilityState.Nullable;
}
else
{
state = NullabilityState.NotNull;
}
if (underlyingType.IsGenericType)
{
++index;
}
}
else
{
if (!parser.ParseNullableState(index++, ref state)
&& GetNullableContext(memberInfo) is { } contextState)
{
state = contextState;
}
if (underlyingType.IsArray)
{
elementState = GetNullabilityInfo(memberInfo, underlyingType.GetElementType()!, parser, ref index);
}
}
if (underlyingType.IsGenericType)
{
Type[] genericArguments = underlyingType.GetGenericArguments();
genericArgumentsState = new NullabilityInfo[genericArguments.Length];
for (int i = 0; i < genericArguments.Length; i++)
{
genericArgumentsState[i] = GetNullabilityInfo(memberInfo, genericArguments[i], parser, ref index);
}
}
return new NullabilityInfo(type, state, state, elementState, genericArgumentsState);
}
private static NullableAttributeStateParser CreateParser(IList<CustomAttributeData> customAttributes)
{
foreach (CustomAttributeData attribute in customAttributes)
{
if (attribute.AttributeType.Name == "NullableAttribute" &&
attribute.AttributeType.Namespace == CompilerServicesNameSpace &&
attribute.ConstructorArguments.Count == 1)
{
return new NullableAttributeStateParser(attribute.ConstructorArguments[0].Value);
}
}
return new NullableAttributeStateParser(null);
}
private void TryLoadGenericMetaTypeNullability(MemberInfo memberInfo, NullabilityInfo nullability)
{
MemberInfo? metaMember = GetMemberMetadataDefinition(memberInfo);
Type? metaType = null;
if (metaMember is FieldInfo field)
{
metaType = field.FieldType;
}
else if (metaMember is PropertyInfo property)
{
metaType = GetPropertyMetaType(property);
}
if (metaType != null)
{
CheckGenericParameters(nullability, metaMember!, metaType, memberInfo.ReflectedType);
}
}
private static MemberInfo GetMemberMetadataDefinition(MemberInfo member)
{
Type? type = member.DeclaringType;
if ((type != null) && type.IsGenericType && !type.IsGenericTypeDefinition)
{
return GetMemberWithSameMetadataDefinitionAs(type.GetGenericTypeDefinition(), member);
}
return member;
}
static bool HasSameMetadataDefinitionAs(MemberInfo mi, MemberInfo other) { throw new NotImplementedException(); }
static MemberInfo GetMemberWithSameMetadataDefinitionAs(Type type, MemberInfo member)
{
if (member == null)
throw new ArgumentNullException();
const BindingFlags all = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance;
foreach (MemberInfo myMemberInfo in type.GetMembers(all))
{
if (HasSameMetadataDefinitionAs(myMemberInfo, member))
{
return myMemberInfo;
}
}
throw new Exception();
}
private static Type GetPropertyMetaType(PropertyInfo property)
{
if (property.GetGetMethod(true) is MethodInfo method)
{
return method.ReturnType;
}
return property.GetSetMethod(true)!.GetParameters()[0].ParameterType;
}
private void CheckGenericParameters(NullabilityInfo nullability, MemberInfo metaMember, Type metaType, Type? reflectedType)
{
if (metaType.IsGenericParameter)
{
if (nullability.ReadState == NullabilityState.NotNull)
{
TryUpdateGenericParameterNullability(nullability, metaType, reflectedType);
}
}
else if (metaType.ContainsGenericParameters)
{
if (nullability.GenericTypeArguments.Length > 0)
{
Type[] genericArguments = metaType.GetGenericArguments();
for (int i = 0; i < genericArguments.Length; i++)
{
CheckGenericParameters(nullability.GenericTypeArguments[i], metaMember, genericArguments[i], reflectedType);
}
}
else if (nullability.ElementType is { } elementNullability && metaType.IsArray)
{
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);
}
}
}
private bool TryUpdateGenericParameterNullability(NullabilityInfo nullability, Type genericParameter, Type? reflectedType)
{
Debug.Assert(genericParameter.IsGenericParameter);
if (reflectedType is not null
&& !IsGenericMethodParameter(genericParameter)
&& TryUpdateGenericTypeParameterNullabilityFromReflectedType(nullability, genericParameter, reflectedType, reflectedType))
{
return true;
}
if (IsValueTypeOrValueTypeByRef(nullability.Type))
{
return true;
}
var state = NullabilityState.Unknown;
if (CreateParser(genericParameter.GetCustomAttributesData()).ParseNullableState(0, ref state))
{
nullability.ReadState = state;
nullability.WriteState = state;
return true;
}
if (GetNullableContext(genericParameter) is { } contextState)
{
nullability.ReadState = contextState;
nullability.WriteState = contextState;
return true;
}
return false;
}
bool IsGenericMethodParameter(Type genericParameter) => genericParameter.IsGenericParameter && genericParameter.DeclaringMethod != null;
private bool TryUpdateGenericTypeParameterNullabilityFromReflectedType(NullabilityInfo nullability, Type genericParameter, Type context, Type reflectedType)
{
Debug.Assert(genericParameter.IsGenericParameter && !IsGenericMethodParameter(genericParameter));
Type contextTypeDefinition = context.IsGenericType && !context.IsGenericTypeDefinition ? context.GetGenericTypeDefinition() : context;
if (genericParameter.DeclaringType == contextTypeDefinition)
{
return false;
}
Type? baseType = contextTypeDefinition.BaseType;
if (baseType is null)
{
return false;
}
if (!baseType.IsGenericType
|| (baseType.IsGenericTypeDefinition ? baseType : baseType.GetGenericTypeDefinition()) != genericParameter.DeclaringType)
{
return TryUpdateGenericTypeParameterNullabilityFromReflectedType(nullability, genericParameter, baseType, reflectedType);
}
Type[] genericArguments = baseType.GetGenericArguments();
Type genericArgument = genericArguments[genericParameter.GenericParameterPosition];
if (genericArgument.IsGenericParameter)
{
return TryUpdateGenericParameterNullability(nullability, genericArgument, reflectedType);
}
NullableAttributeStateParser parser = CreateParser(contextTypeDefinition.GetCustomAttributesData());
int nullabilityStateIndex = 1; // start at 1 since index 0 is the type itself
for (int i = 0; i < genericParameter.GenericParameterPosition; i++)
{
nullabilityStateIndex += CountNullabilityStates(genericArguments[i]);
}
return TryPopulateNullabilityInfo(nullability, parser, ref nullabilityStateIndex);
static int CountNullabilityStates(Type type)
{
Type underlyingType = Nullable.GetUnderlyingType(type) ?? type;
if (underlyingType.IsGenericType)
{
int count = 1;
foreach (Type genericArgument in underlyingType.GetGenericArguments())
{
count += CountNullabilityStates(genericArgument);
}
return count;
}
if (underlyingType.HasElementType)
{
return (underlyingType.IsArray ? 1 : 0) + CountNullabilityStates(underlyingType.GetElementType()!);
}
return type.IsValueType ? 0 : 1;
}
}
private static bool TryPopulateNullabilityInfo(NullabilityInfo nullability, NullableAttributeStateParser parser, ref int index)
{
bool isValueType = IsValueTypeOrValueTypeByRef(nullability.Type);
if (!isValueType)
{
var state = NullabilityState.Unknown;
if (!parser.ParseNullableState(index, ref state))
{
return false;
}
nullability.ReadState = state;
nullability.WriteState = state;
}
if (!isValueType || (Nullable.GetUnderlyingType(nullability.Type) ?? nullability.Type).IsGenericType)
{
index++;
}
if (nullability.GenericTypeArguments.Length > 0)
{
foreach (NullabilityInfo genericTypeArgumentNullability in nullability.GenericTypeArguments)
{
TryPopulateNullabilityInfo(genericTypeArgumentNullability, parser, ref index);
}
}
else if (nullability.ElementType is { } elementTypeNullability)
{
TryPopulateNullabilityInfo(elementTypeNullability, parser, ref index);
}
return true;
}
private static NullabilityState TranslateByte(object? value)
{
return value is byte b ? TranslateByte(b) : NullabilityState.Unknown;
}
private static NullabilityState TranslateByte(byte b) =>
b switch
{
1 => NullabilityState.NotNull,
2 => NullabilityState.Nullable,
_ => 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;
private readonly object? _nullableAttributeArgument;
public NullableAttributeStateParser(object? nullableAttributeArgument)
{
this._nullableAttributeArgument = nullableAttributeArgument;
}
public static NullableAttributeStateParser Unknown => new(UnknownByte);
public bool ParseNullableState(int index, ref NullabilityState state)
{
switch (this._nullableAttributeArgument)
{
case byte b:
state = TranslateByte(b);
return true;
case ReadOnlyCollection<CustomAttributeTypedArgument> args
when index < args.Count && args[index].Value is byte elementB:
state = TranslateByte(elementB);
return true;
default:
return false;
}
}
}
}
}