diff --git a/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs b/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs index 1c6ef166c4f34b..a871cd419d2657 100644 --- a/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs +++ b/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs @@ -17,10 +17,32 @@ namespace System.Text.Json.SourceGeneration internal static class RoslynExtensions { private static readonly Func? s_isClosedTypeAccessor = CreateIsClosedTypeAccessor(); + private static readonly Func? s_memorySafetyRulesVersionAccessor = CreateMemorySafetyRulesVersionAccessor(); public static LanguageVersion? GetLanguageVersion(this Compilation compilation) => compilation is CSharpCompilation csc ? csc.LanguageVersion : null; + public static bool UsesUpdatedMemorySafetyRules(this SemanticModel semanticModel) + { + const int UpdatedMemorySafetyRulesVersion = 2; + + // The module API includes both the compilation option and the legacy feature flag. + // Older compiler hosts expose only the temporary feature-flag opt-in. + return s_memorySafetyRulesVersionAccessor is { } getVersion + ? getVersion(semanticModel.Compilation.SourceModule) >= UpdatedMemorySafetyRulesVersion + : semanticModel.SyntaxTree.Options.Features.ContainsKey("updated-memory-safety-rules"); + } + + private static Func? CreateMemorySafetyRulesVersionAccessor() + { + // The enum-valued API is unavailable in Roslyn 3.11; delegate binding accepts its underlying int type. + // https://github.com/dotnet/roslyn/pull/84860 + MethodInfo? getter = typeof(IModuleSymbol).GetProperty("MemorySafetyRulesVersion")?.GetMethod; + return getter is null + ? null + : (Func)getter.CreateDelegate(typeof(Func)); + } + public static INamedTypeSymbol? GetBestTypeByMetadataName(this Compilation compilation, Type type) { Debug.Assert(!type.IsArray, "Resolution logic only capable of handling named types."); diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index bb0283cc71456a..8ec68abadc4318 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -643,7 +643,7 @@ private SourceText GenerateForObject(ContextGenerationSpec contextSpec, TypeGene if (propInitMethodName != null) { writer.WriteLine(); - GeneratePropMetadataInitFunc(writer, propInitMethodName, typeMetadata); + GeneratePropMetadataInitFunc(writer, contextSpec, propInitMethodName, typeMetadata); } if (serializeMethodName != null) @@ -659,10 +659,10 @@ private SourceText GenerateForObject(ContextGenerationSpec contextSpec, TypeGene } // Generate UnsafeAccessor methods or reflection cache fields for property accessors. - _emitValueTypeSetterDelegate |= GeneratePropertyAccessors(writer, typeMetadata); + _emitValueTypeSetterDelegate |= GeneratePropertyAccessors(writer, contextSpec, typeMetadata); // Generate constructor accessor for inaccessible [JsonConstructor] constructors. - GenerateConstructorAccessor(writer, typeMetadata); + GenerateConstructorAccessor(writer, contextSpec, typeMetadata); writer.Indentation--; writer.WriteLine('}'); @@ -826,7 +826,7 @@ private static string FormatNullCast(UnionCaseSpec caseSpec) : $"({fqn}?)"; } - private void GeneratePropMetadataInitFunc(SourceWriter writer, string propInitMethodName, TypeGenerationSpec typeGenerationSpec) + private void GeneratePropMetadataInitFunc(SourceWriter writer, ContextGenerationSpec contextSpec, string propInitMethodName, TypeGenerationSpec typeGenerationSpec) { ImmutableEquatableArray properties = typeGenerationSpec.PropertyGenSpecs; HashSet duplicateMemberNames = GetDuplicateMemberNames(properties); @@ -855,8 +855,8 @@ property.DefaultIgnoreCondition is JsonIgnoreCondition.Always && string propertyTypeFQN = isIgnoredPropertyOfUnusedType ? "object" : property.PropertyType.FullyQualifiedName; - string getterValue = GetPropertyGetterValue(property, typeGenerationSpec, propertyName, declaringTypeFQN, i, duplicateMemberNames.Contains(property.MemberName)); - string setterValue = GetPropertySetterValue(property, typeGenerationSpec, propertyName, declaringTypeFQN, i, duplicateMemberNames.Contains(property.MemberName)); + string getterValue = GetPropertyGetterValue(contextSpec, property, typeGenerationSpec, propertyName, declaringTypeFQN, i, duplicateMemberNames.Contains(property.MemberName)); + string setterValue = GetPropertySetterValue(contextSpec, property, typeGenerationSpec, propertyName, declaringTypeFQN, i, duplicateMemberNames.Contains(property.MemberName)); string ignoreConditionNamedArg = property.DefaultIgnoreCondition.HasValue ? $"{JsonIgnoreConditionTypeRef}.{property.DefaultIgnoreCondition.Value}" @@ -949,7 +949,7 @@ private static bool NeedsAccessorForGetter(PropertyGenerationSpec property) /// private static bool NeedsAccessorForSetter(PropertyGenerationSpec property) { - if (property.DefaultIgnoreCondition is JsonIgnoreCondition.Always) + if (property.IsReadOnly || property.DefaultIgnoreCondition is JsonIgnoreCondition.Always) { return false; } @@ -969,7 +969,14 @@ private static bool NeedsAccessorForSetter(PropertyGenerationSpec property) return false; } + private static string GetUnboxExpression(ContextGenerationSpec contextSpec, string declaringTypeFQN) + { + string expression = $"{UnsafeTypeRef}.Unbox<{declaringTypeFQN}>(obj)"; + return contextSpec.UseUpdatedMemorySafetyRules ? $"unsafe({expression})" : expression; + } + private static string GetPropertyGetterValue( + ContextGenerationSpec contextSpec, PropertyGenerationSpec property, TypeGenerationSpec typeGenerationSpec, string propertyName, @@ -995,7 +1002,7 @@ private static string GetPropertyGetterValue( { // UnsafeAccessor externs for value types take 'ref T'. string castExpr = typeGenerationSpec.TypeRef.IsValueType - ? $"ref {UnsafeTypeRef}.Unbox<{declaringTypeFQN}>(obj)" + ? $"ref {GetUnboxExpression(contextSpec, declaringTypeFQN)}" : $"({declaringTypeFQN})obj"; string accessorName = property.IsProperty @@ -1005,9 +1012,13 @@ private static string GetPropertyGetterValue( return $"static obj => {accessorName}({castExpr})"; } - // Reflection fallback wrappers are strongly typed; cast in the delegate. string getterName = GetAccessorName(typeFriendlyName, "get", property.MemberName, propertyIndex, needsDisambiguation); + if (!property.IsProperty) + { + return $"static obj => {getterName}(obj)"; + } + // Reflection fallback property wrappers are strongly typed; cast in the delegate. return $"static obj => {getterName}(({declaringTypeFQN})obj)"; } @@ -1015,6 +1026,7 @@ private static string GetPropertyGetterValue( } private static string GetPropertySetterValue( + ContextGenerationSpec contextSpec, PropertyGenerationSpec property, TypeGenerationSpec typeGenerationSpec, string propertyName, @@ -1029,19 +1041,19 @@ private static string GetPropertySetterValue( if (property is { CanUseSetter: true, IsInitOnlySetter: true }) { - return GetAccessorBasedSetterDelegate(property, typeGenerationSpec, declaringTypeFQN, propertyIndex, needsDisambiguation); + return GetAccessorBasedSetterDelegate(contextSpec, property, typeGenerationSpec, declaringTypeFQN, propertyIndex, needsDisambiguation); } if (property.CanUseSetter) { return typeGenerationSpec.TypeRef.IsValueType - ? $"""static (obj, value) => {UnsafeTypeRef}.Unbox<{declaringTypeFQN}>(obj).{propertyName} = value!""" + ? $"""static (obj, value) => {GetUnboxExpression(contextSpec, declaringTypeFQN)}.{propertyName} = value!""" : $"""static (obj, value) => (({declaringTypeFQN})obj).{propertyName} = value!"""; } if (NeedsAccessorForSetter(property)) { - return GetAccessorBasedSetterDelegate(property, typeGenerationSpec, declaringTypeFQN, propertyIndex, needsDisambiguation); + return GetAccessorBasedSetterDelegate(contextSpec, property, typeGenerationSpec, declaringTypeFQN, propertyIndex, needsDisambiguation); } return "null"; @@ -1052,6 +1064,7 @@ private static string GetPropertySetterValue( /// or the strongly typed reflection wrapper. /// private static string GetAccessorBasedSetterDelegate( + ContextGenerationSpec contextSpec, PropertyGenerationSpec property, TypeGenerationSpec typeGenerationSpec, string declaringTypeFQN, @@ -1063,7 +1076,7 @@ private static string GetAccessorBasedSetterDelegate( if (property.CanUseUnsafeAccessors) { string castExpr = typeGenerationSpec.TypeRef.IsValueType - ? $"ref {UnsafeTypeRef}.Unbox<{declaringTypeFQN}>(obj)" + ? $"ref {GetUnboxExpression(contextSpec, declaringTypeFQN)}" : $"({declaringTypeFQN})obj"; if (property.IsProperty) @@ -1076,17 +1089,23 @@ private static string GetAccessorBasedSetterDelegate( return $"static (obj, value) => {fieldName}({castExpr}) = value!"; } - // Reflection fallback wrapper is strongly typed; cast in the delegate like UnsafeAccessor. string setterName = GetAccessorName(typeFriendlyName, "set", property.MemberName, propertyIndex, needsDisambiguation); + if (!property.IsProperty) + { + return $"static (obj, value) => {setterName}(obj, value!)"; + } + + // Reflection fallback property wrappers are strongly typed; cast in the delegate like UnsafeAccessor. string setterCastExpr = typeGenerationSpec.TypeRef.IsValueType - ? $"ref {UnsafeTypeRef}.Unbox<{declaringTypeFQN}>(obj)" + ? $"ref {GetUnboxExpression(contextSpec, declaringTypeFQN)}" : $"({declaringTypeFQN})obj"; return $"static (obj, value) => {setterName}({setterCastExpr}, value!)"; } - private static bool GeneratePropertyAccessors(SourceWriter writer, TypeGenerationSpec typeGenerationSpec) + private static bool GeneratePropertyAccessors(SourceWriter writer, ContextGenerationSpec contextSpec, TypeGenerationSpec typeGenerationSpec) { + string safetyModifier = contextSpec.UseUpdatedMemorySafetyRules ? "safe " : ""; ImmutableEquatableArray properties = typeGenerationSpec.PropertyGenSpecs; HashSet duplicateMemberNames = GetDuplicateMemberNames(properties); bool needsAccessors = false; @@ -1141,14 +1160,14 @@ private static bool GeneratePropertyAccessors(SourceWriter writer, TypeGeneratio { string accessorName = GetAccessorName(typeFriendlyName, "get", property.MemberName, i, disambiguate); writer.WriteLine($"""[{UnsafeAccessorAttributeTypeRef}({UnsafeAccessorKindTypeRef}.Method, Name = "get_{property.MemberName}")]"""); - writer.WriteLine($"private static extern {propertyTypeFQN} {accessorName}({refPrefix}{declaringTypeFQN} obj);"); + writer.WriteLine($"private static {safetyModifier}extern {propertyTypeFQN} {accessorName}({refPrefix}{declaringTypeFQN} obj);"); } if (needsSetterAccessor) { string accessorName = GetAccessorName(typeFriendlyName, "set", property.MemberName, i, disambiguate); writer.WriteLine($"""[{UnsafeAccessorAttributeTypeRef}({UnsafeAccessorKindTypeRef}.Method, Name = "set_{property.MemberName}")]"""); - writer.WriteLine($"private static extern void {accessorName}({refPrefix}{declaringTypeFQN} obj, {propertyTypeFQN} value);"); + writer.WriteLine($"private static {safetyModifier}extern void {accessorName}({refPrefix}{declaringTypeFQN} obj, {propertyTypeFQN} value);"); } } else @@ -1156,7 +1175,7 @@ private static bool GeneratePropertyAccessors(SourceWriter writer, TypeGeneratio // Field: single UnsafeAccessor that returns ref T, used for both get and set. string fieldAccessorName = GetAccessorName(typeFriendlyName, "field", property.MemberName, i, disambiguate); writer.WriteLine($"""[{UnsafeAccessorAttributeTypeRef}({UnsafeAccessorKindTypeRef}.Field, Name = "{property.MemberName}")]"""); - writer.WriteLine($"private static extern ref {propertyTypeFQN} {fieldAccessorName}({refPrefix}{declaringTypeFQN} obj);"); + writer.WriteLine($"private static {safetyModifier}extern ref {propertyTypeFQN} {fieldAccessorName}({refPrefix}{declaringTypeFQN} obj);"); } } } @@ -1247,7 +1266,7 @@ private static bool GeneratePropertyAccessors(SourceWriter writer, TypeGeneratio string constraintClauses = firstProperty.DeclaringTypeParameterConstraintClauses is { } c ? $" {c}" : ""; writer.WriteLine(); - writer.WriteLine($"private static class __GenericAccessors_{typeFriendlyName}<{typeParamList}>{constraintClauses}"); + writer.WriteLine($"private static partial class __GenericAccessors_{typeFriendlyName}_{firstProperty.DeclaringTypeIndex}<{typeParamList}>{constraintClauses}"); writer.WriteLine('{'); writer.Indentation++; @@ -1258,7 +1277,7 @@ private static bool GeneratePropertyAccessors(SourceWriter writer, TypeGeneratio bool disambiguate = entry.Disambiguate; bool needsGetter = entry.NeedsGetter; bool needsSetter = entry.NeedsSetter; - string openPropertyTypeFQN = property.OpenPropertyTypeFQN!; + string openPropertyTypeFQN = property.OpenPropertyTypeFQN ?? property.PropertyType.FullyQualifiedName; if (property.IsProperty) { @@ -1266,21 +1285,21 @@ private static bool GeneratePropertyAccessors(SourceWriter writer, TypeGeneratio { string accessorName = GetAccessorName(typeFriendlyName, "get", property.MemberName, index, disambiguate); writer.WriteLine($"""[{UnsafeAccessorAttributeTypeRef}({UnsafeAccessorKindTypeRef}.Method, Name = "get_{property.MemberName}")]"""); - writer.WriteLine($"public static extern {openPropertyTypeFQN} {accessorName}({refPrefix}{openDeclaringTypeFQN} obj);"); + writer.WriteLine($"public static {safetyModifier}extern {openPropertyTypeFQN} {accessorName}({refPrefix}{openDeclaringTypeFQN} obj);"); } if (needsSetter) { string accessorName = GetAccessorName(typeFriendlyName, "set", property.MemberName, index, disambiguate); writer.WriteLine($"""[{UnsafeAccessorAttributeTypeRef}({UnsafeAccessorKindTypeRef}.Method, Name = "set_{property.MemberName}")]"""); - writer.WriteLine($"public static extern void {accessorName}({refPrefix}{openDeclaringTypeFQN} obj, {openPropertyTypeFQN} value);"); + writer.WriteLine($"public static {safetyModifier}extern void {accessorName}({refPrefix}{openDeclaringTypeFQN} obj, {openPropertyTypeFQN} value);"); } } else { string fieldAccessorName = GetAccessorName(typeFriendlyName, "field", property.MemberName, index, disambiguate); writer.WriteLine($"""[{UnsafeAccessorAttributeTypeRef}({UnsafeAccessorKindTypeRef}.Field, Name = "{property.MemberName}")]"""); - writer.WriteLine($"public static extern ref {openPropertyTypeFQN} {fieldAccessorName}({refPrefix}{openDeclaringTypeFQN} obj);"); + writer.WriteLine($"public static {safetyModifier}extern ref {openPropertyTypeFQN} {fieldAccessorName}({refPrefix}{openDeclaringTypeFQN} obj);"); } } @@ -1306,7 +1325,7 @@ private static string GetAccessorName(string typeFriendlyName, string accessorKi /// /// For properties on generic types using wrapper-class UnsafeAccessors (.NET 9+), returns the /// fully qualified accessor reference including the generic wrapper class prefix, e.g. - /// __GenericAccessors_MyType<int>.__get_MyType_Name. + /// __GenericAccessors_MyType_0<int>.__get_MyType_Name. /// For non-generic types, returns the plain accessor name. /// private static string GetQualifiedAccessorName(PropertyGenerationSpec property, string typeFriendlyName, string accessorKind, string memberName, int propertyIndex, bool needsDisambiguation) @@ -1320,7 +1339,7 @@ private static string GetQualifiedAccessorName(PropertyGenerationSpec property, string closedTypeArgs = property.DeclaringType.FullyQualifiedName; int openAngle = closedTypeArgs.IndexOf('<'); string typeArgsList = closedTypeArgs.Substring(openAngle); - return $"__GenericAccessors_{typeFriendlyName}{typeArgsList}.{accessorName}"; + return $"__GenericAccessors_{typeFriendlyName}_{property.DeclaringTypeIndex}{typeArgsList}.{accessorName}"; } private static string GetReflectionCacheName(string typeFriendlyName, string accessorKind, string memberName, int propertyIndex, bool needsDisambiguation) @@ -1352,8 +1371,18 @@ private static HashSet GetDuplicateMemberNames(ImmutableEquatableArray

static TypeName __ctor_TypeName(params) /// - private static string GetConstructorAccessorName(TypeGenerationSpec typeSpec) - => $"__ctor_{typeSpec.TypeInfoPropertyName}"; + private static string GetConstructorAccessorName(TypeGenerationSpec typeSpec, bool qualified = true) + { + string accessorName = $"__ctor_{typeSpec.TypeInfoPropertyName}"; + if (qualified && typeSpec.CanUseUnsafeAccessorForConstructor && typeSpec.OpenDeclaringTypeFQN is not null) + { + string typeFQN = typeSpec.TypeRef.FullyQualifiedName; + string typeArgsList = typeFQN.Substring(typeFQN.IndexOf('<')); + return $"__GenericAccessors_{typeSpec.TypeInfoPropertyName}_0{typeArgsList}.{accessorName}"; + } + + return accessorName; + } private static string GetConstructorReflectionCacheName(TypeGenerationSpec typeSpec) => $"s_ctor_{typeSpec.TypeInfoPropertyName}"; @@ -1363,7 +1392,7 @@ private static string GetConstructorReflectionCacheName(TypeGenerationSpec typeS /// For UnsafeAccessor: emits a [UnsafeAccessor(Constructor)] extern method. /// For reflection fallback: emits a cached ConstructorInfo and a wrapper method. /// - private static void GenerateConstructorAccessor(SourceWriter writer, TypeGenerationSpec typeSpec) + private static void GenerateConstructorAccessor(SourceWriter writer, ContextGenerationSpec contextSpec, TypeGenerationSpec typeSpec) { if (!typeSpec.ConstructorIsInaccessible) { @@ -1372,10 +1401,21 @@ private static void GenerateConstructorAccessor(SourceWriter writer, TypeGenerat writer.WriteLine(); - string typeFQN = typeSpec.TypeRef.FullyQualifiedName; - string wrapperName = GetConstructorAccessorName(typeSpec); + bool useGenericWrapper = typeSpec.CanUseUnsafeAccessorForConstructor && typeSpec.OpenDeclaringTypeFQN is not null; + string typeFQN = useGenericWrapper ? typeSpec.OpenDeclaringTypeFQN! : typeSpec.TypeRef.FullyQualifiedName; + string wrapperName = GetConstructorAccessorName(typeSpec, qualified: false); ImmutableEquatableArray parameters = typeSpec.CtorParamGenSpecs; + if (useGenericWrapper) + { + // The constructor and member accessors share the declaring type's helper. + string typeParamList = string.Join(", ", typeSpec.DeclaringTypeParameterNames!); + string constraintClauses = typeSpec.DeclaringTypeParameterConstraintClauses is { } c ? $" {c}" : ""; + writer.WriteLine($"private static partial class __GenericAccessors_{typeSpec.TypeInfoPropertyName}_0<{typeParamList}>{constraintClauses}"); + writer.WriteLine('{'); + writer.Indentation++; + } + // Build the parameter list for the wrapper method. var wrapperParams = new StringBuilder(); var callArgs = new StringBuilder(); @@ -1388,14 +1428,24 @@ private static void GenerateConstructorAccessor(SourceWriter writer, TypeGenerat callArgs.Append(", "); } - wrapperParams.Append($"{param.ParameterType.FullyQualifiedName} p{param.ParameterIndex}"); - callArgs.Append($"p{param.ParameterIndex}"); + string parameterTypeFQN = (useGenericWrapper ? param.OpenParameterTypeFQN : null) ?? param.ParameterType.FullyQualifiedName; + string refModifier = param.RefKind switch + { + RefKind.Ref => "ref ", + RefKind.Out => "out ", + // 'in' preserves the readonly by-ref signature without requiring C# 12. + RefKind.In or RefKindRefReadOnlyParameter => "in ", + _ => "", + }; + wrapperParams.Append($"{refModifier}{parameterTypeFQN} p{param.ParameterIndex}"); + callArgs.Append(param.RefKind is RefKind.Out ? "null" : $"p{param.ParameterIndex}"); } if (typeSpec.CanUseUnsafeAccessorForConstructor) { + string safetyModifier = contextSpec.UseUpdatedMemorySafetyRules ? "safe " : ""; writer.WriteLine($"[{UnsafeAccessorAttributeTypeRef}({UnsafeAccessorKindTypeRef}.Constructor)]"); - writer.WriteLine($"private static extern {typeFQN} {wrapperName}({wrapperParams});"); + writer.WriteLine($"{(useGenericWrapper ? "public" : "private")} static {safetyModifier}extern {typeFQN} {wrapperName}({wrapperParams});"); } else { @@ -1405,15 +1455,47 @@ private static void GenerateConstructorAccessor(SourceWriter writer, TypeGenerat string argTypes = parameters.Count == 0 ? EmptyTypeArray - : $"new global::System.Type[] {{{string.Join(", ", parameters.Select(p => $"typeof({p.ParameterType.FullyQualifiedName})"))}}}"; + : $"new global::System.Type[] {{{string.Join(", ", parameters.Select(p => $"typeof({p.ParameterType.FullyQualifiedName}){(p.RefKind is RefKind.None ? "" : ".MakeByRefType()")}"))}}}"; writer.WriteLine($"private static global::System.Reflection.ConstructorInfo? {cacheName};"); string invokeArgs = parameters.Count == 0 ? "null" - : $"new object?[] {{{string.Join(", ", parameters.Select(p => $"p{p.ParameterIndex}"))}}}"; + : $"new object?[] {{{callArgs}}}"; + string constructorInfo = $"{cacheName} ??= typeof({typeFQN}).GetConstructor(InstanceMemberBindingFlags, binder: null, {argTypes}, modifiers: null)!"; - writer.WriteLine($"private static {typeFQN} {wrapperName}({wrapperParams}) => ({typeFQN})({cacheName} ??= typeof({typeFQN}).GetConstructor(InstanceMemberBindingFlags, binder: null, {argTypes}, modifiers: null)!).Invoke({invokeArgs});"); + if (parameters.Any(p => p.RefKind is RefKind.Ref or RefKind.Out)) + { + writer.WriteLine($$""" + private static {{typeFQN}} {{wrapperName}}({{wrapperParams}}) + { + object?[] args = {{invokeArgs}}; + {{typeFQN}} result = ({{typeFQN}})({{constructorInfo}}).Invoke(args); + """); + writer.Indentation++; + + foreach (ParameterGenerationSpec param in parameters) + { + if (param.RefKind is RefKind.Ref or RefKind.Out) + { + writer.WriteLine($"p{param.ParameterIndex} = ({param.ParameterType.FullyQualifiedName})args[{param.ParameterIndex}]!;"); + } + } + + writer.WriteLine("return result;"); + writer.Indentation--; + writer.WriteLine('}'); + } + else + { + writer.WriteLine($"private static {typeFQN} {wrapperName}({wrapperParams}) => ({typeFQN})({constructorInfo}).Invoke({invokeArgs});"); + } + } + + if (useGenericWrapper) + { + writer.Indentation--; + writer.WriteLine('}'); } } diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index b081434abd08b1..e4f614ff5f055a 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -13,6 +13,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using SourceGenerators; +using GenericTypeMetadata = (string? OpenDeclaringTypeFQN, SourceGenerators.ImmutableEquatableArray? DeclaringTypeParameterNames, string? DeclaringTypeParameterConstraintClauses); namespace System.Text.Json.SourceGeneration { @@ -57,6 +58,7 @@ private sealed class Parser private readonly Queue _typesToGenerate = new(); #pragma warning disable RS1024 // Compare symbols correctly https://github.com/dotnet/roslyn-analyzers/issues/5804 private readonly Dictionary _generatedTypes = new(SymbolEqualityComparer.Default); + private readonly Dictionary _genericTypeDefinitions = new(SymbolEqualityComparer.Default); #pragma warning restore public List Diagnostics { get; } = new(); @@ -99,6 +101,7 @@ public Parser(KnownTypeSymbols knownSymbols) // Ensure context-scoped metadata caches are empty. Debug.Assert(_typesToGenerate.Count == 0); Debug.Assert(_generatedTypes.Count == 0); + Debug.Assert(_genericTypeDefinitions.Count == 0); Debug.Assert(_contextClassLocation is null); INamedTypeSymbol? contextTypeSymbol = semanticModel.GetDeclaredSymbol(contextClassDeclaration, cancellationToken); @@ -178,10 +181,12 @@ public Parser(KnownTypeSymbols knownSymbols) Namespace = contextTypeSymbol.ContainingNamespace is { IsGlobalNamespace: false } ns ? ns.ToDisplayString() : null, ContextClassDeclarations = classDeclarationList.ToImmutableEquatableArray(), GeneratedOptionsSpec = options, + UseUpdatedMemorySafetyRules = semanticModel.UsesUpdatedMemorySafetyRules(), }; // Clear the caches of generated metadata between the processing of context classes. _generatedTypes.Clear(); + _genericTypeDefinitions.Clear(); _typesToGenerate.Clear(); _contextClassLocation = null; return contextGenSpec; @@ -909,9 +914,18 @@ private TypeGenerationSpec ParseTypeGenerationSpec(in TypeToGenerate typeToGener classType = ClassType.TypeUnsupportedBySourceGen; } + bool canUseGenericUnsafeAccessors = type is INamedTypeSymbol { IsGenericType: true } namedConstructorType + && _knownSymbols.SupportsGenericUnsafeAccessors + && !contextType.IsGenericType + && namedConstructorType.ContainingType is not { IsGenericType: true }; + GenericTypeMetadata genericTypeDefinition = GetGenericTypeDefinition(type); + return new TypeGenerationSpec { TypeRef = typeRef, + OpenDeclaringTypeFQN = genericTypeDefinition.OpenDeclaringTypeFQN, + DeclaringTypeParameterNames = genericTypeDefinition.DeclaringTypeParameterNames, + DeclaringTypeParameterConstraintClauses = genericTypeDefinition.DeclaringTypeParameterConstraintClauses, TypeInfoPropertyName = typeInfoPropertyName, GenerationMode = typeToGenerate.Mode ?? options?.GenerationMode ?? JsonSourceGenerationMode.Default, ClassType = classType, @@ -939,8 +953,7 @@ polymorphismOptions is null && ConstructorIsInaccessible = constructorIsInaccessible, CanUseUnsafeAccessorForConstructor = constructorIsInaccessible && _knownSymbols.UnsafeAccessorAttributeType is not null - && (type is not INamedTypeSymbol { IsGenericType: true } - || _knownSymbols.SupportsGenericUnsafeAccessors), + && (type is not INamedTypeSymbol { IsGenericType: true } || canUseGenericUnsafeAccessors), NullableUnderlyingType = nullableUnderlyingType, RuntimeTypeRef = runtimeTypeRef, IsValueTuple = type.IsTupleType, @@ -2204,10 +2217,12 @@ private List ParsePropertyGenerationSpecs( List properties = new(); PropertyHierarchyResolutionState state = new(options); hasExtensionDataProperty = false; + int declaringTypeIndex = -1; // Walk the type hierarchy starting from the current type up to the base type(s) foreach (INamedTypeSymbol currentType in typeToGenerate.Type.GetSortedTypeHierarchy()) { + declaringTypeIndex++; var declaringTypeRef = new TypeRef(currentType); ImmutableArray members = currentType.GetMembers(); @@ -2278,6 +2293,7 @@ void AddMember( PropertyGenerationSpec? propertySpec = ParsePropertyGenerationSpec( contextType, declaringTypeRef, + declaringTypeIndex, typeLocation, memberType, memberInfo, @@ -2421,6 +2437,7 @@ private bool IsValidDataExtensionPropertyType(ITypeSymbol type) private PropertyGenerationSpec? ParsePropertyGenerationSpec( INamedTypeSymbol contextType, TypeRef declaringType, + int declaringTypeIndex, Location? typeLocation, ITypeSymbol memberType, ISymbol memberInfo, @@ -2549,6 +2566,12 @@ private bool IsValidDataExtensionPropertyType(ITypeSymbol type) } } + bool canUseGenericUnsafeAccessors = memberInfo.ContainingType.IsGenericType + && _knownSymbols.SupportsGenericUnsafeAccessors + && !contextType.IsGenericType + && memberInfo.ContainingType.ContainingType is not { IsGenericType: true }; + GenericTypeMetadata genericTypeDefinition = GetGenericTypeDefinition(memberInfo.ContainingType); + return new PropertyGenerationSpec { NameSpecifiedInSourceCode = memberInfo.MemberNameNeedsAtSign() ? "@" + memberInfo.Name : memberInfo.Name, @@ -2572,18 +2595,17 @@ private bool IsValidDataExtensionPropertyType(ITypeSymbol type) HasJsonInclude = hasJsonInclude, CanUseUnsafeAccessors = _knownSymbols.UnsafeAccessorAttributeType is not null && (memberInfo.ContainingType is not INamedTypeSymbol { IsGenericType: true } - || _knownSymbols.SupportsGenericUnsafeAccessors), - OpenDeclaringTypeFQN = memberInfo.ContainingType is INamedTypeSymbol { IsGenericType: true } && _knownSymbols.SupportsGenericUnsafeAccessors - ? memberInfo.ContainingType.OriginalDefinition.GetFullyQualifiedName() : null, - OpenPropertyTypeFQN = memberInfo.ContainingType is INamedTypeSymbol { IsGenericType: true } && _knownSymbols.SupportsGenericUnsafeAccessors + || canUseGenericUnsafeAccessors), + OpenDeclaringTypeFQN = genericTypeDefinition.OpenDeclaringTypeFQN, + OpenPropertyTypeFQN = canUseGenericUnsafeAccessors + && !SymbolEqualityComparer.Default.Equals(memberType, memberInfo.OriginalDefinition.GetMemberType()) ? memberInfo.OriginalDefinition.GetMemberType().GetFullyQualifiedName() : null, - DeclaringTypeParameterNames = memberInfo.ContainingType is INamedTypeSymbol { IsGenericType: true } namedType && _knownSymbols.SupportsGenericUnsafeAccessors - ? namedType.OriginalDefinition.TypeParameters.Select(tp => tp.Name).ToImmutableEquatableArray() : null, - DeclaringTypeParameterConstraintClauses = memberInfo.ContainingType is INamedTypeSymbol { IsGenericType: true } namedType2 && _knownSymbols.SupportsGenericUnsafeAccessors - ? GetTypeParameterConstraintClauses(namedType2.OriginalDefinition) : null, + DeclaringTypeParameterNames = genericTypeDefinition.DeclaringTypeParameterNames, + DeclaringTypeParameterConstraintClauses = genericTypeDefinition.DeclaringTypeParameterConstraintClauses, IsExtensionData = isExtensionData, PropertyType = propertyTypeRef, DeclaringType = declaringType, + DeclaringTypeIndex = declaringTypeIndex, ConverterType = converterType, IsGetterNonNullableAnnotation = isGetterNonNullable, IsSetterNonNullableAnnotation = isSetterNonNullable, @@ -2871,6 +2893,8 @@ private void ProcessMember( constructorParameters[i] = new ParameterGenerationSpec { ParameterType = parameterTypeRef, + OpenParameterTypeFQN = !SymbolEqualityComparer.Default.Equals(parameterInfo.Type, parameterInfo.OriginalDefinition.Type) + ? parameterInfo.OriginalDefinition.Type.GetFullyQualifiedName() : null, Name = parameterInfo.Name, HasDefaultValue = parameterInfo.HasExplicitDefaultValue, DefaultValue = parameterInfo.HasExplicitDefaultValue ? parameterInfo.ExplicitDefaultValue : null, @@ -3406,6 +3430,26 @@ void AddTypeIfNotNull(ITypeSymbol? type) } } + private GenericTypeMetadata GetGenericTypeDefinition(ITypeSymbol type) + { + if (type is not INamedTypeSymbol { IsGenericType: true } namedType) + { + return default; + } + + namedType = namedType.OriginalDefinition; + if (!_genericTypeDefinitions.TryGetValue(namedType, out GenericTypeMetadata result)) + { + result = ( + namedType.GetFullyQualifiedName(), + namedType.TypeParameters.Select(tp => tp.MemberNameNeedsAtSign() ? "@" + tp.Name : tp.Name).ToImmutableEquatableArray(), + GetTypeParameterConstraintClauses(namedType)); + _genericTypeDefinitions.Add(namedType, result); + } + + return result; + } + ///

/// Extracts the type parameter constraint clauses from a generic type using /// Roslyn's . diff --git a/src/libraries/System.Text.Json/gen/Model/ContextGenerationSpec.cs b/src/libraries/System.Text.Json/gen/Model/ContextGenerationSpec.cs index 00c7192c3ae58c..2e535fb3368706 100644 --- a/src/libraries/System.Text.Json/gen/Model/ContextGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/ContextGenerationSpec.cs @@ -36,5 +36,7 @@ public sealed record ContextGenerationSpec public required ImmutableEquatableArray ContextClassDeclarations { get; init; } public required SourceGenerationOptionsSpec? GeneratedOptionsSpec { get; init; } + + public required bool UseUpdatedMemorySafetyRules { get; init; } } } diff --git a/src/libraries/System.Text.Json/gen/Model/ParameterGenerationSpec.cs b/src/libraries/System.Text.Json/gen/Model/ParameterGenerationSpec.cs index 528fcc661016d2..ff65638a6e3a77 100644 --- a/src/libraries/System.Text.Json/gen/Model/ParameterGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/ParameterGenerationSpec.cs @@ -50,5 +50,13 @@ public sealed record ParameterGenerationSpec /// The ref kind of the parameter (None, Ref, Out, In, or RefReadOnlyParameter). /// public required RefKind RefKind { get; init; } + + /// + /// The FQN of the parameter type using the open type parameters described by + /// + /// (e.g., "global::System.Collections.Generic.List<T[]>"). + /// Null when the parameter type does not contain generic parameters. + /// + public string? OpenParameterTypeFQN { get; init; } } } diff --git a/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs b/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs index 5f13cfd26c13b8..475dffed152ed1 100644 --- a/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs @@ -146,13 +146,6 @@ public sealed record PropertyGenerationSpec /// public string? OpenDeclaringTypeFQN { get; init; } - /// - /// When is true and the declaring type is generic, - /// contains the FQN of the property type using open type parameters - /// (e.g., "T" for a generic property, "string" for a concrete one). - /// - public string? OpenPropertyTypeFQN { get; init; } - /// /// The type parameter names of the generic declaring type (e.g., ["T"]). /// Null when the declaring type is not generic. @@ -181,11 +174,27 @@ public sealed record PropertyGenerationSpec /// public required TypeRef DeclaringType { get; init; } + /// + /// The zero-based position of in the inheritance hierarchy + /// of the containing . + /// The contract type comes first, followed by its base types in most-derived-first order. + /// + public required int DeclaringTypeIndex { get; init; } + /// /// Design-time specified custom converter type. /// public required TypeRef? ConverterType { get; init; } + /// + /// When is true and the declaring type is generic, + /// contains the FQN of the property or field type using the open type parameters + /// described by + /// (e.g., "global::System.Collections.Generic.List<T[]>"). + /// Null when the member type does not contain generic parameters. + /// + public string? OpenPropertyTypeFQN { get; init; } + /// /// Determines if the specified property should be included in the fast-path method body. /// diff --git a/src/libraries/System.Text.Json/gen/Model/TypeGenerationSpec.cs b/src/libraries/System.Text.Json/gen/Model/TypeGenerationSpec.cs index 4a4d21c3d30296..ea47bed731588a 100644 --- a/src/libraries/System.Text.Json/gen/Model/TypeGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/TypeGenerationSpec.cs @@ -33,6 +33,25 @@ public sealed record TypeGenerationSpec /// public required TypeRef TypeRef { get; init; } + /// + /// When the type is generic, contains the FQN of the declaring type using open type parameters + /// (e.g., "global::TestApp.MyGenericType<T>"). + /// + public string? OpenDeclaringTypeFQN { get; init; } + + /// + /// The type parameter names of the generic declaring type (e.g., ["T"]). + /// Null when the declaring type is not generic. + /// + public ImmutableEquatableArray? DeclaringTypeParameterNames { get; init; } + + /// + /// The combined type parameter constraint clauses of the generic declaring type + /// (e.g., "where T : notnull, global::MyNamespace.MyBase where U : struct"). + /// Null when the declaring type is not generic or has no constraints. + /// + public string? DeclaringTypeParameterConstraintClauses { get; init; } + /// /// The name of the public JsonTypeInfo<T> property for this type on the generated context class. /// For example, if the context class is named MyJsonContext, and the value of this property is JsonMessage; diff --git a/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.AttributePresence.cs b/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.AttributePresence.cs index 1d224af8e492c9..0a65bf0b015dcf 100644 --- a/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.AttributePresence.cs +++ b/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.AttributePresence.cs @@ -39,6 +39,43 @@ public virtual async Task NonPublicCtor_WithJsonConstructorAttribute_And_Require Assert.Equal("test", result.Name); } + [Theory] + [InlineData(typeof(PrivateParameterizedCtor_WithAttribute), """{"Value":42}""")] + [InlineData(typeof(PrivateParameterizedCtor_WithAttribute), """{"Value":"test"}""")] + [InlineData(typeof(PrivateParameterizedStructCtor_WithAttribute), """{"Value":42}""")] + [InlineData(typeof(PrivateParameterizedStructCtor_WithAttribute), """{"Value":"test"}""")] + [InlineData(typeof(PrivateParameterlessCtor_WithAttribute), """{"X":42,"Value":7}""")] + [InlineData(typeof(PrivateParameterlessCtor_WithAttribute), """{"X":42,"Value":"test"}""")] + [InlineData(typeof(PrivateParameterizedCtor_WithAttribute_FixedParameter), """{"X":42,"Value":7}""")] + [InlineData(typeof(PrivateParameterizedCtor_WithAttribute_FixedParameter), """{"X":42,"Value":"test"}""")] + [InlineData(typeof(PrivateCtorWithMixedParameterTypes), """{"Value":42,"Count":7}""")] + [InlineData(typeof(PrivateCtorWithMixedParameterTypes), """{"Value":"test","Count":7}""")] + [InlineData(typeof(PrivateCtorWithGenericMembers), """{"Value":"constructor","Extra":"member"}""")] + [InlineData(typeof(PrivateCtorWithCompositeGenericMembers), """{"Value":[[1,2],[],[3]],"Extra":[[4],[5,6]]}""")] + [InlineData(typeof(PrivateCtorWithCompositeGenericMembers), """{"Value":[["one","two"],[],["three"]],"Extra":[["four"],["five","six"]]}""")] + [InlineData(typeof(PrivateCtorWithInheritedGenericMembers), """{"Value":42}""")] + [InlineData(typeof(GenericConstructorOuter.Nested), """{"Value":42}""")] + [InlineData(typeof(GenericConstructorOuter.Nested), """{"Value":42,"Item":"nested"}""")] + [InlineData(typeof(GenericConstructorOuter.Middle.Inner), """{"Value":42,"Item":"nested","Extra":17}""")] + [InlineData(typeof(GenericConstructorOuter.Middle.Inner), """{"Value":"outer","Item":42,"Extra":"member"}""")] + public async Task NonPublicGenericCtors_WithJsonConstructorAttribute_WorksAsExpected(Type type, string json) + { + object result = await Serializer.DeserializeWrapper(json, type); + Assert.IsType(type, result); + JsonTestHelper.AssertJsonEqual(json, await Serializer.SerializeWrapper(result, type)); + } + + [Theory] + [InlineData("""{"Value":42,"Name":"test"}""", "default")] + [InlineData("""{"Value":42,"Name":"test","Tag":"updated"}""", "updated")] + public async Task NonPublicGenericCtor_RequiredAndInitOnlyProperties(string json, string expectedTag) + { + var result = await Serializer.DeserializeWrapper>(json); + Assert.Equal(42, result.Value); + Assert.Equal("test", result.Name); + Assert.Equal(expectedTag, result.Tag); + } + [Theory] [InlineData(typeof(PrivateParameterlessCtor_WithAttribute))] [InlineData(typeof(InternalParameterlessCtor_WithAttribute))] diff --git a/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs b/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs index 217a5d7224ed0e..ee119df4c8f5e3 100644 --- a/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs +++ b/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs @@ -1890,6 +1890,34 @@ public class ClassWithRequiredProperty public required string? Bar { get; set; } } + [Theory] + [InlineData(typeof(PrivateCtorWithInParameters), + """{"Value":42,"Label":"test"}""", + """{"Value":42,"Label":"test"}""")] + [InlineData(typeof(PrivateCtorWithByRefParameters), + """{"Value":42,"InValue":17,"ReadOnlyValue":23,"Count":5}""", + """{"Value":42,"InValue":17,"ReadOnlyValue":23,"Count":5,"Output":42}""")] + [InlineData(typeof(PrivateCtorWithByRefParameters), + """{"Value":"ref","InValue":"in","ReadOnlyValue":"readonly","Count":5}""", + """{"Value":"ref","InValue":"in","ReadOnlyValue":"readonly","Count":5,"Output":"ref"}""")] + [InlineData(typeof(PrivateStructCtorWithByRefParameters), + """{"Value":42,"Label":"in","Number":17,"Count":5}""", + """{"Value":42,"Label":"in","Number":17,"Count":5,"Output":43}""")] + [InlineData(typeof(PrivateGenericStructCtorWithByRefParameters), + """{"Value":42,"InValue":17,"ReadOnlyValue":23,"Count":5}""", + """{"Value":42,"InValue":17,"ReadOnlyValue":23,"Count":5,"Output":42}""")] + [InlineData(typeof(PrivateGenericStructCtorWithByRefParameters), + """{"Value":"ref","InValue":"in","ReadOnlyValue":"readonly","Count":5}""", + """{"Value":"ref","InValue":"in","ReadOnlyValue":"readonly","Count":5,"Output":"ref"}""")] + [InlineData(typeof(PrivateCtorWithOutParameters), "{}", """{"Value":42,"Label":"output"}""")] + [InlineData(typeof(PrivateCtorWithOutParameters), """{"Value":99,"Label":"ignored"}""", """{"Value":42,"Label":"output"}""")] + public async Task NonPublicCtors_WithByRefParameters(Type type, string json, string expectedJson) + { + object result = await Serializer.DeserializeWrapper(json, type); + Assert.IsType(type, result); + JsonTestHelper.AssertJsonEqual(expectedJson, await Serializer.SerializeWrapper(result, type)); + } + [Fact] public async Task DeserializeType_WithInParameters() { diff --git a/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.InitOnly.cs b/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.InitOnly.cs index b9c0b51e3b66b5..f8bde1e04ed570 100644 --- a/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.InitOnly.cs +++ b/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.InitOnly.cs @@ -110,6 +110,21 @@ public async Task NullableStructWithInitOnlyProperty() Assert.Equal(deserializedValue, value); } +#if NET + [Fact] + public async Task GenericColor_InitOnlyProperties_CanRoundtrip() + { + const string Json = """{"A":1,"R":0.25,"G":0.5,"B":0.75}"""; + var color = await Serializer.DeserializeWrapper>(Json); + + Assert.Equal(1f, color.A); + Assert.Equal(0.25f, color.R); + Assert.Equal(0.5f, color.G); + Assert.Equal(0.75f, color.B); + JsonTestHelper.AssertJsonEqual(Json, await Serializer.SerializeWrapper(color)); + } +#endif + public class ClassWithInitOnlyProperty { public int MyInt { get; init; } diff --git a/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.NonPublicAccessors.cs b/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.NonPublicAccessors.cs index 0d56f8d2e00513..c98c5a8e1ca7e7 100644 --- a/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.NonPublicAccessors.cs +++ b/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.NonPublicAccessors.cs @@ -701,10 +701,6 @@ public static StructWithJsonIncludePrivateProperties Create(string name, int num internal readonly int GetNumber() => Number; } - /// - /// Generic type with inaccessible [JsonInclude] properties to exercise reflection fallback - /// (UnsafeAccessor does not support generic types). - /// public class GenericClassWithPrivateJsonIncludeProperties { [JsonInclude] @@ -824,7 +820,6 @@ public virtual async Task JsonInclude_StructWithPrivateProperties_CanRoundtrip() [Fact] public virtual async Task JsonInclude_GenericType_PrivateProperties_CanRoundtrip() { - // Generic types use reflection fallback (UnsafeAccessor doesn't support generics). var obj = GenericClassWithPrivateJsonIncludeProperties.Create(42, "test"); string json = await Serializer.SerializeWrapper(obj); Assert.Contains(@"""Value"":42", json); @@ -835,6 +830,288 @@ public virtual async Task JsonInclude_GenericType_PrivateProperties_CanRoundtrip Assert.Equal("test", deserialized.GetLabel()); } + public struct GenericStructWithPrivateJsonIncludeProperties + { + [JsonInclude] + private T Value { get; set; } + + [JsonInclude] + private string Label { get; set; } + } + + [Theory] + [InlineData(typeof(GenericStructWithPrivateJsonIncludeProperties), """{"Value":42,"Label":"test"}""")] + [InlineData(typeof(GenericStructWithPrivateJsonIncludeProperties), """{"Value":"hello","Label":"test"}""")] + public async Task JsonInclude_GenericStruct_PrivateProperties_CanRoundtrip(Type type, string json) + { + object result = await Serializer.DeserializeWrapper(json, type); + JsonTestHelper.AssertJsonEqual(json, await Serializer.SerializeWrapper(result, type)); + } + + public class GenericClassWithReadOnlyJsonIncludeMembers + { + [JsonInclude] + [JsonPropertyName("Value")] + private readonly T _value; + + [JsonInclude] + private string Label { get; } = "initial"; + + public GenericClassWithReadOnlyJsonIncludeMembers() : this(default!) { } + + public GenericClassWithReadOnlyJsonIncludeMembers(T value) => _value = value; + + public T GetValue() => _value; + } + + [Theory] + [InlineData("""{"Value":42}""")] + [InlineData("""{"Label":"changed"}""")] + public async Task JsonInclude_GenericReadOnlyMembers_AreNotDeserialized(string json) + { + var result = await Serializer.DeserializeWrapper>(json); + Assert.Equal(0, result.GetValue()); + JsonTestHelper.AssertJsonEqual("""{"Value":0,"Label":"initial"}""", await Serializer.SerializeWrapper(result)); + } + + public class GenericClassWithPrivateJsonIncludeFields + { + [JsonInclude] + [JsonPropertyName("Value")] + private T _value; + + [JsonInclude] + [JsonPropertyName("Label")] + private string _label; + + public GenericClassWithPrivateJsonIncludeFields() : this(default!, "initial") { } + + public GenericClassWithPrivateJsonIncludeFields(T value, string label) + { + _value = value; + _label = label; + } + + public T GetValue() => _value; + public string GetLabel() => _label; + } + + public struct GenericStructWithPrivateJsonIncludeFields + { + [JsonInclude] + [JsonPropertyName("Value")] + private T _value; + + [JsonInclude] + [JsonPropertyName("Label")] + private string _label; + + public GenericStructWithPrivateJsonIncludeFields(T value, string label) + { + _value = value; + _label = label; + } + + public readonly T GetValue() => _value; + public readonly string GetLabel() => _label; + } + + [Theory] + [InlineData(typeof(GenericClassWithPrivateJsonIncludeFields), """{"Value":42,"Label":"test"}""")] + [InlineData(typeof(GenericClassWithPrivateJsonIncludeFields), """{"Value":"hello","Label":"test"}""")] + [InlineData(typeof(GenericStructWithPrivateJsonIncludeFields), """{"Value":42,"Label":"test"}""")] + [InlineData(typeof(GenericStructWithPrivateJsonIncludeFields), """{"Value":"hello","Label":"test"}""")] + public async Task JsonInclude_GenericFields_CanRoundtrip(Type type, string json) + { + object result = await Serializer.DeserializeWrapper(json, type); + JsonTestHelper.AssertJsonEqual(json, await Serializer.SerializeWrapper(result, type)); + } + + public struct StructWithPrivateConstructorAndMembers + { + public int Value { get; } + + [JsonInclude, JsonPropertyName("Field")] + private int _field; + + [JsonInclude, JsonPropertyName("ReadOnly")] + private readonly int _readOnly; + + [JsonInclude] + private string Property { get; set; } + + [JsonInclude] + private string InitOnly { get; init; } + + [JsonConstructor] + private StructWithPrivateConstructorAndMembers(int value) + { + Value = value; + _field = -1; + _readOnly = 23; + Property = "initial"; + InitOnly = "initial"; + } + + public readonly int GetField() => _field; + public readonly int GetReadOnly() => _readOnly; + } + + public struct GenericStructWithPrivateInitOnlyMembers + { + public T Value { get; } + + [JsonInclude, JsonPropertyName("ReadOnly")] + private readonly T _readOnly; + + [JsonInclude] + private T InitOnly { get; init; } + + [JsonConstructor] + private GenericStructWithPrivateInitOnlyMembers(T value) + { + Value = value; + _readOnly = value; + InitOnly = default!; + } + } + + [Theory] + [InlineData(typeof(StructWithPrivateConstructorAndMembers), + """{"Value":42}""", + """{"Value":42,"Field":-1,"ReadOnly":23,"Property":"initial","InitOnly":"initial"}""")] + [InlineData(typeof(StructWithPrivateConstructorAndMembers), + """{"Value":42,"Field":17,"ReadOnly":99,"Property":"updated","InitOnly":"initialized"}""", + """{"Value":42,"Field":17,"ReadOnly":23,"Property":"updated","InitOnly":"initialized"}""")] + [InlineData(typeof(GenericStructWithPrivateInitOnlyMembers), + """{"Value":42,"ReadOnly":99,"InitOnly":17}""", + """{"Value":42,"ReadOnly":42,"InitOnly":17}""")] + [InlineData(typeof(GenericStructWithPrivateInitOnlyMembers), + """{"Value":"constructor","ReadOnly":"ignored","InitOnly":"initialized"}""", + """{"Value":"constructor","ReadOnly":"constructor","InitOnly":"initialized"}""")] + public async Task JsonInclude_Struct_PrivateConstructorAndMembers_CanRoundtrip(Type type, string json, string expectedJson) + { + object result = await Serializer.DeserializeWrapper(json, type); + Assert.IsType(type, result); + JsonTestHelper.AssertJsonEqual(expectedJson, await Serializer.SerializeWrapper(result, type)); + } + + public class GenericMemberOuter + { + public class Nested + { + [JsonInclude] + private TOuter OuterValue { get; set; } + + public TInner InnerValue { get; init; } + } + + public class Nested + { + [JsonInclude] + private TOuter Value { get; set; } + } + } + + public class GenericAccessorBase + { + [JsonInclude] + [JsonPropertyName("BaseValue")] + private T Value { get; set; } + } + + public class GenericAccessorDerived : GenericAccessorBase + { + [JsonInclude] + [JsonPropertyName("DerivedValue")] + private T Value { get; set; } + } + + public class NonGenericAccessorDerived : GenericAccessorBase { } + + public class GenericAccessorGrandBase where TGrand : class + { + [JsonInclude] + private TGrand GrandSecret { get; set; } = default!; + } + + public class GenericAccessorIntermediate : GenericAccessorGrandBase where TBase : struct + { + [JsonInclude] + private TBase BaseSecret { get; set; } + } + + public class NonGenericAccessorLeaf : GenericAccessorIntermediate { } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task JsonInclude_MultipleGenericBaseTypes_CanRoundtrip(bool deserialize) + { + const string PopulatedJson = """{"BaseSecret":42,"GrandSecret":"secret"}"""; + const string DefaultJson = """{"BaseSecret":0,"GrandSecret":null}"""; + + NonGenericAccessorLeaf value = deserialize + ? await Serializer.DeserializeWrapper(PopulatedJson) + : new NonGenericAccessorLeaf(); + + JsonTestHelper.AssertJsonEqual( + deserialize ? PopulatedJson : DefaultJson, + await Serializer.SerializeWrapper(value)); + } + + public class GenericAccessorDerivedWithPublicProperty : GenericAccessorBase + { + public T DerivedValue { get; set; } + } + + public class GenericAccessorDerivedWithIgnoredProperty : GenericAccessorBase + { + [JsonIgnore] + public T IgnoredValue { get; init; } + } + + public class GenericAccessorDerived : GenericAccessorBase + { + [JsonInclude] + [JsonPropertyName("DerivedValue")] + private TSecond Value { get; set; } + } + + public class GenericMembersWithKeywordParameter<@class> where @class : struct + { + [JsonInclude] + private @class Value { get; set; } + + public @class Other { get; init; } + } + + public class ConstrainedGenericAccessor + where TValue : struct + where TCollection : ICollection, new() + { + [JsonInclude] + private TValue Value { get; set; } + + public TCollection Values { get; init; } = new(); + } + + [Theory] + [InlineData(typeof(GenericMemberOuter.Nested), """{"OuterValue":42,"InnerValue":"test"}""")] + [InlineData(typeof(GenericMemberOuter.Nested), """{"Value":42}""")] + [InlineData(typeof(GenericAccessorDerived), """{"BaseValue":1,"DerivedValue":2}""")] + [InlineData(typeof(NonGenericAccessorDerived), """{"BaseValue":42}""")] + [InlineData(typeof(GenericAccessorDerivedWithPublicProperty), """{"BaseValue":1,"DerivedValue":2}""")] + [InlineData(typeof(GenericAccessorDerivedWithIgnoredProperty), """{"BaseValue":42}""")] + [InlineData(typeof(GenericAccessorDerived), """{"BaseValue":42,"DerivedValue":"test"}""")] + [InlineData(typeof(GenericMembersWithKeywordParameter), """{"Value":42,"Other":7}""")] + [InlineData(typeof(ConstrainedGenericAccessor>), """{"Value":42,"Values":[1,2]}""")] + public async Task JsonInclude_GenericNestedAndInheritedMembers_CanRoundtrip(Type type, string json) + { + object result = await Serializer.DeserializeWrapper(json, type); + JsonTestHelper.AssertJsonEqual(json, await Serializer.SerializeWrapper(result, type)); + } + public class ConstraintBase { public int Id { get; set; } @@ -865,5 +1142,31 @@ public virtual async Task InitOnlyProperties_GenericTypeWithConstraints_CanRound Assert.Equal(1, deserialized.Response.Id); Assert.Equal("extra", deserialized.Response.Extra); } + + public class ConstrainedGenericClassWithOptionalInitOnlyProperties where T : notnull, ConstraintBase + { + public T? Response { get; init; } + public string Name { get; init; } = "default"; + } + + [Fact] + public async Task InitOnlyProperties_ConstrainedGenericType_PreservesDefaults() + { + var value = new ConstrainedGenericClassWithOptionalInitOnlyProperties + { + Response = new ConstraintDerived { Id = 2, Extra = "optional" }, + Name = "updated" + }; + + string json = await Serializer.SerializeWrapper(value); + var deserialized = await Serializer.DeserializeWrapper>(json); + Assert.Equal(2, deserialized.Response.Id); + Assert.Equal("optional", deserialized.Response.Extra); + Assert.Equal("updated", deserialized.Name); + + deserialized = await Serializer.DeserializeWrapper>("{}"); + Assert.Null(deserialized.Response); + Assert.Equal("default", deserialized.Name); + } } } diff --git a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Constructor.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Constructor.cs index 232106f9d52ee7..6db32dde838f3c 100644 --- a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Constructor.cs +++ b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Constructor.cs @@ -44,6 +44,233 @@ public class PrivateParameterizedCtor_WithAttribute_And_RequiredProperty private PrivateParameterizedCtor_WithAttribute_And_RequiredProperty(int x) => X = x; } + public class PrivateParameterizedCtor_WithAttribute + { + public T Value { get; } + + [JsonConstructor] + private PrivateParameterizedCtor_WithAttribute(T value) => Value = value; + } + + public struct PrivateParameterizedStructCtor_WithAttribute + { + public T Value { get; } + + [JsonConstructor] + private PrivateParameterizedStructCtor_WithAttribute(T value) => Value = value; + } + + public class PrivateParameterlessCtor_WithAttribute + { + public int X { get; } + public T Value { get; set; } + + [JsonConstructor] + private PrivateParameterlessCtor_WithAttribute() => X = 42; + } + + public class PrivateParameterizedCtor_WithAttribute_FixedParameter + { + public int X { get; } + public T Value { get; set; } + + [JsonConstructor] + private PrivateParameterizedCtor_WithAttribute_FixedParameter(int x) => X = x; + } + + public class PrivateParameterizedCtor_WithAttribute_And_RequiredProperty + { + public T Value { get; } + public required string Name { get; set; } + public string Tag { get; init; } = "default"; + + [JsonConstructor] + private PrivateParameterizedCtor_WithAttribute_And_RequiredProperty(T value) => Value = value; + } + + public class PrivateCtorWithMixedParameterTypes + { + public T Value { get; } + public int Count { get; } + + [JsonConstructor] + private PrivateCtorWithMixedParameterTypes(T value, int count) + { + Value = value; + Count = count; + } + } + + public class PrivateCtorWithInParameters + { + public int Value { get; } + public string Label { get; } + + [JsonConstructor] + private PrivateCtorWithInParameters(in int value, ref readonly string label) + { + Value = value; + Label = label; + } + + private PrivateCtorWithInParameters(int value, string label) => + throw new InvalidOperationException("Incorrect constructor."); + } + + public class PrivateCtorWithByRefParameters + { + public T Value { get; } + public T InValue { get; } + public T ReadOnlyValue { get; } + public int Count { get; } + public T Output { get; } + + [JsonConstructor] + private PrivateCtorWithByRefParameters(out T output, ref T value, in T inValue, ref readonly T readOnlyValue, int count) + { + Output = output = value; + Value = value; + value = default!; + InValue = inValue; + ReadOnlyValue = readOnlyValue; + Count = count; + } + + private PrivateCtorWithByRefParameters(T output, T value, T inValue, T readOnlyValue, int count) => + throw new InvalidOperationException("Incorrect constructor."); + } + + public struct PrivateStructCtorWithByRefParameters + { + public int Value { get; } + public string Label { get; } + public int Number { get; } + public int Count { get; } + public int Output { get; } + + [JsonConstructor] + private PrivateStructCtorWithByRefParameters(out int output, ref int value, in string label, ref readonly int number, int count) + { + Output = output = value + 1; + Value = value; + value = -1; + Label = label; + Number = number; + Count = count; + } + } + + public struct PrivateGenericStructCtorWithByRefParameters + { + public T Value { get; } + public T InValue { get; } + public T ReadOnlyValue { get; } + public int Count { get; } + public T Output { get; } + + [JsonConstructor] + private PrivateGenericStructCtorWithByRefParameters(out T output, ref T value, in T inValue, ref readonly T readOnlyValue, int count) + { + Output = output = value; + Value = value; + value = default!; + InValue = inValue; + ReadOnlyValue = readOnlyValue; + Count = count; + } + } + + public class PrivateCtorWithOutParameters + { + public int Value { get; } + public string Label { get; } + + [JsonConstructor] + private PrivateCtorWithOutParameters(out int value, out string label) + { + Value = value = 42; + Label = label = "output"; + } + } + + public class PrivateCtorWithGenericMembers where T : class + { + public T Value { get; } + + [JsonInclude] + private T? Extra { get; set; } + + [JsonConstructor] + private PrivateCtorWithGenericMembers(T value) => Value = value; + } + + public class PrivateCtorWithCompositeGenericMembers + { + public List Value { get; } + + [JsonInclude] + private List Extra { get; set; } = new(); + + [JsonConstructor] + private PrivateCtorWithCompositeGenericMembers(List value) => Value = value; + } + + public class GenericConstructorBase + { + [JsonInclude] + public T Value { get; protected set; } + } + + public class PrivateCtorWithInheritedGenericMembers : GenericConstructorBase + { + [JsonConstructor] + private PrivateCtorWithInheritedGenericMembers(T value) => Value = value; + } + + public class GenericConstructorOuter + { + public class Middle + { + public class Inner + { + public T Value { get; } + public TItem Item { get; } + + [JsonInclude] + private T Extra { get; set; } + + [JsonConstructor] + private Inner(T value, TItem item) + { + Value = value; + Item = item; + Extra = default!; + } + } + } + + public class Nested + { + public T Value { get; } + + [JsonConstructor] + private Nested(T value) => Value = value; + } + + public class Nested + { + public T Value { get; } + public TItem Item { get; } + + [JsonConstructor] + private Nested(T value, TItem item) + { + Value = value; + Item = item; + } + } + } + public class InternalParameterlessCtor { internal InternalParameterlessCtor() { } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.TestLibrary/System.Text.Json.TestLibrary.targets b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.TestLibrary/System.Text.Json.TestLibrary.targets index f4b8bd9d504e63..584580d756d4d4 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.TestLibrary/System.Text.Json.TestLibrary.targets +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.TestLibrary/System.Text.Json.TestLibrary.targets @@ -1,6 +1,7 @@ netstandard2.0 + $(Features);updated-memory-safety-rules diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs index 7debd36c1ea8c5..188df5a166201b 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.cs @@ -608,11 +608,14 @@ public static void CombiningContextWithCustomResolver_ReplacePoco() JsonTypeInfo typeInfo = JsonTypeInfo.CreateJsonTypeInfo(options); typeInfo.CreateObject = () => new TestPoco(); JsonPropertyInfo property = typeInfo.CreateJsonPropertyInfo(typeof(string), "test"); - property.Get = (o) => System.Runtime.CompilerServices.Unsafe.Unbox(o).IntProperty.ToString(); + property.Get = (o) => unsafe(System.Runtime.CompilerServices.Unsafe.Unbox(o)).IntProperty.ToString(); property.Set = (o, val) => { - System.Runtime.CompilerServices.Unsafe.Unbox(o).StringProperty = (string)val; - System.Runtime.CompilerServices.Unsafe.Unbox(o).IntProperty = int.Parse((string)val); + unsafe + { + System.Runtime.CompilerServices.Unsafe.Unbox(o).StringProperty = (string)val; + System.Runtime.CompilerServices.Unsafe.Unbox(o).IntProperty = int.Parse((string)val); + } }; typeInfo.Properties.Add(property); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs index 1f1726d2f5fe43..d6cbf400f2b844 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs @@ -40,6 +40,25 @@ protected ConstructorTests_Metadata(JsonSerializerWrapper stringWrapper) [JsonSerializable(typeof(ProtectedParameterizedCtor))] [JsonSerializable(typeof(PrivateParameterizedCtor_WithAttribute))] [JsonSerializable(typeof(PrivateParameterizedCtor_WithAttribute_And_RequiredProperty))] + [JsonSerializable(typeof(PrivateParameterizedCtor_WithAttribute))] + [JsonSerializable(typeof(PrivateParameterizedCtor_WithAttribute))] + [JsonSerializable(typeof(PrivateParameterizedStructCtor_WithAttribute))] + [JsonSerializable(typeof(PrivateParameterizedStructCtor_WithAttribute))] + [JsonSerializable(typeof(PrivateParameterlessCtor_WithAttribute))] + [JsonSerializable(typeof(PrivateParameterlessCtor_WithAttribute))] + [JsonSerializable(typeof(PrivateParameterizedCtor_WithAttribute_FixedParameter))] + [JsonSerializable(typeof(PrivateParameterizedCtor_WithAttribute_FixedParameter))] + [JsonSerializable(typeof(PrivateParameterizedCtor_WithAttribute_And_RequiredProperty))] + [JsonSerializable(typeof(PrivateCtorWithMixedParameterTypes))] + [JsonSerializable(typeof(PrivateCtorWithMixedParameterTypes))] + [JsonSerializable(typeof(PrivateCtorWithGenericMembers))] + [JsonSerializable(typeof(PrivateCtorWithCompositeGenericMembers))] + [JsonSerializable(typeof(PrivateCtorWithCompositeGenericMembers))] + [JsonSerializable(typeof(PrivateCtorWithInheritedGenericMembers))] + [JsonSerializable(typeof(GenericConstructorOuter.Nested))] + [JsonSerializable(typeof(GenericConstructorOuter.Nested))] + [JsonSerializable(typeof(GenericConstructorOuter.Middle.Inner))] + [JsonSerializable(typeof(GenericConstructorOuter.Middle.Inner))] [JsonSerializable(typeof(InternalParameterizedCtor_WithAttribute))] [JsonSerializable(typeof(ProtectedParameterizedCtor_WithAttribute))] [JsonSerializable(typeof(PrivateParameterlessCtor_WithAttribute))] @@ -177,6 +196,13 @@ protected ConstructorTests_Metadata(JsonSerializerWrapper stringWrapper) [JsonSerializable(typeof(TypeWith_RefReadonlyParameter_Primitive))] [JsonSerializable(typeof(TypeWith_RefReadonlyParameter_Struct))] [JsonSerializable(typeof(TypeWith_RefReadonlyParameter_ReferenceType))] + [JsonSerializable(typeof(PrivateCtorWithInParameters))] + [JsonSerializable(typeof(PrivateCtorWithByRefParameters))] + [JsonSerializable(typeof(PrivateCtorWithByRefParameters))] + [JsonSerializable(typeof(PrivateStructCtorWithByRefParameters))] + [JsonSerializable(typeof(PrivateGenericStructCtorWithByRefParameters))] + [JsonSerializable(typeof(PrivateGenericStructCtorWithByRefParameters))] + [JsonSerializable(typeof(PrivateCtorWithOutParameters))] [JsonSerializable(typeof(ParameterizedClass_WithStringProperty))] [JsonSerializable(typeof(ParameterizedRecord_WithStringProperty))] internal sealed partial class ConstructorTestsContext_Metadata : JsonSerializerContext @@ -214,6 +240,25 @@ public ConstructorTests_Default(JsonSerializerWrapper jsonSerializer) : base(jso [JsonSerializable(typeof(ProtectedParameterizedCtor))] [JsonSerializable(typeof(PrivateParameterizedCtor_WithAttribute))] [JsonSerializable(typeof(PrivateParameterizedCtor_WithAttribute_And_RequiredProperty))] + [JsonSerializable(typeof(PrivateParameterizedCtor_WithAttribute))] + [JsonSerializable(typeof(PrivateParameterizedCtor_WithAttribute))] + [JsonSerializable(typeof(PrivateParameterizedStructCtor_WithAttribute))] + [JsonSerializable(typeof(PrivateParameterizedStructCtor_WithAttribute))] + [JsonSerializable(typeof(PrivateParameterlessCtor_WithAttribute))] + [JsonSerializable(typeof(PrivateParameterlessCtor_WithAttribute))] + [JsonSerializable(typeof(PrivateParameterizedCtor_WithAttribute_FixedParameter))] + [JsonSerializable(typeof(PrivateParameterizedCtor_WithAttribute_FixedParameter))] + [JsonSerializable(typeof(PrivateParameterizedCtor_WithAttribute_And_RequiredProperty))] + [JsonSerializable(typeof(PrivateCtorWithMixedParameterTypes))] + [JsonSerializable(typeof(PrivateCtorWithMixedParameterTypes))] + [JsonSerializable(typeof(PrivateCtorWithGenericMembers))] + [JsonSerializable(typeof(PrivateCtorWithCompositeGenericMembers))] + [JsonSerializable(typeof(PrivateCtorWithCompositeGenericMembers))] + [JsonSerializable(typeof(PrivateCtorWithInheritedGenericMembers))] + [JsonSerializable(typeof(GenericConstructorOuter.Nested))] + [JsonSerializable(typeof(GenericConstructorOuter.Nested))] + [JsonSerializable(typeof(GenericConstructorOuter.Middle.Inner))] + [JsonSerializable(typeof(GenericConstructorOuter.Middle.Inner))] [JsonSerializable(typeof(InternalParameterizedCtor_WithAttribute))] [JsonSerializable(typeof(ProtectedParameterizedCtor_WithAttribute))] [JsonSerializable(typeof(PrivateParameterlessCtor_WithAttribute))] @@ -351,6 +396,13 @@ public ConstructorTests_Default(JsonSerializerWrapper jsonSerializer) : base(jso [JsonSerializable(typeof(TypeWith_RefReadonlyParameter_Primitive))] [JsonSerializable(typeof(TypeWith_RefReadonlyParameter_Struct))] [JsonSerializable(typeof(TypeWith_RefReadonlyParameter_ReferenceType))] + [JsonSerializable(typeof(PrivateCtorWithInParameters))] + [JsonSerializable(typeof(PrivateCtorWithByRefParameters))] + [JsonSerializable(typeof(PrivateCtorWithByRefParameters))] + [JsonSerializable(typeof(PrivateStructCtorWithByRefParameters))] + [JsonSerializable(typeof(PrivateGenericStructCtorWithByRefParameters))] + [JsonSerializable(typeof(PrivateGenericStructCtorWithByRefParameters))] + [JsonSerializable(typeof(PrivateCtorWithOutParameters))] [JsonSerializable(typeof(ParameterizedClass_WithStringProperty))] [JsonSerializable(typeof(ParameterizedRecord_WithStringProperty))] internal sealed partial class ConstructorTestsContext_Default : JsonSerializerContext diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyVisibilityTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyVisibilityTests.cs index bc836b1dbd4aed..7c6952cf6efac5 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyVisibilityTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyVisibilityTests.cs @@ -99,6 +99,47 @@ public override async Task JsonIgnoreCondition_TypeLevel_Always_ThrowsInvalidOpe Assert.Contains(@"""MyInt"":42", json); } + [Theory] + [InlineData(JsonSourceGenerationMode.Metadata, false)] + [InlineData(JsonSourceGenerationMode.Metadata, true)] + [InlineData(JsonSourceGenerationMode.Default, false)] + [InlineData(JsonSourceGenerationMode.Default, true)] + public static void GenericContexts_InaccessibleGenericMembers_CanRoundtrip(JsonSourceGenerationMode mode, bool nested) + { + JsonSerializerContext context = (mode, nested) switch + { + (JsonSourceGenerationMode.Metadata, false) => GenericAccessorContext_Metadata.Default, + (JsonSourceGenerationMode.Metadata, true) => GenericAccessorContextContainer.NestedGenericAccessorContext_Metadata.Default, + (JsonSourceGenerationMode.Default, false) => GenericAccessorContext_Default.Default, + _ => GenericAccessorContextContainer.NestedGenericAccessorContext_Default.Default, + }; + + const string Json = """{"Value":42,"Label":"test"}"""; + var result = (GenericClassWithPrivateJsonIncludeProperties)JsonSerializer.Deserialize( + Json, typeof(GenericClassWithPrivateJsonIncludeProperties), context); + Assert.Equal(42, result.GetValue()); + Assert.Equal("test", result.GetLabel()); + JsonTestHelper.AssertJsonEqual(Json, JsonSerializer.Serialize( + result, typeof(GenericClassWithPrivateJsonIncludeProperties), context)); + } + + [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(GenericClassWithPrivateJsonIncludeProperties))] + private partial class GenericAccessorContext_Metadata : JsonSerializerContext { } + + [JsonSerializable(typeof(GenericClassWithPrivateJsonIncludeProperties))] + private partial class GenericAccessorContext_Default : JsonSerializerContext { } + + private partial class GenericAccessorContextContainer + { + [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(GenericClassWithPrivateJsonIncludeProperties))] + public partial class NestedGenericAccessorContext_Metadata : JsonSerializerContext { } + + [JsonSerializable(typeof(GenericClassWithPrivateJsonIncludeProperties))] + public partial class NestedGenericAccessorContext_Default : JsonSerializerContext { } + } + [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(ClassWithNewSlotField))] [JsonSerializable(typeof(int))] @@ -263,7 +304,28 @@ public override async Task JsonIgnoreCondition_TypeLevel_Always_ThrowsInvalidOpe [JsonSerializable(typeof(ClassWithJsonIncludePrivateGetterProperties))] [JsonSerializable(typeof(StructWithJsonIncludePrivateProperties))] [JsonSerializable(typeof(GenericClassWithPrivateJsonIncludeProperties))] + [JsonSerializable(typeof(GenericStructWithPrivateJsonIncludeProperties))] + [JsonSerializable(typeof(GenericStructWithPrivateJsonIncludeProperties))] + [JsonSerializable(typeof(GenericClassWithReadOnlyJsonIncludeMembers))] + [JsonSerializable(typeof(GenericClassWithPrivateJsonIncludeFields))] + [JsonSerializable(typeof(GenericClassWithPrivateJsonIncludeFields))] + [JsonSerializable(typeof(GenericStructWithPrivateJsonIncludeFields))] + [JsonSerializable(typeof(GenericStructWithPrivateJsonIncludeFields))] + [JsonSerializable(typeof(StructWithPrivateConstructorAndMembers))] + [JsonSerializable(typeof(GenericStructWithPrivateInitOnlyMembers))] + [JsonSerializable(typeof(GenericStructWithPrivateInitOnlyMembers))] + [JsonSerializable(typeof(GenericMemberOuter.Nested))] + [JsonSerializable(typeof(GenericMemberOuter.Nested))] + [JsonSerializable(typeof(GenericAccessorDerived))] + [JsonSerializable(typeof(NonGenericAccessorDerived))] + [JsonSerializable(typeof(NonGenericAccessorLeaf))] + [JsonSerializable(typeof(GenericAccessorDerivedWithPublicProperty))] + [JsonSerializable(typeof(GenericAccessorDerivedWithIgnoredProperty))] + [JsonSerializable(typeof(GenericAccessorDerived))] + [JsonSerializable(typeof(GenericMembersWithKeywordParameter))] + [JsonSerializable(typeof(ConstrainedGenericAccessor>))] [JsonSerializable(typeof(ConstrainedGenericClassWithInitOnlyProperties))] + [JsonSerializable(typeof(ConstrainedGenericClassWithOptionalInitOnlyProperties))] [JsonSerializable(typeof(ClassWithInitOnlyPropertyDefaults))] [JsonSerializable(typeof(StructWithInitOnlyPropertyDefaults))] [JsonSerializable(typeof(ClassUsingIgnoreWhenWritingDefaultAttribute))] @@ -286,6 +348,9 @@ public override async Task JsonIgnoreCondition_TypeLevel_Always_ThrowsInvalidOpe [JsonSerializable(typeof(Class2))] [JsonSerializable(typeof(NamespaceBase.Class1), TypeInfoPropertyName = "Class1FromNamespaceBase")] [JsonSerializable(typeof(NamespaceBase.Class2), TypeInfoPropertyName = "Class2FromNamespaceBase")] +#if NET + [JsonSerializable(typeof(System.Numerics.Colors.Argb))] +#endif internal sealed partial class PropertyVisibilityTestsContext_Metadata : JsonSerializerContext { } @@ -558,7 +623,28 @@ partial class DefaultContextWithGlobalIgnoreSetting : JsonSerializerContext; [JsonSerializable(typeof(ClassWithJsonIncludePrivateGetterProperties))] [JsonSerializable(typeof(StructWithJsonIncludePrivateProperties))] [JsonSerializable(typeof(GenericClassWithPrivateJsonIncludeProperties))] + [JsonSerializable(typeof(GenericStructWithPrivateJsonIncludeProperties))] + [JsonSerializable(typeof(GenericStructWithPrivateJsonIncludeProperties))] + [JsonSerializable(typeof(GenericClassWithReadOnlyJsonIncludeMembers))] + [JsonSerializable(typeof(GenericClassWithPrivateJsonIncludeFields))] + [JsonSerializable(typeof(GenericClassWithPrivateJsonIncludeFields))] + [JsonSerializable(typeof(GenericStructWithPrivateJsonIncludeFields))] + [JsonSerializable(typeof(GenericStructWithPrivateJsonIncludeFields))] + [JsonSerializable(typeof(StructWithPrivateConstructorAndMembers))] + [JsonSerializable(typeof(GenericStructWithPrivateInitOnlyMembers))] + [JsonSerializable(typeof(GenericStructWithPrivateInitOnlyMembers))] + [JsonSerializable(typeof(GenericMemberOuter.Nested))] + [JsonSerializable(typeof(GenericMemberOuter.Nested))] + [JsonSerializable(typeof(GenericAccessorDerived))] + [JsonSerializable(typeof(NonGenericAccessorDerived))] + [JsonSerializable(typeof(NonGenericAccessorLeaf))] + [JsonSerializable(typeof(GenericAccessorDerivedWithPublicProperty))] + [JsonSerializable(typeof(GenericAccessorDerivedWithIgnoredProperty))] + [JsonSerializable(typeof(GenericAccessorDerived))] + [JsonSerializable(typeof(GenericMembersWithKeywordParameter))] + [JsonSerializable(typeof(ConstrainedGenericAccessor>))] [JsonSerializable(typeof(ConstrainedGenericClassWithInitOnlyProperties))] + [JsonSerializable(typeof(ConstrainedGenericClassWithOptionalInitOnlyProperties))] [JsonSerializable(typeof(ClassWithInitOnlyPropertyDefaults))] [JsonSerializable(typeof(StructWithInitOnlyPropertyDefaults))] [JsonSerializable(typeof(ClassUsingIgnoreWhenWritingDefaultAttribute))] @@ -581,6 +667,9 @@ partial class DefaultContextWithGlobalIgnoreSetting : JsonSerializerContext; [JsonSerializable(typeof(Class2))] [JsonSerializable(typeof(NamespaceBase.Class1), TypeInfoPropertyName = "Class1FromNamespaceBase")] [JsonSerializable(typeof(NamespaceBase.Class2), TypeInfoPropertyName = "Class2FromNamespaceBase")] +#if NET + [JsonSerializable(typeof(System.Numerics.Colors.Argb))] +#endif internal sealed partial class PropertyVisibilityTestsContext_Default : JsonSerializerContext { } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets index 75afc6000505c8..fc27dad3c45e48 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets @@ -1,6 +1,8 @@ $(NetCoreAppCurrent);$(NetFrameworkCurrent) + $(Features);updated-memory-safety-rules + true true true diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/UnsafeAccessors_GenericTypeInitOnlyProperties/netcoreapp/MyContext.GenericPropsInt32.g.cs.txt b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/UnsafeAccessors_GenericTypeInitOnlyProperties/netcoreapp/MyContext.GenericPropsInt32.g.cs.txt index dbf4848f12de37..c420a27b3bfeb6 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/UnsafeAccessors_GenericTypeInitOnlyProperties/netcoreapp/MyContext.GenericPropsInt32.g.cs.txt +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/Baselines/UnsafeAccessors_GenericTypeInitOnlyProperties/netcoreapp/MyContext.GenericPropsInt32.g.cs.txt @@ -58,7 +58,7 @@ namespace TestApp DeclaringType = typeof(global::TestApp.GenericProps), Converter = null, Getter = static obj => ((global::TestApp.GenericProps)obj).Value, - Setter = static (obj, value) => __GenericAccessors_GenericPropsInt32.__set_GenericPropsInt32_Value((global::TestApp.GenericProps)obj, value!), + Setter = static (obj, value) => __GenericAccessors_GenericPropsInt32_0.__set_GenericPropsInt32_Value((global::TestApp.GenericProps)obj, value!), IgnoreCondition = null, HasJsonInclude = false, IsExtensionData = false, @@ -78,7 +78,7 @@ namespace TestApp DeclaringType = typeof(global::TestApp.GenericProps), Converter = null, Getter = static obj => ((global::TestApp.GenericProps)obj).Label, - Setter = static (obj, value) => __GenericAccessors_GenericPropsInt32.__set_GenericPropsInt32_Label((global::TestApp.GenericProps)obj, value!), + Setter = static (obj, value) => __GenericAccessors_GenericPropsInt32_0.__set_GenericPropsInt32_Label((global::TestApp.GenericProps)obj, value!), IgnoreCondition = null, HasJsonInclude = false, IsExtensionData = false, @@ -112,7 +112,7 @@ namespace TestApp } - private static class __GenericAccessors_GenericPropsInt32 + private static partial class __GenericAccessors_GenericPropsInt32_0 { [global::System.Runtime.CompilerServices.UnsafeAccessorAttribute(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_Value")] public static extern void __set_GenericPropsInt32_Value(global::TestApp.GenericProps obj, T value); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/System.Text.Json.SourceGeneration.Unit.Tests.targets b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/System.Text.Json.SourceGeneration.Unit.Tests.targets index 1c425a78879baa..dd8a1663c5956a 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/System.Text.Json.SourceGeneration.Unit.Tests.targets +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/System.Text.Json.SourceGeneration.Unit.Tests.targets @@ -1,6 +1,7 @@ $(NetCoreAppCurrent);$(NetFrameworkCurrent) + $(Features);updated-memory-safety-rules true true diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonEncodedTextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonEncodedTextTests.cs index 8ea5600314b2e7..440829536b6bb1 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonEncodedTextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonEncodedTextTests.cs @@ -502,7 +502,7 @@ public override bool WillEncode(int unicodeScalar) public unsafe override int FindFirstCharacterToEncode(char* text, int textLength) { - return Default.FindFirstCharacterToEncode(text, textLength); + return unsafe(Default.FindFirstCharacterToEncode(text, textLength)); } @@ -516,7 +516,7 @@ public override int MaxOutputCharactersPerInputCharacter public unsafe override bool TryEncodeUnicodeScalar(int unicodeScalar, char* buffer, int bufferLength, out int numberOfCharactersWritten) { - return Default.TryEncodeUnicodeScalar(unicodeScalar, buffer, bufferLength, out numberOfCharactersWritten); + return unsafe(Default.TryEncodeUnicodeScalar(unicodeScalar, buffer, bufferLength, out numberOfCharactersWritten)); } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj index fa32ef081daf11..61b3ca3d0a19e6 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj @@ -21,7 +21,7 @@ Enable reflection and preserve required assemblies --> true - $(Features.Replace('nullablePublicOnly', '')) + $(Features.Replace('nullablePublicOnly', ''));updated-memory-safety-rules false false diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.Values.StringSegment.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.Values.StringSegment.cs index 6c716bc68090f3..c6b729fe97ea37 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.Values.StringSegment.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.Values.StringSegment.cs @@ -1319,13 +1319,13 @@ private static void WriteStringValueHelper(Utf8JsonWriter writer, ReadOnlySpa switch (encoding) { case StringValueEncodingType.Utf16: - writer.WriteStringValue(MemoryMarshal.Cast(value)); + writer.WriteStringValue(unsafe(MemoryMarshal.Cast(value))); break; case StringValueEncodingType.Utf8: - writer.WriteStringValue(MemoryMarshal.Cast(value)); + writer.WriteStringValue(unsafe(MemoryMarshal.Cast(value))); break; case StringValueEncodingType.Base64: - writer.WriteBase64StringValue(MemoryMarshal.Cast(value)); + writer.WriteBase64StringValue(unsafe(MemoryMarshal.Cast(value))); break; default: Assert.Fail($"Encoding {encoding} not valid."); @@ -1341,13 +1341,13 @@ private static void WriteStringValueSegmentHelper(Utf8JsonWriter writer, Read switch (encoding) { case StringValueEncodingType.Utf16: - writer.WriteStringValueSegment(MemoryMarshal.Cast(value), isFinal); + writer.WriteStringValueSegment(unsafe(MemoryMarshal.Cast(value)), isFinal); break; case StringValueEncodingType.Utf8: - writer.WriteStringValueSegment(MemoryMarshal.Cast(value), isFinal); + writer.WriteStringValueSegment(unsafe(MemoryMarshal.Cast(value)), isFinal); break; case StringValueEncodingType.Base64: - writer.WriteBase64StringSegment(MemoryMarshal.Cast(value), isFinal); + writer.WriteBase64StringSegment(unsafe(MemoryMarshal.Cast(value)), isFinal); break; default: Assert.Fail($"Encoding {encoding} not valid."); @@ -1363,13 +1363,13 @@ private static void WriteStringValueSegmentsHelper(Utf8JsonWriter writer, Rea switch (encoding) { case StringValueEncodingType.Utf16: - writer.WriteStringValueSegment(MemoryMarshal.Cast(value), true); + writer.WriteStringValueSegment(unsafe(MemoryMarshal.Cast(value)), true); break; case StringValueEncodingType.Utf8: - writer.WriteStringValueSegment(MemoryMarshal.Cast(value), true); + writer.WriteStringValueSegment(unsafe(MemoryMarshal.Cast(value)), true); break; case StringValueEncodingType.Base64: - writer.WriteBase64StringSegment(MemoryMarshal.Cast(value), true); + writer.WriteBase64StringSegment(unsafe(MemoryMarshal.Cast(value)), true); break; default: Assert.Fail($"Encoding {encoding} not valid."); @@ -1386,16 +1386,16 @@ private static void WriteStringValueSegmentsHelper(Utf8JsonWriter writer, Rea switch (encoding) { case StringValueEncodingType.Utf16: - writer.WriteStringValueSegment(MemoryMarshal.Cast(value1), false); - writer.WriteStringValueSegment(MemoryMarshal.Cast(value2), true); + writer.WriteStringValueSegment(unsafe(MemoryMarshal.Cast(value1)), false); + writer.WriteStringValueSegment(unsafe(MemoryMarshal.Cast(value2)), true); break; case StringValueEncodingType.Utf8: - writer.WriteStringValueSegment(MemoryMarshal.Cast(value1), false); - writer.WriteStringValueSegment(MemoryMarshal.Cast(value2), true); + writer.WriteStringValueSegment(unsafe(MemoryMarshal.Cast(value1)), false); + writer.WriteStringValueSegment(unsafe(MemoryMarshal.Cast(value2)), true); break; case StringValueEncodingType.Base64: - writer.WriteBase64StringSegment(MemoryMarshal.Cast(value1), false); - writer.WriteBase64StringSegment(MemoryMarshal.Cast(value2), true); + writer.WriteBase64StringSegment(unsafe(MemoryMarshal.Cast(value1)), false); + writer.WriteBase64StringSegment(unsafe(MemoryMarshal.Cast(value2)), true); break; default: Assert.Fail($"Encoding {encoding} not valid."); @@ -1412,19 +1412,19 @@ private static void WriteStringValueSegmentsHelper(Utf8JsonWriter writer, Rea switch (encoding) { case StringValueEncodingType.Utf16: - writer.WriteStringValueSegment(MemoryMarshal.Cast(value1), false); - writer.WriteStringValueSegment(MemoryMarshal.Cast(value2), false); - writer.WriteStringValueSegment(MemoryMarshal.Cast(value3), true); + writer.WriteStringValueSegment(unsafe(MemoryMarshal.Cast(value1)), false); + writer.WriteStringValueSegment(unsafe(MemoryMarshal.Cast(value2)), false); + writer.WriteStringValueSegment(unsafe(MemoryMarshal.Cast(value3)), true); break; case StringValueEncodingType.Utf8: - writer.WriteStringValueSegment(MemoryMarshal.Cast(value1), false); - writer.WriteStringValueSegment(MemoryMarshal.Cast(value2), false); - writer.WriteStringValueSegment(MemoryMarshal.Cast(value3), true); + writer.WriteStringValueSegment(unsafe(MemoryMarshal.Cast(value1)), false); + writer.WriteStringValueSegment(unsafe(MemoryMarshal.Cast(value2)), false); + writer.WriteStringValueSegment(unsafe(MemoryMarshal.Cast(value3)), true); break; case StringValueEncodingType.Base64: - writer.WriteBase64StringSegment(MemoryMarshal.Cast(value1), false); - writer.WriteBase64StringSegment(MemoryMarshal.Cast(value2), false); - writer.WriteBase64StringSegment(MemoryMarshal.Cast(value3), true); + writer.WriteBase64StringSegment(unsafe(MemoryMarshal.Cast(value1)), false); + writer.WriteBase64StringSegment(unsafe(MemoryMarshal.Cast(value2)), false); + writer.WriteBase64StringSegment(unsafe(MemoryMarshal.Cast(value3)), true); break; default: Assert.Fail($"Encoding {encoding} not valid."); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs index fa7e2a38e0347f..a7800cb043a255 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs @@ -152,7 +152,7 @@ public class BuggyJavaScriptEncoder : JavaScriptEncoder public override unsafe int FindFirstCharacterToEncode(char* text, int textLength) { // Access the text pointer even though it might be null and text length is 0. - return *text; + return unsafe(*text); } public override unsafe bool TryEncodeUnicodeScalar(int unicodeScalar, char* buffer, int bufferLength, out int numberOfCharactersWritten)