Skip to content

Commit

Permalink
Add support for nullable reference type metadata.
Browse files Browse the repository at this point in the history
  • Loading branch information
eiriktsarpalis committed Sep 22, 2023
1 parent aeb4e30 commit e96ef20
Show file tree
Hide file tree
Showing 20 changed files with 453 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,15 @@ internal sealed class JsonPropertyConverter<TDeclaringType, TPropertyType> : Jso
private readonly JsonConverter<TPropertyType> _propertyTypeConverter;
private readonly Getter<TDeclaringType, TPropertyType>? _getter;
private readonly Setter<TDeclaringType, TPropertyType>? _setter;
private readonly bool _getterDisallowsNull;
private readonly bool _setterDisallowsNull;

public JsonPropertyConverter(IPropertyShape<TDeclaringType, TPropertyType> property, JsonConverter<TPropertyType> propertyTypeConverter)
: base(property.Name)
{
_propertyTypeConverter = propertyTypeConverter;
_getterDisallowsNull = property.IsGetterNonNullableReferenceType;
_setterDisallowsNull = property.IsSetterNonNullableReferenceType;

if (property.HasGetter)
{
Expand All @@ -48,6 +52,7 @@ public JsonPropertyConverter(IConstructorParameterShape<TDeclaringType, TPropert
: base(parameter.Name!)
{
_propertyTypeConverter = propertyConverter;
_setterDisallowsNull = parameter.IsNonNullableReferenceType;
_setter = parameter.GetSetter();
}

Expand All @@ -59,6 +64,12 @@ public override void Read(ref Utf8JsonReader reader, ref TDeclaringType declarin
Debug.Assert(_setter != null);

TPropertyType? result = _propertyTypeConverter.Read(ref reader, typeof(TPropertyType), options);
if (result is null && _setterDisallowsNull)
{
Throw();
void Throw() => JsonHelpers.ThrowJsonException($"The property '{Name}' cannot be set to null.");
}

_setter(ref declaringType, result!);
}

Expand All @@ -67,6 +78,12 @@ public override void Write(Utf8JsonWriter writer, ref TDeclaringType declaringTy
Debug.Assert(_getter != null);

TPropertyType value = _getter(ref declaringType);
if (value is null && _getterDisallowsNull)
{
Throw();
void Throw() => JsonHelpers.ThrowJsonException($"The property '{Name}' cannot contain null.");
}

_propertyTypeConverter.Write(writer, value, options);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Buffers.Binary;
using System.Diagnostics;
using System.Numerics;
using System.Text;

namespace TypeShape.Applications.RandomGenerator;
Expand Down Expand Up @@ -278,6 +279,7 @@ private static IEnumerable<KeyValuePair<Type, object>> CreateDefaultGenerators()
yield return Create((random, _) => random.Next());
yield return Create((random, _) => NextLong(random));
yield return Create((random, _) => new Int128(NextULong(random), NextULong(random)));
yield return Create((random, _) => new BigInteger(NextLong(random)));

yield return Create((random, size) => (Half)((random.NextDouble() - 0.5) * size));
yield return Create((random, size) => (float)((random.NextDouble() - 0.5) * size));
Expand Down
58 changes: 57 additions & 1 deletion src/TypeShape.SourceGenerator/Helpers/RoslynHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,65 @@ namespace TypeShape.SourceGenerator.Helpers;

internal static class RoslynHelpers
{
public static bool IsNonNullableReferenceType(this ITypeSymbol type)
{
return !type.IsValueType && type.NullableAnnotation is NullableAnnotation.NotAnnotated;
}

public static bool IsNonNullableReferenceType(this IParameterSymbol parameter)
{
return !parameter.Type.IsValueType && IsParameterNonNullable(parameter, parameter.NullableAnnotation);
}

public static void GetNullableReferenceTypeInfo(this ISymbol member, out bool isGetterNonNullable, out bool isSetterNonNullable)
{
Debug.Assert(member is IFieldSymbol or IPropertySymbol);

if (member is IFieldSymbol { Type.IsValueType: false } field)
{
isGetterNonNullable = IsReturnValueNonNullable(field, field.NullableAnnotation);
isSetterNonNullable = IsParameterNonNullable(field, field.NullableAnnotation);
}
else if (member is IPropertySymbol { Type.IsValueType: false } property)
{
Debug.Assert(!property.IsIndexer);

isGetterNonNullable = property.GetMethod != null && IsReturnValueNonNullable(property, property.NullableAnnotation);
isSetterNonNullable = property.SetMethod != null && IsParameterNonNullable(property, property.NullableAnnotation);
}
else
{
isGetterNonNullable = false;
isSetterNonNullable = false;
}
}

private static bool IsReturnValueNonNullable(ISymbol symbol, NullableAnnotation returnTypeAnnotation)
{
return
!symbol.HasCodeAnalysisAttribute("MaybeNullAttribute") &&
(returnTypeAnnotation is NullableAnnotation.NotAnnotated ||
symbol.HasCodeAnalysisAttribute("NotNullAttribute"));
}

private static bool IsParameterNonNullable(ISymbol symbol, NullableAnnotation parameterAnnotation)
{
return
!symbol.HasCodeAnalysisAttribute("AllowNullAttribute") &&
(parameterAnnotation is NullableAnnotation.NotAnnotated ||
symbol.HasCodeAnalysisAttribute("DisallowNullAttribute"));
}

private static bool HasCodeAnalysisAttribute(this ISymbol symbol, string attributeName)
{
return symbol.GetAttributes().Any(attr =>
attr.AttributeClass?.Name == attributeName &&
attr.AttributeClass.ContainingNamespace.ToDisplayString() == "System.Diagnostics.CodeAnalysis");
}

public static ITypeSymbol EraseCompilerMetadata(this Compilation compilation, ITypeSymbol type)
{
if (type.NullableAnnotation is NullableAnnotation.Annotated)
if (type.NullableAnnotation != NullableAnnotation.None)
{
type = type.WithNullableAnnotation(NullableAnnotation.None);
}
Expand Down
1 change: 1 addition & 0 deletions src/TypeShape.SourceGenerator/Model/ConstructorModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public sealed record ConstructorParameterModel
public required TypeId ParameterType { get; init; }
public required int Position { get; init; }
public required bool IsRequired { get; init; }
public required bool IsNonNullableReferenceType { get; init; }
public required bool IsMemberInitializer { get; init; }
public required bool IsAutoProperty { get; init; }
public required bool HasDefaultValue { get; init; }
Expand Down
9 changes: 6 additions & 3 deletions src/TypeShape.SourceGenerator/Model/PropertyModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ public sealed record PropertyModel
public required TypeId? DeclaringInterfaceType { get; init; }
public required TypeId PropertyType { get; init; }

public bool EmitGetter { get; init; }
public bool EmitSetter { get; init; }
public required bool EmitGetter { get; init; }
public required bool EmitSetter { get; init; }

public bool IsField { get; init; }
public required bool IsField { get; init; }

public required bool IsGetterNonNullableReferenceType { get; init; }
public required bool IsSetterNonNullableReferenceType { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ private ConstructorParameterModel MapConstructorParameter(IParameterSymbol param
IsRequired = !parameter.HasExplicitDefaultValue,
IsMemberInitializer = false,
IsAutoProperty = false,
IsNonNullableReferenceType = parameter.IsNonNullableReferenceType(),
HasDefaultValue = parameter.HasExplicitDefaultValue,
DefaultValue = parameter.HasExplicitDefaultValue ? parameter.ExplicitDefaultValue : null,
DefaultValueRequiresCast = parameter.Type
Expand All @@ -129,6 +130,7 @@ private ConstructorParameterModel MapConstructorParameter(IParameterSymbol param
}

TypeId typeId = EnqueueForGeneration(propertySymbol.Type);
propertySymbol.GetNullableReferenceTypeInfo(out _, out bool isSetterNonNullable);
return new ConstructorParameterModel
{
ParameterType = typeId,
Expand All @@ -137,6 +139,7 @@ private ConstructorParameterModel MapConstructorParameter(IParameterSymbol param
IsRequired = propertySymbol.IsRequired,
IsMemberInitializer = true,
IsAutoProperty = propertySymbol.IsAutoProperty(),
IsNonNullableReferenceType = isSetterNonNullable,
HasDefaultValue = false,
DefaultValue = null,
DefaultValueRequiresCast = false,
Expand All @@ -151,6 +154,7 @@ private ConstructorParameterModel MapConstructorParameter(IParameterSymbol param
}

TypeId typeId = EnqueueForGeneration(fieldSymbol.Type);
fieldSymbol.GetNullableReferenceTypeInfo(out _, out bool isSetterNonNullable);
return new ConstructorParameterModel
{
ParameterType = typeId,
Expand All @@ -159,6 +163,7 @@ private ConstructorParameterModel MapConstructorParameter(IParameterSymbol param
IsRequired = fieldSymbol.IsRequired,
IsMemberInitializer = true,
IsAutoProperty = false,
IsNonNullableReferenceType = isSetterNonNullable,
HasDefaultValue = false,
DefaultValue = null,
DefaultValueRequiresCast = false,
Expand Down Expand Up @@ -198,6 +203,7 @@ ConstructorParameterModel MapTupleConstructorParameter(ITypeSymbol tupleElement,
HasDefaultValue = false,
IsRequired = true,
IsMemberInitializer = false,
IsNonNullableReferenceType = tupleElement.IsNonNullableReferenceType(),
IsAutoProperty = false,
DefaultValue = null,
DefaultValueRequiresCast = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,28 +69,35 @@ private IEnumerable<ISymbol> ResolvePropertyAndFieldSymbols(ITypeSymbol type)
private PropertyModel MapProperty(TypeId typeId, IPropertySymbol property)
{
Debug.Assert(!property.IsStatic && !property.IsIndexer);
property.GetNullableReferenceTypeInfo(out bool isGetterNonNullable, out bool isSetterNonNullable);
return new PropertyModel
{
Name = property.Name,
UnderlyingMemberName = property.Name,
DeclaringType = typeId,
DeclaringInterfaceType = property.ContainingType.TypeKind is TypeKind.Interface ? CreateTypeId(property.ContainingType) : null,
PropertyType = EnqueueForGeneration(property.Type),
IsGetterNonNullableReferenceType = isGetterNonNullable,
IsSetterNonNullableReferenceType = isSetterNonNullable,
EmitGetter = property.GetMethod is { } getter && IsAccessibleFromGeneratedType(getter),
EmitSetter = property.SetMethod is IMethodSymbol { IsInitOnly: false } setter && IsAccessibleFromGeneratedType(setter),
IsField = false,
};
}

private PropertyModel MapField(TypeId typeId, IFieldSymbol field)
{
Debug.Assert(!field.IsStatic);
field.GetNullableReferenceTypeInfo(out bool isGetterNonNullable, out bool isSetterNonNullable);
return new PropertyModel
{
Name = field.Name,
UnderlyingMemberName = field.Name,
DeclaringType = typeId,
DeclaringInterfaceType = null,
PropertyType = EnqueueForGeneration(field.Type),
IsGetterNonNullableReferenceType = isGetterNonNullable,
IsSetterNonNullableReferenceType = isSetterNonNullable,
EmitGetter = true,
EmitSetter = !field.IsReadOnly,
IsField = true,
Expand All @@ -99,15 +106,19 @@ private PropertyModel MapField(TypeId typeId, IFieldSymbol field)

private PropertyModel MapClassTupleElement(TypeId typeId, ITypeSymbol element, int index)
{
bool isNonNullableReferenceType = element.IsNonNullableReferenceType();
return new PropertyModel
{
Name = $"Item{index + 1}",
UnderlyingMemberName = $"{string.Join("", Enumerable.Repeat("Rest.", index / 7))}Item{(index % 7) + 1}",
DeclaringType = typeId,
DeclaringInterfaceType = null,
PropertyType = EnqueueForGeneration(element),
IsGetterNonNullableReferenceType = isNonNullableReferenceType,
IsSetterNonNullableReferenceType = isNonNullableReferenceType,
EmitGetter = true,
EmitSetter = false,
IsField = false
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ private static void FormatConstructorParameterFactory(SourceWriter writer, TypeM
Name = "{{parameter.Name}}",
ParameterType = {{parameter.ParameterType.GeneratedPropertyName}},
IsRequired = {{FormatBool(parameter.IsRequired)}},
IsNonNullableReferenceType = {{FormatBool(parameter.IsNonNullableReferenceType)}},
HasDefaultValue = {{FormatBool(parameter.HasDefaultValue)}},
DefaultValue = {{FormatDefaultValueExpr(parameter)}},
Setter = static (ref {{constructorArgumentStateFQN}} state, {{parameter.ParameterType.FullyQualifiedName}} value) => {{FormatSetterBody(constructor, parameter)}},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ private static void FormatPropertyFactory(SourceWriter writer, string methodName
Setter = {{(property.EmitSetter ? $"static (ref {type.Id.FullyQualifiedName} obj, {property.PropertyType.FullyQualifiedName} value) => obj.{property.UnderlyingMemberName} = value" : "null")}},
AttributeProviderFunc = {{FormatAttributeProviderFunc(type, property)}},
IsField = {{FormatBool(property.IsField)}},
IsGetterNonNullableReferenceType = {{FormatBool(property.IsGetterNonNullableReferenceType)}},
IsSetterNonNullableReferenceType = {{FormatBool(property.IsSetterNonNullableReferenceType)}},
};
""");

Expand Down
5 changes: 5 additions & 0 deletions src/TypeShape/Abstractions/IConstructorParameterShape.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ public interface IConstructorParameterShape
/// </remarks>
bool IsRequired { get; }

/// <summary>
/// Specifies whether the parameter is a non-nullable reference type.
/// </summary>
bool IsNonNullableReferenceType { get; }

/// <summary>
/// The provider used for parameter-level attribute resolution.
/// </summary>
Expand Down
10 changes: 10 additions & 0 deletions src/TypeShape/Abstractions/IPropertyShape.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ public interface IPropertyShape
/// </summary>
bool IsField { get; }

/// <summary>
/// Specifies whether the getter returns a non-nullable reference type.
/// </summary>
bool IsGetterNonNullableReferenceType { get; }

/// <summary>
/// Specifies whether the setter accepts a non-nullable reference type.
/// </summary>
bool IsSetterNonNullableReferenceType { get; }

/// <summary>
/// Accepts an <see cref="ITypeShapeVisitor"/> for strongly typed traversal.
/// </summary>
Expand Down
83 changes: 83 additions & 0 deletions src/TypeShape/ReflectionProvider/Helpers/ReflectionHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,89 @@ public static bool IsIEnumerable(this Type type)
return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>);
}

public static void GetNonNullableReferenceInfo(this MemberInfo memberInfo, out bool isGetterNonNullable, out bool isSetterNonNullable)
{
if (GetNullabilityInfo(memberInfo) is NullabilityInfo info)
{
isGetterNonNullable = info.ReadState is NullabilityState.NotNull;
isSetterNonNullable = info.WriteState is NullabilityState.NotNull;
}
else
{
isGetterNonNullable = false;
isSetterNonNullable = false;
}
}

public static bool IsNonNullableReferenceType(this ParameterInfo parameterInfo)
{
if (GetNullabilityInfo(parameterInfo) is NullabilityInfo info)
{
// Workaround for https://github.com/dotnet/runtime/issues/92487
if (parameterInfo.Member.TryGetGenericMethodDefinition() is MethodBase genericMethod &&
genericMethod.GetParameters()[parameterInfo.Position] is { ParameterType: { IsGenericParameter: true } typeParam })
{
Attribute? attr = typeParam.GetCustomAttributes().FirstOrDefault(attr =>
{
Type attrType = attr.GetType();
return attrType.Namespace == "System.Runtime.CompilerServices" && attrType.Name == "NullableAttribute";
});

byte[]? nullableFlags = (byte[])attr?.GetType().GetField("NullableFlags")?.GetValue(attr)!;
return nullableFlags[0] == 1;
}

return info.WriteState is NullabilityState.NotNull;
}
else
{
return false;
}
}

public static MethodBase? TryGetGenericMethodDefinition(this MemberInfo methodBase)
{
Debug.Assert(methodBase is MethodInfo or ConstructorInfo);

if (methodBase.DeclaringType!.IsGenericType)
{
Type genericTypeDef = methodBase.DeclaringType.GetGenericTypeDefinition();
MethodBase[] methods = methodBase.MemberType is MemberTypes.Constructor
? genericTypeDef.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
: genericTypeDef.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);

MethodBase match = methods.First(m => m.MetadataToken == methodBase.MetadataToken);
return ReferenceEquals(match, methodBase) ? null : match;
}

if (methodBase is MethodInfo { IsGenericMethod: true } methodInfo)
{
return methodInfo.GetGenericMethodDefinition();
}

return null;
}

private static NullabilityInfo? GetNullabilityInfo(ICustomAttributeProvider memberInfo)
{
Debug.Assert(memberInfo is PropertyInfo or FieldInfo or ParameterInfo);

switch (memberInfo)
{
case PropertyInfo prop:
return prop.PropertyType.IsValueType ? null : new NullabilityInfoContext().Create(prop);

case FieldInfo field:
return field.FieldType.IsValueType ? null : new NullabilityInfoContext().Create(field);

case ParameterInfo parameter:
return parameter.ParameterType.IsValueType ? null : new NullabilityInfoContext().Create(parameter);

default:
return null;
}
}

public static bool CanBeGenericArgument(this Type type)
{
return !(type == typeof(void) || type.IsPointer || type.IsByRef || type.IsByRefLike || type.ContainsGenericParameters);
Expand Down
Loading

0 comments on commit e96ef20

Please sign in to comment.