Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,15 @@ public KnownTypeSymbols(Compilation compilation)
public INamedTypeSymbol? SetsRequiredMembersAttributeType => GetOrResolveType("System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute", ref _SetsRequiredMembersAttributeType);
private Option<INamedTypeSymbol?> _SetsRequiredMembersAttributeType;

public INamedTypeSymbol? UnsafeAccessorAttributeType => GetOrResolveType("System.Runtime.CompilerServices.UnsafeAccessorAttribute", ref _UnsafeAccessorAttributeType);
private Option<INamedTypeSymbol?> _UnsafeAccessorAttributeType;

// OverloadResolutionPriorityAttribute was added in .NET 9; its presence indicates
// the runtime also supports generic type parameters in UnsafeAccessor.
public bool SupportsGenericUnsafeAccessors => UnsafeAccessorAttributeType is not null
&& GetOrResolveType("System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute", ref _OverloadResolutionPriorityAttributeType) is not null;
Comment thread
eiriktsarpalis marked this conversation as resolved.
private Option<INamedTypeSymbol?> _OverloadResolutionPriorityAttributeType;

public INamedTypeSymbol? JsonStringEnumConverterType => GetOrResolveType("System.Text.Json.Serialization.JsonStringEnumConverter", ref _JsonStringEnumConverterType);
private Option<INamedTypeSymbol?> _JsonStringEnumConverterType;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,6 @@ internal static class DiagnosticDescriptors
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static DiagnosticDescriptor JsonConstructorInaccessible { get; } = DiagnosticDescriptorHelper.Create(
id: "SYSLIB1222",
title: new LocalizableResourceString(nameof(SR.JsonConstructorInaccessibleTitle), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)),
messageFormat: new LocalizableResourceString(nameof(SR.JsonConstructorInaccessibleMessageFormat), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)),
category: JsonConstants.SystemTextJsonSourceGenerationName,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

