Skip to content

Commit 37e1d24

Browse files
Align generic polymorphism failure semantics
Match the refined System.Text.Json behavior by rejecting derived-type registrations that fail for the requested closed base, including specialization mismatches and generic constraint violations. Expand source-generation, reflection, and cross-provider coverage for the runtime regression matrix. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f5ead997-2f05-463d-acd9-1dbcbd15b13e
1 parent e34160b commit 37e1d24

8 files changed

Lines changed: 291 additions & 200 deletions

File tree

docs/docs/shape-providers.md

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -348,42 +348,17 @@ record Mid<T> : Base<List<T>>;
348348
record Leaf<T>(List<T> Items) : Mid<T>;
349349
```
350350

351-
Open generic derived types are rejected at compile-time (for the source generator) or shape-construction time (for the reflection provider) when the registration is *fundamentally* invalid -- that is, no instantiation of the base type could ever satisfy it:
351+
Open generic derived types are rejected at compile time (for the source generator) or shape-construction time (for the reflection provider) when the registration cannot be resolved for the requested base type:
352352

353353
* The derived type does not derive from or implement *any* instantiation of the base type.
354+
* The derived type targets a different construction of the generic base — e.g. `Derived<T> : Base<T, int>` registered for `Base<string, string>`.
354355
* The derived type has type parameters that cannot be inferred from any base specification — e.g. `Derived<T, U> : Base<T>` leaves `U` unbound.
356+
* The inferred type arguments do not satisfy a generic constraint declared by the derived type.
355357
* The derived type matches more than one ancestor instantiation of the base — only relevant for interface bases.
356358

357359
The source generator emits diagnostic `PT0013` for these failures with a short message describing the reason; the reflection provider throws `InvalidOperationException` with an equivalent message.
358360

359-
#### Per-instantiation filtering
360-
361-
A registration that is well-formed in isolation but does not apply to the *particular* closed base being resolved is silently filtered rather than reported as an error. This allows a single declaration to span multiple closed instantiations naturally:
362-
363-
```csharp
364-
// Cat targets Animal<int>; Dog targets Animal<string>. The two attributes coexist:
365-
// when resolving Animal<int>, Cat is included and Dog is filtered; the converse holds for Animal<string>.
366-
[DerivedTypeShape(typeof(Cat))]
367-
[DerivedTypeShape(typeof(Dog))]
368-
partial class Animal<T>;
369-
class Cat : Animal<int>;
370-
class Dog : Animal<string>;
371-
```
372-
373-
```csharp
374-
// A constraint on the derived type filters per closed base. For Base<List<int>> the derivation
375-
// applies; for Base<string> (where string does not implement IEnumerable<int>) it is filtered.
376-
[DerivedTypeShape(typeof(Derived<>))]
377-
partial class Base<T>;
378-
class Derived<T> : Base<T> where T : IEnumerable<int>;
379-
```
380-
381-
The two failure modes that are silently filtered (rather than diagnosed) are:
382-
383-
* **Unification mismatch** — a closed derived registered for `Base<int>` simply does not apply when resolving `Base<string>`; the same is true for an open derived whose base specification cannot be unified with the requested closed base (e.g. `Wrapped<T> : Base<List<T>>` against `Base<int>`).
384-
* **Constraint violation** — the resolved substitution does not satisfy a `where T : …` constraint on the derived type for this particular closed base.
385-
386-
In both cases the registration is dropped silently; if all registrations are filtered out, the resulting union shape simply has no derived cases.
361+
Every declared registration participates in the union configuration. PolyType does not filter registrations that only apply to another closed construction of the base. For example, registering both `Cat : Animal<int>` and `Dog : Animal<string>` on `Animal<T>` makes the configuration invalid for either closed base because one registration is not assignable. Define separate closed base declarations when different constructions require different derived-type sets.
387362

388363
### PropertyShapeAttribute
389364

src/PolyType.SourceGenerator/Helpers/OpenGenericDerivedTypeHelpers.cs

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
// behaviour in sync.
66
//
77
// The algorithm is a port of the resolver added in dotnet/runtime#127318 (System.Text.Json
8-
// support for open generic [JsonDerivedType]). See the PR description for the full set of
9-
// supported and rejected patterns.
8+
// support for open generic [JsonDerivedType]), with failure semantics aligned to the refinements
9+
// in dotnet/runtime#130808. See those PRs for the supported and rejected patterns.
1010

1111
using Microsoft.CodeAnalysis;
1212
using PolyType.Roslyn.Helpers;
@@ -406,15 +406,3 @@ internal enum OpenGenericResolutionFailure
406406
/// <summary>The derived type matches the base type through multiple distinct ancestors.</summary>
407407
AmbiguousMatch,
408408
}
409-
410-
internal static class OpenGenericResolutionFailureExtensions
411-
{
412-
// Returns true when the failure is caused by the registration not matching THIS particular
413-
// closed base, but where the same registration could plausibly apply to a different
414-
// instantiation of the base. Such failures are silently skipped rather than reported as
415-
// diagnostics, allowing a single attribute on an open base to span multiple closed
416-
// instantiations naturally.
417-
public static bool IsPerInstantiationFailure(this OpenGenericResolutionFailure failure) =>
418-
failure is OpenGenericResolutionFailure.UnificationFailed
419-
or OpenGenericResolutionFailure.ConstraintViolation;
420-
}

src/PolyType.SourceGenerator/Parser/Parser.cs

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -625,14 +625,6 @@ protected override IEnumerable<DerivedTypeModel> ResolveDerivedTypes(ITypeSymbol
625625
{
626626
if (!TryResolveOpenGenericDerivedType(namedDerivedType, type, out INamedTypeSymbol? specializedDerivedType, out OpenGenericResolutionFailure? failure, out string? failedDetail))
627627
{
628-
if (failure!.Value.IsPerInstantiationFailure())
629-
{
630-
// The registration is valid in isolation but does not apply to this
631-
// particular closed base. A different closed instantiation of the
632-
// same base definition may still match it; silently skip.
633-
continue;
634-
}
635-
636628
ReportDiagnostic(DerivedTypeUnsupportedGenerics, attribute.GetLocation(), derivedType.ToDisplayString(), type.ToDisplayString(), FormatOpenGenericFailureReason(failure!.Value, failedDetail));
637629
continue;
638630
}
@@ -641,16 +633,6 @@ protected override IEnumerable<DerivedTypeModel> ResolveDerivedTypes(ITypeSymbol
641633
}
642634
else if (!type.IsAssignableFrom(derivedType))
643635
{
644-
// The closed derived type does not fit this particular closed base. If the
645-
// base is a generic instantiation AND the derived inherits from some other
646-
// instantiation of the same base definition, silently skip -- the registration
647-
// is targeted at a different closed base. Otherwise treat as a hard error.
648-
if (type is INamedTypeSymbol { IsGenericType: true } namedBase &&
649-
derivedType.GetCompatibleGenericBaseTypes(namedBase.OriginalDefinition).Any())
650-
{
651-
continue;
652-
}
653-
654636
ReportDiagnostic(DerivedTypeNotAssignableToBase, attribute.GetLocation(), derivedType.ToDisplayString(), type.ToDisplayString());
655637
continue;
656638
}

src/PolyType.TestCases/TestTypes.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,29 @@ public static IEnumerable<ITestCase> GetTestCasesCore()
612612
(OpenGenericMultiLevelBase<List<int>>)new OpenGenericMultiLevelLeaf<int>([10, 20]),
613613
isUnion: true,
614614
provider: p);
615+
yield return TestCase.Create(
616+
(OpenGenericRepeatedBase<int, int>)new OpenGenericRepeatedDerived<int>(1, 2, "valid"),
617+
isUnion: true,
618+
provider: p);
619+
yield return TestCase.Create(
620+
(OpenGenericInterfaceConstraintBase<List<int>>)new OpenGenericInterfaceConstraintDerived<List<int>>([1, 2], "valid"),
621+
isUnion: true,
622+
provider: p);
623+
yield return TestCase.Create(
624+
(OpenGenericNewConstraintBase<OpenGenericNewConstraintArgument>)new OpenGenericNewConstraintDerived<OpenGenericNewConstraintArgument>(new() { Value = 42 }, "valid"),
625+
isUnion: true,
626+
provider: p);
627+
yield return TestCase.Create(
628+
(OpenGenericDeepJaggedBase<List<int[][][]>>)new OpenGenericDeepJaggedDerived<int>(
629+
[
630+
[
631+
[
632+
[1, 2],
633+
],
634+
],
635+
]),
636+
isUnion: true,
637+
provider: p);
615638

616639
yield return TestCase.Create(new RecordWithoutNamespace(42));
617640
yield return TestCase.Create(new GenericRecordWithoutNamespace<int>(42), p);
@@ -2771,6 +2794,29 @@ public partial record OpenGenericMultiLevelBase<T>;
27712794
public partial record OpenGenericMultiLevelMid<T> : OpenGenericMultiLevelBase<List<T>>;
27722795
public partial record OpenGenericMultiLevelLeaf<T>(List<T> Items) : OpenGenericMultiLevelMid<T>;
27732796

2797+
[DerivedTypeShape(typeof(OpenGenericRepeatedDerived<>), Name = "repeated")]
2798+
public partial record OpenGenericRepeatedBase<T1, T2>;
2799+
public partial record OpenGenericRepeatedDerived<T>(T First, T Second, string Marker) : OpenGenericRepeatedBase<T, T>;
2800+
2801+
[DerivedTypeShape(typeof(OpenGenericInterfaceConstraintDerived<>), Name = "interfaceConstraint")]
2802+
public partial record OpenGenericInterfaceConstraintBase<T>;
2803+
public partial record OpenGenericInterfaceConstraintDerived<T>(T Value, string Marker) : OpenGenericInterfaceConstraintBase<T>
2804+
where T : IEnumerable<int>;
2805+
2806+
[DerivedTypeShape(typeof(OpenGenericNewConstraintDerived<>), Name = "newConstraint")]
2807+
public partial record OpenGenericNewConstraintBase<T>;
2808+
public partial record OpenGenericNewConstraintDerived<T>(T Value, string Marker) : OpenGenericNewConstraintBase<T>
2809+
where T : class, new();
2810+
2811+
public partial class OpenGenericNewConstraintArgument
2812+
{
2813+
public int Value { get; set; }
2814+
}
2815+
2816+
[DerivedTypeShape(typeof(OpenGenericDeepJaggedDerived<>), Name = "deepJagged")]
2817+
public partial record OpenGenericDeepJaggedBase<T>;
2818+
public partial record OpenGenericDeepJaggedDerived<T>(List<T[][][]> Value) : OpenGenericDeepJaggedBase<List<T[][][]>>;
2819+
27742820
[GenerateShape]
27752821
public partial record PropertyRequiredByAttribute
27762822
{
@@ -3599,6 +3645,10 @@ public delegate Task<int> LargeAsyncDelegate(
35993645
[GenerateShapeFor<OpenGenericArrayBase<int[]>>]
36003646
[GenerateShapeFor<IOpenGenericInterfaceBase<int>>]
36013647
[GenerateShapeFor<OpenGenericMultiLevelBase<List<int>>>]
3648+
[GenerateShapeFor<OpenGenericRepeatedBase<int, int>>]
3649+
[GenerateShapeFor<OpenGenericInterfaceConstraintBase<List<int>>>]
3650+
[GenerateShapeFor<OpenGenericNewConstraintBase<OpenGenericNewConstraintArgument>>]
3651+
[GenerateShapeFor<OpenGenericDeepJaggedBase<List<int[][][]>>>]
36023652
[GenerateShapeFor<GenericRecordWithoutNamespace<int>>]
36033653
[GenerateShapeFor<GenericContainerWithoutNamespace<int>.Record<string>>]
36043654
[GenerateShapeFor<IAsyncEnumerable<int>>]

src/PolyType/ReflectionProvider/OpenGenericDerivedTypeResolver.cs

Lines changed: 3 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
// behaviour in sync.
66
//
77
// The algorithm is a port of the resolver added in dotnet/runtime#127318 (System.Text.Json
8-
// support for open generic [JsonDerivedType]). See the PR description for the full set of
9-
// supported and rejected patterns.
8+
// support for open generic [JsonDerivedType]), with failure semantics aligned to the refinements
9+
// in dotnet/runtime#130808. See those PRs for the supported and rejected patterns.
1010

1111
using System.Diagnostics;
1212
using System.Diagnostics.CodeAnalysis;
@@ -176,16 +176,13 @@ public static bool TryResolveOpenGenericDerivedType(
176176
Type openDerivedType,
177177
Type baseType,
178178
[NotNullWhen(true)] out Type? closedDerivedType,
179-
[NotNullWhen(false)] out string? failureReason,
180-
out OpenGenericResolutionFailureKind failureKind)
179+
[NotNullWhen(false)] out string? failureReason)
181180
{
182181
closedDerivedType = null;
183182
failureReason = null;
184-
failureKind = default;
185183

186184
if (!baseType.IsGenericType)
187185
{
188-
failureKind = OpenGenericResolutionFailureKind.NotAssignable;
189186
failureReason = "the derived type is not assignable to the base type";
190187
return false;
191188
}
@@ -205,7 +202,6 @@ public static bool TryResolveOpenGenericDerivedType(
205202

206203
if (matchingBases.Count == 0)
207204
{
208-
failureKind = OpenGenericResolutionFailureKind.NotAssignable;
209205
failureReason = "the derived type is not assignable to the base type";
210206
return false;
211207
}
@@ -231,7 +227,6 @@ public static bool TryResolveOpenGenericDerivedType(
231227
{
232228
if (!referencedParams.Contains(required))
233229
{
234-
failureKind = OpenGenericResolutionFailureKind.UnboundParameter;
235230
failureReason = $"the type parameter '{required.Name}' of the derived type is not bound by the base type's arguments";
236231
return false;
237232
}
@@ -292,15 +287,13 @@ public static bool TryResolveOpenGenericDerivedType(
292287
}
293288
else
294289
{
295-
failureKind = OpenGenericResolutionFailureKind.AmbiguousMatch;
296290
failureReason = "the derived type matches the base type through multiple ancestors";
297291
return false;
298292
}
299293
}
300294

301295
if (successCount == 0 || successfulArgs is null)
302296
{
303-
failureKind = OpenGenericResolutionFailureKind.UnificationFailed;
304297
failureReason = "the base type's arguments do not match the derived type's base specification";
305298
return false;
306299
}
@@ -312,41 +305,8 @@ public static bool TryResolveOpenGenericDerivedType(
312305
}
313306
catch (Exception ex) when (ex is ArgumentException or TypeLoadException)
314307
{
315-
failureKind = OpenGenericResolutionFailureKind.ConstraintViolation;
316308
failureReason = "the closed derived type would violate one of its declared generic constraints";
317309
return false;
318310
}
319311
}
320312
}
321-
322-
// Identifies why an open generic derived type could not be resolved against a constructed base.
323-
// Mirrors PolyType.SourceGenerator.Helpers.OpenGenericResolutionFailure on the source-gen side.
324-
internal enum OpenGenericResolutionFailureKind
325-
{
326-
/// <summary>The derived type cannot be assigned to the base type (no matching ancestor at all).</summary>
327-
NotAssignable,
328-
329-
/// <summary>A matching ancestor exists but its type arguments do not unify with this particular closed base.</summary>
330-
UnificationFailed,
331-
332-
/// <summary>One of the derived type's parameters is not referenced by any matching ancestor's base specification.</summary>
333-
UnboundParameter,
334-
335-
/// <summary>The resolved substitution does not satisfy the derived type's declared generic constraints.</summary>
336-
ConstraintViolation,
337-
338-
/// <summary>The derived type unifies with the closed base through more than one ancestor.</summary>
339-
AmbiguousMatch,
340-
}
341-
342-
// Helpers for classifying open generic resolution failures.
343-
internal static class OpenGenericResolutionFailureKindExtensions
344-
{
345-
// Returns true when the failure is caused by the registration not matching THIS particular
346-
// closed base, but where the same registration could plausibly apply to a different
347-
// instantiation of the base. Callers that close attributes against a specific base may
348-
// silently skip such registrations rather than surfacing them as errors.
349-
public static bool IsPerInstantiationFailure(this OpenGenericResolutionFailureKind kind) =>
350-
kind is OpenGenericResolutionFailureKind.UnificationFailed
351-
or OpenGenericResolutionFailureKind.ConstraintViolation;
352-
}

src/PolyType/ReflectionProvider/ReflectionTypeShapeProvider.cs

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -329,17 +329,8 @@ private IUnionTypeShape CreateUnionTypeShape(Type unionType, FSharpUnionInfo? fS
329329
derivedType,
330330
unionType,
331331
out Type? closedDerivedType,
332-
out string? failureReason,
333-
out OpenGenericResolutionFailureKind failureKind))
332+
out string? failureReason))
334333
{
335-
if (failureKind.IsPerInstantiationFailure())
336-
{
337-
// The registration is valid in isolation but does not apply to this
338-
// particular closed base. A different closed instantiation of the
339-
// same base definition may still match it; silently skip.
340-
continue;
341-
}
342-
343334
throw new InvalidOperationException(
344335
$"The declared open generic derived type '{derivedType}' could not be resolved against the polymorphic base type '{unionType}': {failureReason}.");
345336
}
@@ -348,17 +339,6 @@ private IUnionTypeShape CreateUnionTypeShape(Type unionType, FSharpUnionInfo? fS
348339
}
349340
else if (!unionType.IsAssignableFrom(derivedType))
350341
{
351-
// The closed derived type does not fit this particular closed base. If the
352-
// base is a generic instantiation AND the derived inherits from some other
353-
// instantiation of the same base definition, silently skip -- the registration
354-
// is targeted at a different closed base. Otherwise the registration is a
355-
// hard misregistration.
356-
if (unionType.IsGenericType &&
357-
derivedType.GetMatchingGenericBaseTypes(unionType.GetGenericTypeDefinition()).Any())
358-
{
359-
continue;
360-
}
361-
362342
throw new InvalidOperationException($"The declared derived type '{derivedType}' is not a valid subtype of '{unionType}'.");
363343
}
364344

0 commit comments

Comments
 (0)