// 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 { /// /// Provides APIs for populating nullability information/context from reflection members: /// , , and . /// public sealed class NullabilityInfoContext { private const string CompilerServicesNameSpace = "System.Runtime.CompilerServices"; private readonly Dictionary _publicOnlyModules = new(); private readonly Dictionary _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; } /// /// Populates for the given . /// 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. /// /// The parameter which nullability info gets populated /// If the parameterInfo parameter is null /// public NullabilityInfo Create(ParameterInfo parameterInfo) { if (parameterInfo == null) throw new ArgumentNullException(); EnsureIsSupported(); IList 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 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; } } /// /// Populates for the given . /// 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. /// /// The parameter which nullability info gets populated /// If the propertyInfo parameter is null /// 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; } ///// ///// Populates for the given . ///// 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. ///// ///// The parameter which nullability info gets populated ///// If the propertyInfo parameter is null ///// //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; } /// /// Populates for the given . /// 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. /// /// The parameter which nullability info gets populated /// If the eventInfo parameter is null /// public NullabilityInfo Create(EventInfo eventInfo) { if (eventInfo == null) throw new ArgumentNullException(); EnsureIsSupported(); return GetNullabilityInfo(eventInfo, eventInfo.EventHandlerType!, CreateParser(eventInfo.GetCustomAttributesData())); } /// /// Populates for the given /// 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. /// /// The parameter which nullability info gets populated /// If the fieldInfo parameter is null /// public NullabilityInfo Create(FieldInfo fieldInfo) { if (fieldInfo == null) throw new ArgumentNullException(); EnsureIsSupported(); IList 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 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(); 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 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 args when index < args.Count && args[index].Value is byte elementB: state = TranslateByte(elementB); return true; default: return false; } } } } }