public static DiagnosticDescriptor DerivedJsonConverterAttributesNotSupported { get; } = DiagnosticDescriptorHelper.Create(
id: "SYSLIB1223",
title: new LocalizableResourceString(nameof(SR.DerivedJsonConverterAttributesNotSupportedTitle), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,9 @@ private sealed partial class Emitter
/// </summary>
private static class ExceptionMessages
{
public const string InaccessibleJsonIncludePropertiesNotSupported =
"The member '{0}.{1}' has been annotated with the JsonIncludeAttribute but is not visible to the source generator.";

public const string IncompatibleConverterType =
"The converter '{0}' is not compatible with the type '{1}'.";

public const string InitOnlyPropertySetterNotSupported =
"Setting init-only properties is not supported in source generation mode.";

public const string InvalidJsonConverterFactoryOutput =
"The converter '{0}' cannot return null or a JsonConverterFactory instance.";

Expand Down
599 changes: 570 additions & 29 deletions src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs

Large diffs are not rendered by default.

57 changes: 38 additions & 19 deletions src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,7 @@ private TypeGenerationSpec ParseTypeGenerationSpec(in TypeToGenerate typeToGener
List<int>? fastPathPropertyIndices = null;
ObjectConstructionStrategy constructionStrategy = default;
bool constructorSetsRequiredMembers = false;
bool constructorIsInaccessible = false;
ParameterGenerationSpec[]? ctorParamSpecs = null;
List<PropertyInitializerGenerationSpec>? propertyInitializerSpecs = null;
CollectionType collectionType = CollectionType.NotApplicable;
Expand Down Expand Up @@ -681,11 +682,8 @@ private TypeGenerationSpec ParseTypeGenerationSpec(in TypeToGenerate typeToGener
{
ReportDiagnostic(DiagnosticDescriptors.MultipleJsonConstructorAttribute, typeToGenerate.Location, type.ToDisplayString());
}
else if (constructor != null && !IsSymbolAccessibleWithin(constructor, within: contextType))
{
ReportDiagnostic(DiagnosticDescriptors.JsonConstructorInaccessible, typeToGenerate.Location, type.ToDisplayString());
constructor = null;
}

constructorIsInaccessible = constructor is not null && !IsSymbolAccessibleWithin(constructor, within: contextType);

classType = ClassType.Object;

Expand All @@ -694,7 +692,11 @@ private TypeGenerationSpec ParseTypeGenerationSpec(in TypeToGenerate typeToGener

ctorParamSpecs = ParseConstructorParameters(typeToGenerate, constructor, out constructionStrategy, out constructorSetsRequiredMembers);
propertySpecs = ParsePropertyGenerationSpecs(contextType, typeToGenerate, typeIgnoreCondition, options, typeNamingPolicy, out hasExtensionDataProperty, out fastPathPropertyIndices);
propertyInitializerSpecs = ParsePropertyInitializers(ctorParamSpecs, propertySpecs, constructorSetsRequiredMembers, ref constructionStrategy);

if (!constructorIsInaccessible)
{
propertyInitializerSpecs = ParsePropertyInitializers(ctorParamSpecs, propertySpecs, constructorSetsRequiredMembers, ref constructionStrategy);
}
}

var typeRef = new TypeRef(type);
Expand Down Expand Up @@ -733,6 +735,11 @@ private TypeGenerationSpec ParseTypeGenerationSpec(in TypeToGenerate typeToGener
CollectionValueType = collectionValueType,
ConstructionStrategy = constructionStrategy,
ConstructorSetsRequiredParameters = constructorSetsRequiredMembers,
ConstructorIsInaccessible = constructorIsInaccessible,
CanUseUnsafeAccessorForConstructor = constructorIsInaccessible
&& _knownSymbols.UnsafeAccessorAttributeType is not null
&& (type is not INamedTypeSymbol { IsGenericType: true }
|| _knownSymbols.SupportsGenericUnsafeAccessors),
NullableUnderlyingType = nullableUnderlyingType,
RuntimeTypeRef = runtimeTypeRef,
IsValueTuple = type.IsTupleType,
Expand Down Expand Up @@ -1120,13 +1127,11 @@ void AddMember(
AddPropertyWithConflictResolution(propertySpec, memberInfo, propertyIndex: properties.Count, ref state);
properties.Add(propertySpec);

// In case of JsonInclude fail if either:
// 1. the getter is not accessible by the source generator or
// 2. neither getter or setter methods are public.
if (propertySpec.HasJsonInclude && (!propertySpec.CanUseGetter || !propertySpec.IsPublic))
{
state.HasInvalidConfigurationForFastPath = true;
}
// Note: ParsePropertyGenerationSpec intentionally does not mark inaccessible
// [JsonInclude] members as invalid for fast-path generation. Some callers rely
// on that omission to source-generate against experimental APIs without
// introducing new warnings until https://github.com/dotnet/runtime/issues/124889
// is completed.
}

bool PropertyIsOverriddenAndIgnored(IPropertySymbol property, Dictionary<string, ISymbol>? ignoredMembers)
Expand Down Expand Up @@ -1379,7 +1384,19 @@ private bool IsValidDataExtensionPropertyType(ITypeSymbol type)
NumberHandling = numberHandling,
ObjectCreationHandling = objectCreationHandling,
Order = order,
HasJsonInclude = hasJsonInclude,
// TODO: remove the inaccessibility check once https://github.com/dotnet/runtime/issues/124889
// is complete; some callers currently rely on this omission when source-generating
// against experimental APIs (tracking: https://github.com/dotnet/runtime/issues/88519).
HasJsonInclude = hasJsonInclude && !hasJsonIncludeButIsInaccessible,
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
? memberInfo.OriginalDefinition.GetMemberType().GetFullyQualifiedName() : null,
DeclaringTypeParameterNames = memberInfo.ContainingType is INamedTypeSymbol { IsGenericType: true } namedType && _knownSymbols.SupportsGenericUnsafeAccessors
? namedType.OriginalDefinition.TypeParameters.Select(tp => tp.Name).ToImmutableEquatableArray() : null,
IsExtensionData = isExtensionData,
PropertyType = propertyTypeRef,
DeclaringType = declaringType,
Expand Down Expand Up @@ -1694,13 +1711,15 @@ private void ProcessMember(
return null;
}

HashSet<string>? memberInitializerNames = null;
HashSet<string>? requiredMemberNames = null;
List<PropertyInitializerGenerationSpec>? propertyInitializers = null;

// Count non-out constructor parameters - out params don't have entries in the args array.
int paramCount = constructorParameters?.Count(p => p.RefKind != RefKind.Out) ?? 0;

// Determine potential init-only or required properties that need to be part of the constructor delegate signature.
// Determine required properties that need to be part of the constructor delegate signature.
// Init-only non-required properties are no longer included here -- they will be set
// via UnsafeAccessor or reflection post-construction to preserve their default values.
foreach (PropertyGenerationSpec property in properties)
{
if (!property.CanUseSetter)
Expand All @@ -1713,11 +1732,11 @@ private void ProcessMember(
continue;
}

if ((property.IsRequired && !constructorSetsRequiredMembers) || property.IsInitOnlySetter)
if (property.IsRequired && !constructorSetsRequiredMembers)
Comment thread
eiriktsarpalis marked this conversation as resolved.
{
if (!(memberInitializerNames ??= new()).Add(property.MemberName))
if (!(requiredMemberNames ??= new()).Add(property.MemberName))
{
// We've already added another member initializer with the same name to our spec list.
// We've already added another required member with the same name to our spec list.
// Duplicates can occur here because the provided list of properties includes shadowed members.
// This is because we generate metadata for *all* members, including shadowed or ignored ones,
// since we need to re-run the deduplication algorithm taking run-time configuration into account.
Expand Down
32 changes: 30 additions & 2 deletions src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,33 @@ public sealed record PropertyGenerationSpec
/// </summary>
public required bool HasJsonInclude { get; init; }

/// <summary>
/// Whether the property can use UnsafeAccessor for its getter/setter.
/// This is true for non-generic types on .NET 8+ and for generic types on .NET 9+
/// (using a generic wrapper class). False when UnsafeAccessorAttribute is not available.
/// </summary>
public required bool CanUseUnsafeAccessors { get; init; }

/// <summary>
/// When <see cref="CanUseUnsafeAccessors"/> is true and the declaring type is generic,
/// contains the FQN of the declaring type using open type parameters
/// (e.g., "global::TestApp.MyGenericType&lt;T&gt;").
/// </summary>
public string? OpenDeclaringTypeFQN { get; init; }

/// <summary>
/// When <see cref="CanUseUnsafeAccessors"/> 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).
/// </summary>
public string? OpenPropertyTypeFQN { get; init; }

/// <summary>
/// The type parameter names of the generic declaring type (e.g., ["T"]).
/// Null when the declaring type is not generic.
/// </summary>
public ImmutableEquatableArray<string>? DeclaringTypeParameterNames { get; init; }

/// <summary>
/// Whether the property has the JsonExtensionDataAttribute.
/// </summary>
Expand Down Expand Up @@ -163,8 +190,9 @@ public bool ShouldIncludePropertyForFastPath(ContextGenerationSpec contextSpec)
return false;
}

// Discard properties without getters
if (!CanUseGetter)
// Discard properties without getters, unless they have [JsonInclude]
// in which case we use UnsafeAccessor or reflection to read the value.
if (!CanUseGetter && !HasJsonInclude)
{
return false;
}
Expand Down
13 changes: 13 additions & 0 deletions src/libraries/System.Text.Json/gen/Model/TypeGenerationSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,19 @@ public sealed record TypeGenerationSpec

public required bool ConstructorSetsRequiredParameters { get; init; }

/// <summary>
/// Whether the deserialization constructor is inaccessible from the generated context.
/// When true, UnsafeAccessor or reflection is used to invoke the constructor.
/// </summary>
public required bool ConstructorIsInaccessible { get; init; }

/// <summary>
/// Whether UnsafeAccessors can be used for the constructor.
/// This is true for non-generic types on .NET 8+ and for generic types on .NET 9+
/// (using a generic wrapper class). False when <c>UnsafeAccessorAttribute</c> is not available.
/// </summary>
public required bool CanUseUnsafeAccessorForConstructor { get; init; }

public required TypeRef? NullableUnderlyingType { get; init; }

/// <summary>
Expand Down
12 changes: 0 additions & 12 deletions src/libraries/System.Text.Json/gen/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,6 @@
<data name="DataExtensionPropertyInvalidTitle" xml:space="preserve">
<value>Data extension property type invalid.</value>
</data>
<data name="InitOnlyPropertyDeserializationNotSupportedTitle" xml:space="preserve">
<value>Deserialization of init-only properties is currently not supported in source generation mode.</value>
</data>
<data name="InitOnlyPropertyDeserializationNotSupportedFormat" xml:space="preserve">
<value>The type '{0}' defines init-only properties, deserialization of which is currently not supported in source generation mode.</value>
</data>
<data name="InaccessibleJsonIncludePropertiesNotSupportedTitle" xml:space="preserve">
<value>Inaccessible properties annotated with the JsonIncludeAttribute are not supported in source generation mode.</value>
</data>
Expand Down Expand Up @@ -195,12 +189,6 @@
<data name="JsonUnsupportedLanguageVersionMessageFormat" xml:space="preserve">
<value>The System.Text.Json source generator is not available in C# {0}. Please use language version {1} or greater.</value>
</data>
<data name="JsonConstructorInaccessibleTitle" xml:space="preserve">
<value>Constructor annotated with JsonConstructorAttribute is inaccessible.</value>
</data>
<data name="JsonConstructorInaccessibleMessageFormat" xml:space="preserve">
<value>The constructor on type '{0}' has been annotated with JsonConstructorAttribute but is not accessible by the source generator.</value>
</data>
<data name="JsonSerializableAttributeOnNonContextTypeTitle" xml:space="preserve">
<value>Types annotated with JsonSerializableAttribute must be classes deriving from JsonSerializerContext.</value>
</data>
Expand Down
24 changes: 2 additions & 22 deletions src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -54,32 +54,12 @@
</trans-unit>
<trans-unit id="InaccessibleJsonIncludePropertiesNotSupportedFormat">
<source>The member '{0}.{1}' has been annotated with the JsonIncludeAttribute but is not visible to the source generator.</source>
<target state="translated">Člen {0}.{1} má anotaci od JsonIncludeAttribute, ale není pro zdrojový generátor viditelný.</target>
<target state="new">The member '{0}.{1}' has been annotated with the JsonIncludeAttribute but is not visible to the source generator.</target>
<note />
</trans-unit>
<trans-unit id="InaccessibleJsonIncludePropertiesNotSupportedTitle">
<source>Inaccessible properties annotated with the JsonIncludeAttribute are not supported in source generation mode.</source>
<target state="translated">Nepřístupné vlastnosti anotované s JsonIncludeAttribute se v režimu generování zdroje nepodporují.</target>
<note />
</trans-unit>
<trans-unit id="InitOnlyPropertyDeserializationNotSupportedFormat">
<source>The type '{0}' defines init-only properties, deserialization of which is currently not supported in source generation mode.</source>
<target state="translated">Typ {0} definuje vlastnosti pouze pro inicializaci, jejichž deserializace se v režimu generování zdroje v současnosti nepodporuje.</target>
<note />
</trans-unit>
<trans-unit id="InitOnlyPropertyDeserializationNotSupportedTitle">
<source>Deserialization of init-only properties is currently not supported in source generation mode.</source>
<target state="translated">Deserializace vlastností pouze pro inicializaci se v současnosti v režimu generování zdroje nepodporuje.</target>
<note />
</trans-unit>
<trans-unit id="JsonConstructorInaccessibleMessageFormat">
<source>The constructor on type '{0}' has been annotated with JsonConstructorAttribute but is not accessible by the source generator.</source>
<target state="translated">Konstruktor typu {0} byl opatřen poznámkou s atributem JsonConstructorAttribute, ale zdrojový generátor k němu nemá přístup.</target>
<note />
</trans-unit>
<trans-unit id="JsonConstructorInaccessibleTitle">
<source>Constructor annotated with JsonConstructorAttribute is inaccessible.</source>
<target state="translated">Konstruktor anotovaný atributem JsonConstructorAttribute je nepřístupný.</target>
<target state="new">Inaccessible properties annotated with the JsonIncludeAttribute are not supported in source generation mode.</target>
<note />
</trans-unit>
<trans-unit id="JsonConverterAttributeInvalidTypeMessageFormat">
Expand Down
Loading
Loading