Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
97a3275
Progress so far on nullability annotations
jozkee May 13, 2024
650b53e
Complete implementation and make all NullableAnnotations tests pass.
eiriktsarpalis May 21, 2024
12cb92e
Update annotations for all failing unit tests.
eiriktsarpalis May 21, 2024
c22a567
Update src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs
eiriktsarpalis May 21, 2024
6be27d9
Update src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs
eiriktsarpalis May 21, 2024
0be6dfe
Address feedback
eiriktsarpalis May 21, 2024
1b0d954
Update to latest approved API and semantics.
eiriktsarpalis May 21, 2024
3c74fb8
Update src/libraries/System.Private.CoreLib/src/System/Reflection/Nul…
eiriktsarpalis May 21, 2024
3656b29
Update src/libraries/System.Private.CoreLib/src/System/Reflection/Nul…
eiriktsarpalis May 21, 2024
dcd54a9
Update src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
eiriktsarpalis May 22, 2024
080a353
Rename more ignoreNullableAnnotations stragglers.
eiriktsarpalis May 22, 2024
56015c7
Update src/libraries/System.Text.Json/src/System/Text/Json/Serializat…
eiriktsarpalis May 22, 2024
f82556c
Remove commented out code and address feedback.
eiriktsarpalis May 22, 2024
22dee5c
Update src/libraries/System.Text.Json/tests/System.Text.Json.SourceGe…
eiriktsarpalis May 22, 2024
6530530
Ensure the original parameter name flows exception messages.
eiriktsarpalis May 22, 2024
89e6cfd
Extract exceptions to a throw helper in the new properties.
eiriktsarpalis May 22, 2024
1e4388f
Extend test coverage to Nullable<T> properties.
eiriktsarpalis May 22, 2024
e2a3791
Revert sln changes
eiriktsarpalis May 22, 2024
4d63f59
Add second-pass review improvements.
eiriktsarpalis May 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ namespace System.Reflection
/// <summary>
/// A class that represents nullability info
/// </summary>
public sealed class NullabilityInfo
#if NET
public
#else
internal
#endif
sealed class NullabilityInfo
{
internal NullabilityInfo(Type type, NullabilityState readState, NullabilityState writeState,
NullabilityInfo? elementType, NullabilityInfo[] typeArguments)
Expand Down Expand Up @@ -46,7 +51,12 @@ internal NullabilityInfo(Type type, NullabilityState readState, NullabilityState
/// <summary>
/// An enum that represents nullability state
/// </summary>
public enum NullabilityState
#if NET
public
#else
internal
#endif
enum NullabilityState
{
/// <summary>
/// Nullability context not enabled (oblivious)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ namespace System.Reflection
/// 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
#if NET
public
#else
internal
#endif
sealed class NullabilityInfoContext
{
private const string CompilerServicesNameSpace = "System.Runtime.CompilerServices";
private readonly Dictionary<Module, NotAnnotatedStatus> _publicOnlyModules = new();
Expand Down Expand Up @@ -65,7 +70,11 @@ private enum NotAnnotatedStatus
/// <returns><see cref="NullabilityInfo" /></returns>
public NullabilityInfo Create(ParameterInfo parameterInfo)
{
#if NET
ArgumentNullException.ThrowIfNull(parameterInfo);
#else
NetstandardHelpers.ThrowIfNull(parameterInfo, nameof(parameterInfo));
#endif

EnsureIsSupported();

Expand Down Expand Up @@ -190,7 +199,11 @@ private static void CheckNullabilityAttributes(NullabilityInfo nullability, ILis
/// <returns><see cref="NullabilityInfo" /></returns>
public NullabilityInfo Create(PropertyInfo propertyInfo)
{
#if NET
ArgumentNullException.ThrowIfNull(propertyInfo);
#else
NetstandardHelpers.ThrowIfNull(propertyInfo, nameof(propertyInfo));
#endif

EnsureIsSupported();

Expand All @@ -212,7 +225,9 @@ public NullabilityInfo Create(PropertyInfo propertyInfo)

if (setter != null)
{
CheckNullabilityAttributes(nullability, setter.GetParametersAsSpan()[^1].GetCustomAttributesData());
ReadOnlySpan<ParameterInfo> parameters = setter.GetParametersAsSpan();
ParameterInfo parameter = parameters[parameters.Length - 1];
CheckNullabilityAttributes(nullability, parameter.GetCustomAttributesData());
}
else
{
Expand Down Expand Up @@ -243,7 +258,11 @@ private bool IsPrivateOrInternalMethodAndAnnotationDisabled(MethodBase method)
/// <returns><see cref="NullabilityInfo" /></returns>
public NullabilityInfo Create(EventInfo eventInfo)
{
#if NET
ArgumentNullException.ThrowIfNull(eventInfo);
#else
NetstandardHelpers.ThrowIfNull(eventInfo, nameof(eventInfo));
#endif

EnsureIsSupported();

Expand All @@ -260,7 +279,11 @@ public NullabilityInfo Create(EventInfo eventInfo)
/// <returns><see cref="NullabilityInfo" /></returns>
public NullabilityInfo Create(FieldInfo fieldInfo)
{
#if NET
ArgumentNullException.ThrowIfNull(fieldInfo);
#else
NetstandardHelpers.ThrowIfNull(fieldInfo, nameof(fieldInfo));
#endif

EnsureIsSupported();

Expand Down Expand Up @@ -497,7 +520,11 @@ private bool TryUpdateGenericParameterNullability(NullabilityInfo nullability, T
Debug.Assert(genericParameter.IsGenericParameter);

if (reflectedType is not null
#if NET
&& !genericParameter.IsGenericMethodParameter
#else
&& !genericParameter.IsGenericMethodParameter()
#endif
&& TryUpdateGenericTypeParameterNullabilityFromReflectedType(nullability, genericParameter, reflectedType, reflectedType))
{
return true;
Expand Down Expand Up @@ -528,7 +555,12 @@ private bool TryUpdateGenericParameterNullability(NullabilityInfo nullability, T

private bool TryUpdateGenericTypeParameterNullabilityFromReflectedType(NullabilityInfo nullability, Type genericParameter, Type context, Type reflectedType)
{
Debug.Assert(genericParameter.IsGenericParameter && !genericParameter.IsGenericMethodParameter);
Debug.Assert(genericParameter.IsGenericParameter &&
#if NET
!genericParameter.IsGenericMethodParameter);
#else
!genericParameter.IsGenericMethodParameter());
#endif

Type contextTypeDefinition = context.IsGenericType && !context.IsGenericTypeDefinition ? context.GetGenericTypeDefinition() : context;
if (genericParameter.DeclaringType == contextTypeDefinition)
Expand Down Expand Up @@ -666,4 +698,54 @@ public bool ParseNullableState(int index, ref NullabilityState state)
}
}
}

#if !NET
internal static class NetstandardHelpers
{
public static void ThrowIfNull(object? argument, string paramName)
{
if (argument is null)
{
Throw(paramName);
static void Throw(string paramName) => throw new ArgumentNullException(paramName);
}
}

[Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:UnrecognizedReflectionPattern",
Justification = "This is finding the MemberInfo with the same MetadataToken as specified MemberInfo. If the specified MemberInfo " +
"exists and wasn't trimmed, then the current Type's MemberInfo couldn't have been trimmed.")]
public static MemberInfo GetMemberWithSameMetadataDefinitionAs(this Type type, MemberInfo member)
{
ThrowIfNull(member, nameof(member));

const BindingFlags all = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance;
foreach (MemberInfo myMemberInfo in type.GetMembers(all))
{
if (myMemberInfo.HasSameMetadataDefinitionAs(member))
{
return myMemberInfo;
}
}

throw new MissingMemberException(type.FullName, member.Name);
}

private static bool HasSameMetadataDefinitionAs(this MemberInfo info, MemberInfo other)
{
if (info.MetadataToken != other.MetadataToken)
return false;

if (!info.Module.Equals(other.Module))
return false;

return true;
}

public static bool IsGenericMethodParameter(this Type type)
=> type.IsGenericParameter && type.DeclaringMethod is not null;

public static ReadOnlySpan<ParameterInfo> GetParametersAsSpan(this MethodBase metaMethod)
=> metaMethod.GetParameters();
}
#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ public JsonSourceGenerationOptionsAttribute(JsonSerializerDefaults defaults)
/// </summary>
public JsonCommentHandling ReadCommentHandling { get; set; }

/// <summary>
/// Specifies the default value of <see cref="JsonSerializerOptions.RespectNullableAnnotations"/> when set.
/// </summary>
public bool RespectNullableAnnotations { get; set; }

/// <summary>
/// Specifies the default value of <see cref="JsonSerializerOptions.UnknownTypeHandling"/> when set.
/// </summary>
Expand Down
111 changes: 111 additions & 0 deletions src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ public static ITypeSymbol EraseCompileTimeMetadata(this Compilation compilation,
type = type.WithNullableAnnotation(NullableAnnotation.None);
}

if (type is IArrayTypeSymbol arrayType)
{
ITypeSymbol elementType = compilation.EraseCompileTimeMetadata(arrayType.ElementType);
return compilation.CreateArrayTypeSymbol(elementType, arrayType.Rank);
}

if (type is INamedTypeSymbol namedType)
{
if (namedType.IsTupleType)
Expand Down Expand Up @@ -189,6 +195,9 @@ SpecialType.System_Byte or SpecialType.System_UInt16 or SpecialType.System_UInt3
SpecialType.System_Single or SpecialType.System_Double or SpecialType.System_Decimal;
}

public static bool IsNullableType(this ITypeSymbol type)
=> !type.IsValueType || type.OriginalDefinition.SpecialType is SpecialType.System_Nullable_T;

public static bool IsNullableValueType(this ITypeSymbol type, [NotNullWhen(true)] out ITypeSymbol? elementType)
{
if (type.IsValueType && type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T })
Expand Down Expand Up @@ -269,5 +278,107 @@ public static string GetTypeKindKeyword(this TypeDeclarationSyntax typeDeclarati
return null;
}
}

public static void ResolveNullabilityAnnotations(this IFieldSymbol field, out bool isGetterNonNullable, out bool isSetterNonNullable)
{
if (field.Type.IsNullableType())
{
// Because System.Text.Json cannot distinguish between nullable and non-nullable type parameters,
// (e.g. the same metadata is being used for both KeyValuePair<string, string?> and KeyValuePair<string, string>),
// we derive nullability annotations from the original definition of the field and not its instantiation.
// This preserves compatibility with the capabilities of the reflection-based NullabilityInfo reader.
field = field.OriginalDefinition;

isGetterNonNullable = IsOutputTypeNonNullable(field, field.Type);
isSetterNonNullable = IsInputTypeNonNullable(field, field.Type);
}
else
{
isGetterNonNullable = isSetterNonNullable = false;
}
}

public static void ResolveNullabilityAnnotations(this IPropertySymbol property, out bool isGetterNonNullable, out bool isSetterNonNullable)
{
if (property.Type.IsNullableType())
{
// Because System.Text.Json cannot distinguish between nullable and non-nullable type parameters,
// (e.g. the same metadata is being used for both KeyValuePair<string, string?> and KeyValuePair<string, string>),
// we derive nullability annotations from the original definition of the field and not its instantiation.
// This preserves compatibility with the capabilities of the reflection-based NullabilityInfo reader.
property = property.OriginalDefinition;

isGetterNonNullable = property.GetMethod != null && IsOutputTypeNonNullable(property, property.Type);
isSetterNonNullable = property.SetMethod != null && IsInputTypeNonNullable(property, property.Type);
}
else
{
isGetterNonNullable = isSetterNonNullable = false;
}
}

public static bool IsNullable(this IParameterSymbol parameter)
{
if (parameter.Type.IsNullableType())
{
// Because System.Text.Json cannot distinguish between nullable and non-nullable type parameters,
// (e.g. the same metadata is being used for both KeyValuePair<string, string?> and KeyValuePair<string, string>),
// we derive nullability annotations from the original definition of the field and not instation.
// This preserves compatibility with the capabilities of the reflection-based NullabilityInfo reader.
parameter = parameter.OriginalDefinition;
return !IsInputTypeNonNullable(parameter, parameter.Type);
}

return false;
}

private static bool IsOutputTypeNonNullable(this ISymbol symbol, ITypeSymbol returnType)
{
if (symbol.HasCodeAnalysisAttribute("MaybeNullAttribute"))
{
return false;
}

if (symbol.HasCodeAnalysisAttribute("NotNullAttribute"))
{
return true;
}

if (returnType is ITypeParameterSymbol { HasNotNullConstraint: false })
{
return false;
}

return returnType.NullableAnnotation is NullableAnnotation.NotAnnotated;
}

private static bool IsInputTypeNonNullable(this ISymbol symbol, ITypeSymbol inputType)
{
Debug.Assert(inputType.IsNullableType());

if (symbol.HasCodeAnalysisAttribute("AllowNullAttribute"))
{
return false;
}

if (symbol.HasCodeAnalysisAttribute("DisallowNullAttribute"))
{
return true;
}

if (inputType is ITypeParameterSymbol { HasNotNullConstraint: false })
{
return false;
}

return inputType.NullableAnnotation is NullableAnnotation.NotAnnotated;
}

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");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ private static class ExceptionMessages

public const string InvalidSerializablePropertyConfiguration =
"Invalid serializable-property configuration specified for type '{0}'. For more information, see 'JsonSourceGenerationMode.Serialization'.";

public const string PropertyGetterDisallowNull =
"The property or field '{0}' on type '{1}' doesn't allow getting null values. Consider updating its nullability annotation.";
};
}
}
Expand Down
Loading