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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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);
Comment thread
eiriktsarpalis marked this conversation as resolved.
}
}

[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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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)
Comment thread
eiriktsarpalis marked this conversation as resolved.
{
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 =>
Comment thread
eiriktsarpalis marked this conversation as resolved.
attr.AttributeClass?.Name == attributeName &&
attr.AttributeClass.ContainingNamespace.ToDisplayString() == "System.Diagnostics.CodeAnalysis");
}
}
}
Original file line numberDiff line numberDiff 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.";
Comment thread
jozkee marked this conversation as resolved.
};
}
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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);
Comment thread
eiriktsarpalis marked this conversation as resolved.
}
}

[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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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)
Comment thread
eiriktsarpalis marked this conversation as resolved.
{
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 =>
Comment thread
eiriktsarpalis marked this conversation as resolved.
attr.AttributeClass?.Name == attributeName &&
attr.AttributeClass.ContainingNamespace.ToDisplayString() == "System.Diagnostics.CodeAnalysis");
}
}
}
Original file line numberDiff line numberDiff 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.";
Comment thread
jozkee marked this conversation as resolved.
};
}
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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);
Comment thread
eiriktsarpalis marked this conversation as resolved.
}
}

[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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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)
Comment thread
eiriktsarpalis marked this conversation as resolved.
{
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 =>
Comment thread
eiriktsarpalis marked this conversation as resolved.
attr.AttributeClass?.Name == attributeName &&
attr.AttributeClass.ContainingNamespace.ToDisplayString() == "System.Diagnostics.CodeAnalysis");
}
}
}
Original file line numberDiff line numberDiff 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.";
Comment thread
jozkee marked this conversation as resolved.
};
}
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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);
Comment thread
eiriktsarpalis marked this conversation as resolved.
}
}

[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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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)
Comment thread
eiriktsarpalis marked this conversation as resolved.
{
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 =>
Comment thread
eiriktsarpalis marked this conversation as resolved.
attr.AttributeClass?.Name == attributeName &&
attr.AttributeClass.ContainingNamespace.ToDisplayString() == "System.Diagnostics.CodeAnalysis");
}
}
}
Original file line numberDiff line numberDiff 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.";
Comment thread
jozkee marked this conversation as resolved.
};
}
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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);
Comment thread
eiriktsarpalis marked this conversation as resolved.
}
}

[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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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)
Comment thread
eiriktsarpalis marked this conversation as resolved.
{
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 =>
Comment thread
eiriktsarpalis marked this conversation as resolved.
attr.AttributeClass?.Name == attributeName &&
attr.AttributeClass.ContainingNamespace.ToDisplayString() == "System.Diagnostics.CodeAnalysis");
}
}
}
Original file line numberDiff line numberDiff 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.";
Comment thread
jozkee marked this conversation as resolved.
};
}
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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);
Comment thread
eiriktsarpalis marked this conversation as resolved.
}
}

[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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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)
Comment thread
eiriktsarpalis marked this conversation as resolved.
{
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 =>
Comment thread
eiriktsarpalis marked this conversation as resolved.
attr.AttributeClass?.Name == attributeName &&
attr.AttributeClass.ContainingNamespace.ToDisplayString() == "System.Diagnostics.CodeAnalysis");
}
}
}
Original file line numberDiff line numberDiff 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.";
Comment thread
jozkee marked this conversation as resolved.
};
}
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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);
Comment thread
eiriktsarpalis marked this conversation as resolved.
}
}

[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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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)
Comment thread
eiriktsarpalis marked this conversation as resolved.
{
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 =>
Comment thread
eiriktsarpalis marked this conversation as resolved.
attr.AttributeClass?.Name == attributeName &&
attr.AttributeClass.ContainingNamespace.ToDisplayString() == "System.Diagnostics.CodeAnalysis");
}
}
}
Original file line numberDiff line numberDiff 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.";
Comment thread
jozkee marked this conversation as resolved.
};
}
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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);
Comment thread
eiriktsarpalis marked this conversation as resolved.
}
}

[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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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)
Comment thread
eiriktsarpalis marked this conversation as resolved.
{
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 =>
Comment thread
eiriktsarpalis marked this conversation as resolved.
attr.AttributeClass?.Name == attributeName &&
attr.AttributeClass.ContainingNamespace.ToDisplayString() == "System.Diagnostics.CodeAnalysis");
}
}
}
Original file line numberDiff line numberDiff 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.";
Comment thread
jozkee marked this conversation as resolved.
};
}
}
Expand Down
Loading