Skip to content

[STJ Source Gen] Propagate type parameter constraints to generic UnsafeAccessor wrapper classes#126506

Closed
steveisok wants to merge 1 commit intodotnet:mainfrom
steveisok:fix/stj-sourcegen-generic-constraints
Closed

[STJ Source Gen] Propagate type parameter constraints to generic UnsafeAccessor wrapper classes#126506
steveisok wants to merge 1 commit intodotnet:mainfrom
steveisok:fix/stj-sourcegen-generic-constraints

Conversation

@steveisok
Copy link
Copy Markdown
Member

Description

Fixes the regression introduced in #124650 where the STJ source generator emits generic wrapper classes for [UnsafeAccessor] methods without propagating the type parameter constraints from the declaring type.

The Problem

When a generic type has constrained type parameters (e.g., PublicKeyCredential<TResponse> where TResponse : notnull, AuthenticatorResponse) and init-only properties, the source generator emits a wrapper class like:

private static class __GenericAccessors_PublicKeyCredential<TResponse>
// Missing: where TResponse : notnull, AuthenticatorResponse
{
    [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_Id")]
    private static extern void __set_Id(PublicKeyCredential<TResponse> obj, BufferSource value);
    //                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //                                  CS0314: TResponse doesn't satisfy constraint
}

This causes CS0314 compilation errors. Reported by @akoeplinger in dotnet/dotnet#5880 (comment) — aspnetcore's IdentityJsonSerializerContext uses PublicKeyCredential<TResponse> where TResponse : notnull, AuthenticatorResponse with init-only properties.

The Fix

Three changes across the source generator:

  1. Model (PropertyGenerationSpec): Added DeclaringTypeParameterConstraints property to carry the constraint clauses (e.g., ["where TResponse : notnull, global::Microsoft.AspNetCore.Identity.AuthenticatorResponse"]).

  2. Parser: Added GetTypeParameterConstraints() helper that reads ITypeParameterSymbol properties (HasNotNullConstraint, HasValueTypeConstraint, HasReferenceTypeConstraint, HasUnmanagedTypeConstraint, HasConstructorConstraint, ConstraintTypes) and builds the corresponding where clauses.

  3. Emitter: Emits the constraint clauses on the generated wrapper class:

private static class __GenericAccessors_PublicKeyCredential<TResponse>
    where TResponse : notnull, global::Microsoft.AspNetCore.Identity.AuthenticatorResponse
{
    // UnsafeAccessor externs now compile correctly
}

cc @eiriktsarpalis @stephentoub

…feAccessor wrapper classes

The source generator emits a generic wrapper class for UnsafeAccessor
methods when the declaring type is generic. However, it only forwarded
the type parameter names without their constraints.

This causes CS0314 compilation errors when the declaring type has
constrained type parameters (e.g., `where TResponse : AuthenticatorResponse`),
because the wrapper class's unconstrained type parameters cannot satisfy
the original type's constraints.

Fix: collect the constraint clauses from each ITypeParameterSymbol in the
parser and emit matching `where` clauses on the generated wrapper class.

Fixes compilation of aspnetcore's IdentityJsonSerializerContext which uses
PublicKeyCredential<TResponse> where TResponse : notnull, AuthenticatorResponse.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@steveisok steveisok added area-System.Text.Json source-generator Indicates an issue with a source generator feature labels Apr 3, 2026
Copilot AI review requested due to automatic review settings April 3, 2026 15:18
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-text-json
See info in area-owners.md if you want to be subscribed.

@steveisok
Copy link
Copy Markdown
Member Author

@eiriktsarpalis you beat me to it. Closing in favor of #126507

@steveisok steveisok closed this Apr 3, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a regression in the System.Text.Json source generator where generic [UnsafeAccessor] wrapper classes for members on generic declaring types were emitted without propagating the declaring type’s generic type-parameter constraints, leading to compilation errors (e.g., CS0314).

Changes:

  • Extend PropertyGenerationSpec to carry declaring type generic constraint clauses.
  • Parse ITypeParameterSymbol constraints into where ... clauses during spec creation.
  • Emit collected where ... constraints on generated __GenericAccessors_*<...> wrapper classes.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs Adds a model field to carry declaring-type generic constraint clauses to emission.
src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs Builds where clauses from Roslyn type-parameter constraint metadata and stores them on the spec.
src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs Emits the stored generic constraint clauses on the generated generic UnsafeAccessor wrapper class.

}
else if (tp.HasReferenceTypeConstraint)
{
(parts ??= new()).Add("class");
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetTypeParameterConstraints emits "class" whenever HasReferenceTypeConstraint is true, but it doesn’t account for nullable reference type constraints ("class?") via ITypeParameterSymbol.ReferenceTypeConstraintNullableAnnotation. Emitting "class" for an original "class?" constraint is stricter and can cause compilation failures when the declaring type uses nullable reference type constraints. Consider emitting "class?" when the nullable annotation is Annotated (and keeping "class" otherwise).

Suggested change
(parts ??= new()).Add("class");
(parts ??= new()).Add(tp.ReferenceTypeConstraintNullableAnnotation == NullableAnnotation.Annotated ? "class?" : "class");

Copilot uses AI. Check for mistakes.
Comment on lines +1856 to +1866
/// <summary>
/// Builds the set of "where T : ..." constraint clauses for the given type parameters.
/// Returns null when no type parameter has any constraint.
/// </summary>
private static ImmutableEquatableArray<string>? GetTypeParameterConstraints(
ImmutableArray<ITypeParameterSymbol> typeParameters)
{
List<string>? clauses = null;

foreach (ITypeParameterSymbol tp in typeParameters)
{
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetTypeParameterConstraints doesn’t propagate the C# 13 "allows ref struct" constraint (Roslyn ITypeParameterSymbol.AllowsRefLikeType). If a declaring type has this constraint, the generated wrapper class must repeat it; otherwise ref-like type arguments won’t be permitted and the generated code can fail to compile. Consider adding support for this constraint (likely via reflection like other generators in the repo do to keep compatibility across Roslyn versions).

Copilot uses AI. Check for mistakes.
Comment on lines 1398 to +1401
DeclaringTypeParameterNames = memberInfo.ContainingType is INamedTypeSymbol { IsGenericType: true } namedType && _knownSymbols.SupportsGenericUnsafeAccessors
? namedType.OriginalDefinition.TypeParameters.Select(tp => tp.Name).ToImmutableEquatableArray() : null,
DeclaringTypeParameterConstraints = memberInfo.ContainingType is INamedTypeSymbol { IsGenericType: true } namedType2 && _knownSymbols.SupportsGenericUnsafeAccessors
? GetTypeParameterConstraints(namedType2.OriginalDefinition.TypeParameters) : null,
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DeclaringTypeParameterConstraints is built from namedType2.OriginalDefinition.TypeParameters, which only includes type parameters declared on the immediate type (and excludes containing type parameters for nested generic types). For nested generics, this will omit outer constraints and can still produce CS0314/constraint-mismatch errors in the generated _GenericAccessors* wrapper. Consider gathering type parameters (and constraints) from the full containing-type chain (similar to how GetTotalTypeParameterCount handles nested generics).

Copilot uses AI. Check for mistakes.
Comment on lines 1037 to +1047
writer.WriteLine($"private static class __GenericAccessors_{typeFriendlyName}<{typeParamList}>");

if (firstProperty.DeclaringTypeParameterConstraints is { } constraints)
{
writer.Indentation++;
foreach (string constraint in constraints)
{
writer.WriteLine(constraint);
}
writer.Indentation--;
}
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change introduces new emitted syntax (generic wrapper type parameter constraint clauses) but there isn’t a unit/baseline test exercising a constrained generic declaring type (e.g., where T : notnull, SomeBase or class?). Adding a source-gen output test for constrained generic init-only properties would help prevent regressions like #124650 and validate the exact emitted where clauses.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.Text.Json source-generator Indicates an issue with a source generator feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants