Skip to content

Fix STJ source gen missing type parameter constraints on generic accessor wrappers#126507

Merged
lewing merged 4 commits intomainfrom
investigate/stj-sg-errors
Apr 3, 2026
Merged

Fix STJ source gen missing type parameter constraints on generic accessor wrappers#126507
lewing merged 4 commits intomainfrom
investigate/stj-sg-errors

Conversation

@eiriktsarpalis
Copy link
Copy Markdown
Member

Note

This PR was generated with the assistance of GitHub Copilot.

Description

Fixes the aspnetcore build failure reported in dotnet/dotnet#5880 (comment), caused by #124650.

When the STJ source generator emits UnsafeAccessor wrapper classes for generic types (e.g., __GenericAccessors_MyType<T>), it was not propagating type parameter constraints from the original type definition. This caused CS0314 compilation errors in downstream consumers (aspnetcore) when the type parameter had constraints like where TResponse : notnull, AuthenticatorResponse.

Before (broken)

private static class __GenericAccessors_...<TResponse>  // missing constraint!
{
    [UnsafeAccessor(...)]
    public static extern ... __get_...(PublicKeyCredential<TResponse> obj);  // CS0314
}

After (fixed)

private static class __GenericAccessors_...<TResponse> where TResponse : notnull, global::...AuthenticatorResponse
{
    [UnsafeAccessor(...)]
    public static extern ... __get_...(PublicKeyCredential<TResponse> obj);  // OK
}

Changes

  • PropertyGenerationSpec.cs — Added DeclaringTypeParameterConstraintClauses property to store constraint clauses
  • JsonSourceGenerator.Parser.cs — Added GetTypeParameterConstraintClause() and GetTypeParameterConstraintsCombined() to extract all constraint kinds (struct, class, notnull, unmanaged, base types, interfaces, new()) from ITypeParameterSymbol
  • JsonSourceGenerator.Emitter.cs — Appends constraint clauses to the generic wrapper class declaration

Tests

  • Unit test: GenericTypeWithConstrainedTypeParameters_InitOnlyProperties — verifies source gen compiles for constrained generic types
  • Runtime test: InitOnlyProperties_GenericTypeWithConstraints_CanRoundtrip — verifies serialization/deserialization end-to-end for a generic type with where T : notnull, BaseClass constraints
  • All 194 source gen unit tests pass
  • All 378 PropertyVisibility runtime tests pass

…ssor wrappers

When the source generator emits UnsafeAccessor wrapper classes for
generic types (e.g., __GenericAccessors_MyType<T>), it was not
propagating type parameter constraints from the original type. This
caused CS0314 compilation errors when the type parameter had
constraints like 'where T : notnull, BaseClass'.

The fix extracts constraint clauses from ITypeParameterSymbol in the
parser and appends them to the emitted generic wrapper class
declaration.

Fixes the aspnetcore build failure for PublicKeyCredential<TResponse>
where TResponse : notnull, AuthenticatorResponse.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@eiriktsarpalis eiriktsarpalis 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:19
@eiriktsarpalis eiriktsarpalis added area-System.Text.Json source-generator Indicates an issue with a source generator feature labels Apr 3, 2026
@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.

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 System.Text.Json source generator output for generic UnsafeAccessor wrapper classes by propagating generic type parameter constraints, preventing downstream compilation failures (e.g., CS0314) when the declaring type has constrained type parameters.

Changes:

  • Capture generic declaring-type constraint clauses during parsing and store them on PropertyGenerationSpec.
  • Emit captured constraint clauses on generated __GenericAccessors_...<T...> wrapper classes.
  • Add unit + runtime test coverage for constrained generic types with init-only properties.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs Adds a new spec property to carry combined declaring-type constraint clauses into emission.
src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs Extracts and formats generic type parameter constraint clauses from ITypeParameterSymbol.
src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs Appends extracted constraint clauses to the generated generic accessor wrapper class declaration.
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs Adds a unit test ensuring generated code compiles for constrained generic types with init-only properties.
src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.NonPublicAccessors.cs Adds constrained-generic test types and a roundtrip test validating runtime serialization/deserialization behavior.
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyVisibilityTests.cs Registers the new constrained generic type in source-gen contexts so it participates in source generation tests.

@github-actions

This comment has been minimized.

eiriktsarpalis and others added 2 commits April 3, 2026 18:46
- Handle AllowsRefLikeType via reflection (Roslyn 4.9+) for the
  'allows ref struct' anti-constraint
- Add Theory test covering notnull, class, class+new(), struct,
  unmanaged, and interface constraints
- Add tests for multi-parameter constraints and base class+interface

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 3, 2026 15:51
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

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

@github-actions

This comment has been minimized.

@agocke
Copy link
Copy Markdown
Member

agocke commented Apr 3, 2026

Do we want to put this behind some sort of opt-in? The feature is useful, but generally the use of unsafe code should come with some sort of user acknowledgment

@eiriktsarpalis
Copy link
Copy Markdown
Member Author

Do we want to put this behind some sort of opt-in? The feature is useful, but generally the use of unsafe code should come with some sort of user acknowledgment

It should be already. Only members explicit opted in via annotation (or setters of init-only properties) will prompt unsafe accessor generation.

…aints

Replace manual constraint clause reconstruction with Roslyn's built-in
SymbolDisplayFormat.IncludeTypeConstraints, which renders all constraint
clauses automatically. This is future-proof against new C# constraint
kinds (like 'allows ref struct') without requiring manual updates or
reflection-based accessors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 3, 2026

🤖 Copilot Code Review — PR #126507

Note

This review was generated by GitHub Copilot.

Holistic Assessment

Motivation: Justified — generic UnsafeAccessor wrapper classes without their type parameter constraints produce a C# compile error (CS0314/CS0315) when constraints are required, so this is a real correctness bug for users with constrained generic types.

Approach: The final approach (using Roslyn's SymbolDisplayGenericsOptions.IncludeTypeConstraints + string parsing) is simpler and more maintainable than the intermediate manually-built approach that was refactored away. The string parsing is sound (see below). The integration and unit tests cover roundtrip behavior. However the unit tests don't assert on generated source content, and allows ref struct was explicitly claimed (in commit ead15ae6) but is unverified after the subsequent refactor (a04c4845).

Summary: ⚠️ Needs Human Review. The code is correct for the tested scenarios, but there's an unverified allows ref struct claim and the unit tests validate compilation success only, not that the where clause text is actually emitted.


Detailed Findings

✅ String parsing — IndexOf(" where ") is robust

GetTypeParameterConstraintClauses finds the first " where " in the Roslyn display string to extract constraint clauses. This is safe because:

  • C# identifiers cannot contain spaces, so neither namespace segments (global::My.Where.Namespace) nor type parameter names (TWhere) can produce the literal " where " (space-where-space) in the display string.
  • The verbatim identifier @where renders as @where (with the @ prefix), which would not match.
  • The Substring(whereIndex + 1) correctly trims the leading space to produce "where T : ...", and the emitter then prepends a space with $" {c}", giving valid C#.

✅ Generated code format — valid C#

private static class __GenericAccessors_Foo<T> where T : notnull, global::NS.MyBase is syntactically correct. The space separator between > and where is correctly inserted. Multi-parameter cases like where T : notnull where U : class are also handled correctly since the entire substring from the first where to the end is preserved.

✅ Incremental caching — correct

DeclaringTypeParameterConstraintClauses is string? added to a sealed record. string participates in structural equality, so the incremental source generator caching contract is preserved.

namedType2 — necessary

namedType is a pattern variable declared in the DeclaringTypeParameterNames initializer expression (line 1407). In C# object initializers, pattern variables share the scope of the entire object creation expression, so namedType is already in scope for line 1409. Redeclaring it would be a compile error — hence namedType2 is required. The existing style is consistent (lines 1403–1405 also repeat the same pattern without capturing).

⚠️ allows ref struct — claimed but unverified

The commit history shows an explicit allows ref struct implementation was added (ead15ae6) via reflection on ITypeParameterSymbol.AllowsRefLikeType, then removed in the subsequent refactor (a04c4845) with the rationale that IncludeTypeConstraints is "future-proof" for new constraint kinds. However:

  • Neither commit included an allows ref struct test case.
  • The final PR has no test verifying that where T : allows ref struct is emitted correctly.

While IncludeTypeConstraints very likely renders allows ref struct (it's part of the Roslyn constraint display pipeline), the claim is unverified. A [InlineData] entry for "allows ref struct" in the VariousConstraints theory would close this gap — though note that allows ref struct types cannot actually be serialized by STJ, so verifying at the compilation level only is acceptable.

⚠️ Unit tests assert compilation success, not generated output

All four new unit tests (GenericTypeWithConstrainedTypeParameters_*) call RunJsonSourceGenerator(compilation, logger: logger) and discard the result. RunJsonSourceGenerator asserts no diagnostic above DiagnosticSeverity.Info, which verifies the generated code compiles — but does not assert that the where clause is actually present in the emitted source.

Before this PR, the generator would emit private static class __GenericAccessors_Foo<T> (missing the constraint), and that also compiles fine when the constraint doesn't affect the class body (e.g., where T : notnull alone is not enforced at the call site of the generated private class). The tests may therefore pass even without the fix for some constraint variants.

The InitOnlyProperties_GenericTypeWithConstraints_CanRoundtrip integration test is more meaningful because it exercises the actual generated class, but consider adding at least one unit test that asserts the generated source contains the expected constraint text:

var result = CompilationHelper.RunJsonSourceGenerator(compilation, logger: logger);
var generatedSource = result.NewCompilation.SyntaxTrees
    .Select(t => t.GetText().ToString())
    .FirstOrDefault(s => s.Contains("__GenericAccessors_"));
Assert.Contains("where T : ", generatedSource);

💡 Missing blank line (minor style)

The new s_fullyQualifiedWithConstraints field at line 34 is immediately followed by private const string JsonExtensionDataAttributeFullName without a blank line, whereas other field/constant groups in the class have separating blank lines.

Generated by Code Review for issue #126507 ·

@lewing
Copy link
Copy Markdown
Member

lewing commented Apr 3, 2026

/ba-g wasm coreclr timeout is non-blocking

@lewing lewing merged commit 6fb2237 into main Apr 3, 2026
91 of 94 checks passed
@lewing lewing deleted the investigate/stj-sg-errors branch April 3, 2026 21:34
radekdoulik pushed a commit to radekdoulik/runtime that referenced this pull request Apr 9, 2026
…ssor wrappers (dotnet#126507)

> [!NOTE]
> This PR was generated with the assistance of GitHub Copilot.

## Description

Fixes the aspnetcore build failure reported in
dotnet/dotnet#5880 (comment),
caused by dotnet#124650.

When the STJ source generator emits `UnsafeAccessor` wrapper classes for
generic types (e.g., `__GenericAccessors_MyType<T>`), it was not
propagating type parameter constraints from the original type
definition. This caused **CS0314** compilation errors in downstream
consumers (aspnetcore) when the type parameter had constraints like
`where TResponse : notnull, AuthenticatorResponse`.

### Before (broken)
```csharp
private static class __GenericAccessors_...<TResponse>  // missing constraint!
{
    [UnsafeAccessor(...)]
    public static extern ... __get_...(PublicKeyCredential<TResponse> obj);  // CS0314
}
```

### After (fixed)
```csharp
private static class __GenericAccessors_...<TResponse> where TResponse : notnull, global::...AuthenticatorResponse
{
    [UnsafeAccessor(...)]
    public static extern ... __get_...(PublicKeyCredential<TResponse> obj);  // OK
}
```

## Changes

- **`PropertyGenerationSpec.cs`** — Added
`DeclaringTypeParameterConstraintClauses` property to store constraint
clauses
- **`JsonSourceGenerator.Parser.cs`** — Added
`GetTypeParameterConstraintClause()` and
`GetTypeParameterConstraintsCombined()` to extract all constraint kinds
(`struct`, `class`, `notnull`, `unmanaged`, base types, interfaces,
`new()`) from `ITypeParameterSymbol`
- **`JsonSourceGenerator.Emitter.cs`** — Appends constraint clauses to
the generic wrapper class declaration

## Tests

- **Unit test**:
`GenericTypeWithConstrainedTypeParameters_InitOnlyProperties` — verifies
source gen compiles for constrained generic types
- **Runtime test**:
`InitOnlyProperties_GenericTypeWithConstraints_CanRoundtrip` — verifies
serialization/deserialization end-to-end for a generic type with `where
T : notnull, BaseClass` constraints
- All 194 source gen unit tests pass
- All 378 PropertyVisibility runtime tests pass

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

5 participants