diff --git a/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs b/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs
index a730e09f2f43b6..79373d7c397214 100644
--- a/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs
+++ b/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs
@@ -5,6 +5,7 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
@@ -524,6 +525,82 @@ public static bool TryValidateGenericConstraints(
return true;
}
+ ///
+ /// Returns if 's declared constraints
+ /// match 's declared constraints exactly, after applying
+ /// to type-constraint references. Used by the
+ /// "uniform derived registration" check to verify that a derived type's parameter
+ /// constraints will be satisfied for every valid specialization of the base.
+ ///
+ /// "Exact match" is used in place of one-sided constraint subsumption because in the
+ /// uniform-applicability regime the two are equivalent (C# already forces a derived
+ /// parameter that's identified with a base parameter to declare at least the base's
+ /// constraints), and an equality check is simpler, easier to reason about, and
+ /// forward-compatible with new C# constraint kinds.
+ ///
+ ///
+ /// Special constraint flags (class, struct, unmanaged,
+ /// new()) must match exactly.
+ /// The set of type constraints must match exactly after substitution
+ /// (order-independent). For F-bounded constraints, the substitution is
+ /// applied to nested type-parameter positions as well, so a derived
+ /// where T : IComparable<T> matches a base
+ /// where U : IComparable<U> only after the substitution
+ /// { T -> U } has been applied to both occurrences.
+ /// The compile-time-only notnull constraint is intentionally ignored
+ /// (it is not surfaced via reflection and is not runtime-enforced). As a
+ /// consequence, two registrations whose constraints differ only in the
+ /// presence of notnull are accepted as equivalent.
+ ///
+ ///
+ /// IMPORTANT: This implementation MIRRORS the reflection-side
+ /// System.Text.Json.Reflection.ReflectionExtensions.AreConstraintsEquivalent.
+ /// Any change to the equivalence rules MUST be applied on both sides.
+ ///
+ public static bool AreConstraintsEquivalent(
+ this Compilation compilation,
+ ITypeParameterSymbol derivedParam,
+ ITypeParameterSymbol baseParam,
+ IReadOnlyDictionary substitution)
+ {
+ if (derivedParam.HasReferenceTypeConstraint != baseParam.HasReferenceTypeConstraint ||
+ derivedParam.HasValueTypeConstraint != baseParam.HasValueTypeConstraint ||
+ derivedParam.HasUnmanagedTypeConstraint != baseParam.HasUnmanagedTypeConstraint ||
+ derivedParam.HasConstructorConstraint != baseParam.HasConstructorConstraint)
+ {
+ return false;
+ }
+
+ ImmutableArray derivedConstraints = derivedParam.ConstraintTypes;
+ ImmutableArray baseConstraints = baseParam.ConstraintTypes;
+ if (derivedConstraints.Length != baseConstraints.Length)
+ {
+ return false;
+ }
+
+ if (derivedConstraints.Length == 0)
+ {
+ return true;
+ }
+
+ // Compare type-constraint sets order-independently. Substitute each derived
+ // constraint into base-parameter terms via the canonical mapping, then check
+ // that the resulting multiset matches the base's constraint set. Length parity
+ // was already established above, so removing each substituted derived constraint
+ // from a fresh copy of the base set proves equality iff every Remove succeeds.
+ var baseSet = new HashSet(baseConstraints, SymbolEqualityComparer.Default);
+ foreach (ITypeSymbol derivedConstraint in derivedConstraints)
+ {
+ ITypeSymbol substituted = compilation.SubstituteTypeParameters(derivedConstraint, substitution);
+ if (!baseSet.Remove(substituted))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
///
/// Constructs using , accounting for
/// nesting: the leading args bind enclosing-type parameters (outermost first) and the
@@ -594,6 +671,215 @@ public static INamedTypeSymbol ConstructWithEnclosingTypeArguments(this INamedTy
public static bool IsGenericTypeDefinition(this ITypeSymbol type)
=> type is INamedTypeSymbol { IsGenericType: true } namedType && SymbolEqualityComparer.Default.Equals(namedType, namedType.ConstructedFrom);
+ ///
+ /// Validates that applies uniformly to every
+ /// specialization of the open generic base type whose definition matches
+ /// constructedBase.OriginalDefinition, and (when it does) produces the closed
+ /// derived type for the closure identified by .
+ ///
+ /// "Uniform" means: there is a single canonical substitution mapping each derived
+ /// type parameter to a base type parameter that simultaneously satisfies every
+ /// matching ancestor of the derived type, with every derived constraint exactly
+ /// matching the constraints on the corresponding base parameter. Per-ancestor
+ /// unifications are computed independently and then verified to coincide -- this is
+ /// what catches asymmetric multi-interface implementations like
+ /// D<U1, U2> : IBase<U1, U2>, IBase<U2, U1>, where each
+ /// ancestor admits a unifier on its own but no single canonical answer covers both.
+ /// Registrations that pin a particular specialization
+ /// (e.g. Derived<T> : Base<T, int>) are rejected: such registrations
+ /// would silently work for one base specialization and break for another, which we
+ /// treat as a misregistration regardless of which specialization is currently being
+ /// generated.
+ ///
+ /// Returns with set to
+ /// the closed derived type, or with a localized
+ /// drawn from SR.Polymorphism_OpenGeneric_*
+ /// (suitable for inclusion in the SYSLIB1229 message).
+ ///
+ /// IMPORTANT: This implementation MIRRORS the reflection-side resolver
+ /// DefaultJsonTypeInfoResolver.Helpers.TryResolveOpenGenericDerivedType in
+ /// src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs.
+ /// Both implementations -- the per-ancestor unification, the canonical-substitution
+ /// consistency check, and the constraint-equivalence rules -- must be kept in lockstep
+ /// so source-gen and reflection produce the same closed type for the same registration.
+ ///
+ public static bool TryResolveOpenGenericDerivedType(
+ this Compilation compilation,
+ INamedTypeSymbol unboundDerived,
+ INamedTypeSymbol constructedBase,
+ out INamedTypeSymbol? resolvedDerivedType,
+ out string? failureReason)
+ {
+ Debug.Assert(unboundDerived.IsUnboundGenericType);
+ Debug.Assert(constructedBase.IsGenericType);
+
+ resolvedDerivedType = null;
+ failureReason = null;
+
+ INamedTypeSymbol derivedDefinition = unboundDerived.OriginalDefinition;
+ INamedTypeSymbol baseDefinition = constructedBase.OriginalDefinition;
+
+ // Every ancestor of the derived type definition whose original definition matches
+ // the base type definition. For classes there is at most one such ancestor; for
+ // interfaces a derived type can implement the same interface definition multiple
+ // times with different type arguments.
+ List matchingBases = derivedDefinition
+ .GetCompatibleGenericBaseTypes(baseDefinition)
+ .ToList();
+
+ if (matchingBases.Count == 0)
+ {
+ failureReason = SR.Polymorphism_OpenGeneric_Reason_NotAssignable;
+ return false;
+ }
+
+ // The complete set of derived parameters that must be bound (enclosing + leaf).
+ List requiredDerivedParams = derivedDefinition.GetAllTypeParameters();
+ List baseParams = baseDefinition.GetAllTypeParameters();
+ var baseParamSet = new HashSet(baseParams, SymbolEqualityComparer.Default);
+
+ // Per-ancestor independent substitutions; the uniform answer must be a single
+ // canonical substitution agreed upon by every ancestor.
+ Dictionary? canonical = null;
+
+ foreach (INamedTypeSymbol ancestor in matchingBases)
+ {
+ var substitution = new Dictionary(
+ requiredDerivedParams.Count, SymbolEqualityComparer.Default);
+
+ if (!ancestor.TryUnifyWith(baseDefinition, substitution))
+ {
+ // No unifier exists. Some position pins a concrete type (e.g. Base)
+ // or a constructed pattern (e.g. Base>) that cannot match the base
+ // type parameter at that position (the rigid target).
+ failureReason = SR.Polymorphism_OpenGeneric_Reason_NonUniformPinning;
+ return false;
+ }
+
+ foreach (ITypeParameterSymbol p in requiredDerivedParams)
+ {
+ if (!substitution.TryGetValue(p, out ITypeSymbol? mapped))
+ {
+ // E.g. D : IBase -- U2 is not bound by this ancestor.
+ failureReason = string.Format(
+ CultureInfo.InvariantCulture,
+ SR.Polymorphism_OpenGeneric_Reason_UnboundParameter,
+ p.Name);
+ return false;
+ }
+
+ if (mapped is not ITypeParameterSymbol mappedBaseParam || !baseParamSet.Contains(mappedBaseParam))
+ {
+ // Defensive: a unifier exists but it would map a derived parameter to
+ // something other than one of the base's own type parameters (i.e. the
+ // result isn't a pure renaming). With the rigid-target unification used
+ // here this is essentially unreachable -- TryUnifyWith binds derived
+ // parameters only against the base definition's own type arguments,
+ // which are all base parameters -- but we report it separately from
+ // NonUniformPinning so any future relaxation of TryUnifyWith (e.g.
+ // binding into nested constructed targets) surfaces with a precise
+ // diagnostic instead of getting silently lumped under pinning.
+ failureReason = SR.Polymorphism_OpenGeneric_Reason_NonUniformUnification;
+ return false;
+ }
+ }
+
+ if (canonical is null)
+ {
+ canonical = substitution;
+ }
+ else if (!SubstitutionsEqual(canonical, substitution))
+ {
+ // Two ancestors agree on independent bindings but produce different
+ // (derived -> base) mappings, e.g. D : IBase, IBase.
+ // There is no single canonical answer for an arbitrary base closure.
+ failureReason = SR.Polymorphism_OpenGeneric_Reason_AmbiguousMatch;
+ return false;
+ }
+ }
+
+ Debug.Assert(canonical is not null);
+
+ // Constraint equivalence: every derived parameter's constraints must exactly
+ // match the constraints on the mapped base parameter (after substitution) so
+ // that any valid closure of the base also yields a valid closure of the
+ // derived. See ReflectionExtensions.AreConstraintsEquivalent for the rationale
+ // behind exact match (vs one-sided subsumption).
+ foreach (ITypeParameterSymbol derivedParam in requiredDerivedParams)
+ {
+ var mappedBaseParam = (ITypeParameterSymbol)canonical[derivedParam];
+ if (!compilation.AreConstraintsEquivalent(derivedParam, mappedBaseParam, canonical))
+ {
+ failureReason = string.Format(
+ CultureInfo.InvariantCulture,
+ SR.Polymorphism_OpenGeneric_Reason_ConstraintMismatch,
+ derivedParam.Name,
+ mappedBaseParam.Name);
+ return false;
+ }
+ }
+
+ // Closure construction: substitute the canonical mapping then specialize each
+ // base parameter to the actual closed-base type argument.
+ var baseParamPosition = new Dictionary(SymbolEqualityComparer.Default);
+ for (int i = 0; i < baseParams.Count; i++)
+ {
+ baseParamPosition[baseParams[i]] = i;
+ }
+
+ List constructedBaseAllArgs = GetAllTypeArguments(constructedBase);
+
+ var closedArgs = new ITypeSymbol[requiredDerivedParams.Count];
+ for (int i = 0; i < requiredDerivedParams.Count; i++)
+ {
+ var mappedBaseParam = (ITypeParameterSymbol)canonical[requiredDerivedParams[i]];
+ closedArgs[i] = constructedBaseAllArgs[baseParamPosition[mappedBaseParam]];
+ }
+
+ resolvedDerivedType = derivedDefinition.ConstructWithEnclosingTypeArguments(closedArgs);
+ return true;
+
+ static bool SubstitutionsEqual(
+ Dictionary a,
+ Dictionary b)
+ {
+ if (a.Count != b.Count)
+ {
+ return false;
+ }
+
+ foreach (KeyValuePair kvp in a)
+ {
+ if (!b.TryGetValue(kvp.Key, out ITypeSymbol? otherValue) ||
+ !SymbolEqualityComparer.Default.Equals(kvp.Value, otherValue))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ static List GetAllTypeArguments(INamedTypeSymbol type)
+ {
+ var result = new List();
+ Append(type.ContainingType, result);
+ result.AddRange(type.TypeArguments);
+ return result;
+
+ static void Append(INamedTypeSymbol? enclosing, List list)
+ {
+ if (enclosing is null)
+ {
+ return;
+ }
+
+ Append(enclosing.ContainingType, list);
+ list.AddRange(enclosing.TypeArguments);
+ }
+ }
+ }
+
public static bool IsNumberType(this ITypeSymbol type)
{
return type.SpecialType is
@@ -787,5 +1073,41 @@ private static bool HasCodeAnalysisAttribute(this ISymbol symbol, string attribu
attr.AttributeClass?.Name == attributeName &&
attr.AttributeClass.ContainingNamespace.ToDisplayString() == "System.Diagnostics.CodeAnalysis");
}
+
+ ///
+ /// Returns an for value tuples of two elements that
+ /// delegates equality and hashing of each tuple component to the corresponding element
+ /// comparer. Useful when neither component has a usable default comparer (e.g. Roslyn
+ /// symbol types, where must be used).
+ ///
+ public static IEqualityComparer<(T1, T2)> CreateTupleComparer(
+ IEqualityComparer firstComparer,
+ IEqualityComparer secondComparer)
+ {
+ return new TupleComparer(firstComparer, secondComparer);
+ }
+
+ private sealed class TupleComparer : IEqualityComparer<(T1, T2)>
+ {
+ private readonly IEqualityComparer _firstComparer;
+ private readonly IEqualityComparer _secondComparer;
+
+ public TupleComparer(IEqualityComparer firstComparer, IEqualityComparer secondComparer)
+ {
+ _firstComparer = firstComparer;
+ _secondComparer = secondComparer;
+ }
+
+ public bool Equals((T1, T2) x, (T1, T2) y) =>
+ _firstComparer.Equals(x.Item1, y.Item1) &&
+ _secondComparer.Equals(x.Item2, y.Item2);
+
+ public int GetHashCode((T1, T2) obj)
+ {
+ int h1 = obj.Item1 is null ? 0 : _firstComparer.GetHashCode(obj.Item1);
+ int h2 = obj.Item2 is null ? 0 : _secondComparer.GetHashCode(obj.Item2);
+ return unchecked(h1 * 397 ^ h2);
+ }
+ }
}
}
diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
index 9d80e95598a98c..83218d3dd994b3 100644
--- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
+++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
@@ -5,7 +5,6 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
-using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
@@ -33,6 +32,10 @@ private sealed class Parser
private static readonly SymbolDisplayFormat s_fullyQualifiedWithConstraints =
SymbolDisplayFormat.FullyQualifiedFormat.AddGenericsOptions(
SymbolDisplayGenericsOptions.IncludeTypeConstraints);
+
+ private static readonly IEqualityComparer<(ISymbol BaseDefinition, ISymbol DerivedDefinition)> s_typePairComparer =
+ RoslynExtensions.CreateTupleComparer(SymbolEqualityComparer.Default, SymbolEqualityComparer.Default);
+
private const string JsonExtensionDataAttributeFullName = "System.Text.Json.Serialization.JsonExtensionDataAttribute";
private const string JsonIgnoreAttributeFullName = "System.Text.Json.Serialization.JsonIgnoreAttribute";
private const string JsonIgnoreConditionFullName = "System.Text.Json.Serialization.JsonIgnoreCondition";
@@ -59,6 +62,13 @@ private sealed class Parser
private readonly Dictionary _generatedTypes = new(SymbolEqualityComparer.Default);
#pragma warning restore
+ // SYSLIB1229 dedup set: emit at most once per (open base definition, open derived
+ // definition) pair across the lifetime of this Parser. Whether a derived registration
+ // applies uniformly to a generic base is a property of the open forms alone; without
+ // dedup we'd spam the same diagnostic for every closure of the same base referenced via
+ // JsonSerializableAttribute.
+ private HashSet<(ISymbol BaseDefinition, ISymbol DerivedDefinition)> DiagnosedOpenDerivedRegistrations => field ??= new(s_typePairComparer);
+
public List Diagnostics { get; } = new();
private Location? _contextClassLocation;
@@ -940,19 +950,19 @@ private void ProcessTypeCustomAttributes(
Debug.Assert(attributeData.ConstructorArguments.Length > 0);
var derivedType = (ITypeSymbol)attributeData.ConstructorArguments[0].Value!;
- if (derivedType is INamedTypeSymbol { IsUnboundGenericType: true } unboundDerived)
+ if (!TryResolveDerivedType(
+ derivedType, typeToGenerate.Type,
+ out ITypeSymbol? resolvedDerivedType, out string? failureReason))
{
- if (!TryResolveOpenGenericDerivedType(
- unboundDerived, typeToGenerate.Type,
- out INamedTypeSymbol? resolvedType, out string? failureReason))
+ if (DiagnosedOpenDerivedRegistrations.Add((typeToGenerate.Type.OriginalDefinition, derivedType.OriginalDefinition)))
{
ReportDiagnostic(DiagnosticDescriptors.OpenGenericDerivedTypeCouldNotBeResolved, attributeData.GetLocation(), derivedType.ToDisplayString(), typeToGenerate.Type.ToDisplayString(), failureReason);
- continue;
}
-
- derivedType = resolvedType!;
+ continue;
}
+ derivedType = resolvedDerivedType!;
+
TypeRef derivedTypeRef = EnqueueType(derivedType, typeToGenerate.Mode);
object? typeDiscriminator = null;
@@ -1037,210 +1047,100 @@ namedUnionType is not null &&
}
///
- /// Source-gen-side resolver: closes against
- /// via structural unification at compile time.
- /// Returns true when the registration can be closed to a unique concrete
- /// type. Returns false with a localized
- /// suitable for inclusion in a diagnostic when the derived type cannot be
- /// resolved against this particular base.
+ /// Source-gen-side resolver for a [JsonDerivedType] registration against
+ /// . Dispatches on the four open/closed × generic/non-generic
+ /// shapes:
+ ///
+ /// Open derived + generic base: delegates to
+ /// for
+ /// uniform-applicability validation and closure construction.
+ /// Open derived + non-generic base: rejected as unassignable.
+ /// Closed derived + generic base: rejected. The attribute lives on the open
+ /// base definition and is shared across every closure; a closed derived necessarily
+ /// pins one specialization and would silently work for that closure and break for
+ /// the others.
+ /// Closed derived + non-generic base: the registration is accepted as-is,
+ /// subject to the post-condition assignability check below.
+ ///
///
- /// IMPORTANT: This implementation MIRRORS the reflection resolver
- /// DefaultJsonTypeInfoResolver.Helpers.TryResolveOpenGenericDerivedType
- /// in src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs.
- /// Both implementations -- the structural unbound pre-check, the per-ancestor
- /// unification, and the ambiguity detection -- must be kept in lockstep so that
- /// source-gen and reflection produce the same closed type for the same registration.
- /// Any algorithmic change here must be applied in the reflection mirror as well.
+ /// Returns with set
+ /// (either the closed derived type from unification, or
+ /// as-is for the pass-through case) only if the resolved type is also a real subtype
+ /// of ; otherwise returns with a
+ /// localized suitable for inclusion in SYSLIB1229.
///
- private bool TryResolveOpenGenericDerivedType(
- INamedTypeSymbol unboundDerived,
+ private bool TryResolveDerivedType(
+ ITypeSymbol declaredDerived,
ITypeSymbol baseType,
- out INamedTypeSymbol? resolvedType,
+ out ITypeSymbol? resolvedDerivedType,
out string? failureReason)
{
- resolvedType = null;
+ resolvedDerivedType = null;
failureReason = null;
- if (baseType is not INamedTypeSymbol { IsGenericType: true } constructedBase)
- {
- failureReason = SR.Polymorphism_OpenGeneric_Reason_NotAssignable;
- return false;
- }
-
- INamedTypeSymbol derivedDefinition = unboundDerived.OriginalDefinition;
- INamedTypeSymbol baseDefinition = constructedBase.OriginalDefinition;
-
- // Collect every ancestor of the derived type definition whose original
- // definition matches the base type definition. For classes there is at
- // most one ancestor; for interfaces a derived type can implement the same
- // interface definition multiple times with different type arguments.
- List matchingBases = derivedDefinition
- .GetCompatibleGenericBaseTypes(baseDefinition)
- .ToList();
-
- if (matchingBases.Count == 0)
- {
- failureReason = SR.Polymorphism_OpenGeneric_Reason_NotAssignable;
- return false;
- }
-
- // The full set of generic parameters we must bind includes the parameters
- // of the derived type definition AS WELL AS those declared by enclosing
- // generic types (Outer.Derived needs T bound from Outer).
- List requiredParams = derivedDefinition.GetAllTypeParameters();
- ImmutableArray constructedBaseArgs = constructedBase.TypeArguments;
-
- // Structural unbound pre-check: every required parameter must appear at least
- // once somewhere in some matching ancestor's type arguments. If a parameter
- // never appears at all, no closed base could ever bind it -- the derived
- // definition is malformed regardless of which closed base it is registered
- // against.
- HashSet referencedParams = new(SymbolEqualityComparer.Default);
- foreach (INamedTypeSymbol mb in matchingBases)
+ if (declaredDerived is INamedTypeSymbol { IsUnboundGenericType: true } unboundDerived)
{
- foreach (ITypeSymbol arg in mb.TypeArguments)
+ if (baseType is not INamedTypeSymbol { IsGenericType: true } constructedBase)
{
- CollectReferencedParameters(arg, referencedParams);
- }
- }
- foreach (ITypeParameterSymbol required in requiredParams)
- {
- if (!referencedParams.Contains(required))
- {
- failureReason = string.Format(
- CultureInfo.InvariantCulture,
- SR.Polymorphism_OpenGeneric_Reason_UnboundParameter,
- required.Name);
+ // Open derived registered against a non-generic base: no closure of the
+ // derived can ever be assignable to the base.
+ failureReason = SR.Polymorphism_OpenGeneric_Reason_NotAssignable;
return false;
}
- }
-
- Dictionary? successfulSubstitution = null;
- int successCount = 0;
-
- foreach (INamedTypeSymbol matchingBase in matchingBases)
- {
- ImmutableArray matchingBaseArgs = matchingBase.TypeArguments;
- Debug.Assert(matchingBaseArgs.Length == constructedBaseArgs.Length,
- "matchingBase and constructedBase share the same generic definition, so arity must match.");
-
- var substitution = new Dictionary(requiredParams.Count, SymbolEqualityComparer.Default);
- bool unified = true;
- for (int i = 0; i < matchingBaseArgs.Length; i++)
- {
- if (!matchingBaseArgs[i].TryUnifyWith(constructedBaseArgs[i], substitution))
- {
- unified = false;
- break;
- }
- }
-
- if (!unified)
- {
- continue;
- }
-
- // Unification succeeded for every position. Every required parameter must be
- // bound; otherwise the resulting closed type would have unbound type arguments
- // (an unspeakable type). A sibling ancestor may still bind this parameter, so
- // failure here is not fatal -- just move on to the next matching ancestor.
- bool allBound = true;
- foreach (ITypeParameterSymbol p in requiredParams)
- {
- if (!substitution.ContainsKey(p))
- {
- allBound = false;
- break;
- }
- }
- if (!allBound)
+ if (!_knownSymbols.Compilation.TryResolveOpenGenericDerivedType(
+ unboundDerived, constructedBase,
+ out INamedTypeSymbol? closedDerivedType, out failureReason))
{
- continue;
+ return false;
}
- successCount++;
- if (successCount == 1)
- {
- successfulSubstitution = substitution;
- }
- else
+ resolvedDerivedType = closedDerivedType;
+ }
+ else
+ {
+ if (baseType is INamedTypeSymbol { IsGenericType: true })
{
- failureReason = SR.Polymorphism_OpenGeneric_Reason_AmbiguousMatch;
+ // Closed derived registered against a generic base, e.g.
+ // [JsonDerivedType(typeof(Cat))] class Animal; class Cat : Animal;
+ // The same JsonDerivedType attribute lives on the open base definition and is
+ // shared across every closure, so a closed derived necessarily pins one
+ // specialization (here Animal) and would silently apply for that closure
+ // and break for every other (Animal, Animal, ...). Reject at
+ // metadata-resolution time.
+ failureReason = SR.Polymorphism_OpenGeneric_Reason_ClosedDerivedOnGenericBase;
return false;
}
- }
-
- if (successCount == 0 || successfulSubstitution is null)
- {
- failureReason = SR.Polymorphism_OpenGeneric_Reason_UnificationFailed;
- return false;
- }
- // Validate constraints up front so the generated code will compile.
- // Note: HasNotNullConstraint is not enforced because `notnull` is not a
- // runtime-enforced constraint. Reflection MakeGenericType accepts e.g.
- // string? for `where T : notnull`; source-gen must match that behavior.
- if (!_knownSymbols.Compilation.TryValidateGenericConstraints(requiredParams, successfulSubstitution, out ITypeParameterSymbol? failedParam, out ITypeSymbol? failedArg))
- {
- failureReason = string.Format(
- CultureInfo.InvariantCulture,
- SR.Polymorphism_OpenGeneric_Reason_ConstraintViolation,
- failedParam.Name,
- failedArg?.ToDisplayString() ?? string.Empty);
+ // Closed derived registered against a non-generic base. The common sensible
+ // case (e.g. [JsonDerivedType(typeof(Cat))] class Animal; class Cat : Animal;)
+ // is accepted unchanged here; outright invalid pairs (e.g.
+ // [JsonDerivedType(typeof(int))] class Foo;) are caught by the post-condition
+ // assignability check below.
+ resolvedDerivedType = declaredDerived;
+ }
+
+ // Post-condition: the resolved derived type must be a real subtype of the
+ // declared base type. For the open-derived / generic-base case this holds by
+ // construction (closure construction substitutes derived parameters with the
+ // matching base type arguments, so the matching ancestor of resolvedDerivedType
+ // is exactly baseType); the check is defensive and never expected to fail. For
+ // the closed-derived / non-generic-base case this is the only compile-time
+ // validation that the registration is well-formed and lifts a runtime-only
+ // failure (PolymorphicTypeResolver -> IsSupportedDerivedType ->
+ // ThrowInvalidOperationException_DerivedTypeNotSupported, surfaced at first
+ // serialization) to a SYSLIB1229 diagnostic at metadata-resolution time.
+ Debug.Assert(resolvedDerivedType is not null);
+ if (!baseType.IsAssignableFrom(resolvedDerivedType))
+ {
+ resolvedDerivedType = null;
+ failureReason = SR.Polymorphism_OpenGeneric_Reason_NotAssignable;
return false;
}
- // Build closedArgs in declaration order using the merged substitution.
- ITypeSymbol[] closedArgs = new ITypeSymbol[requiredParams.Count];
- for (int i = 0; i < requiredParams.Count; i++)
- {
- closedArgs[i] = successfulSubstitution[requiredParams[i]];
- }
-
- // Note: ConstructWithEnclosingTypeArguments takes the parameters in the order
- // they appear when listed as TypeParameters on the type and on its enclosing types.
- // GetAllTypeParameters preserves that order (outer-to-inner, declaration order).
- resolvedType = derivedDefinition.ConstructWithEnclosingTypeArguments(closedArgs);
return true;
}
- private static void CollectReferencedParameters(ITypeSymbol pattern, HashSet set)
- {
- switch (pattern)
- {
- case ITypeParameterSymbol tp:
- set.Add(tp);
- return;
-
- case IArrayTypeSymbol array:
- CollectReferencedParameters(array.ElementType, set);
- return;
-
- case IPointerTypeSymbol pointer:
- CollectReferencedParameters(pointer.PointedAtType, set);
- return;
-
- case INamedTypeSymbol { IsGenericType: true } named:
- // Walk ContainingType to collect type parameters declared on enclosing
- // generic types (e.g. T in Outer.Box). Roslyn's TypeArguments is
- // leaf-only, while the reflection mirror uses Type.GetGenericArguments()
- // which flattens enclosing + leaf. Without this recursion, the unbound
- // pre-check would spuriously reject patterns whose only reference to a
- // type parameter lives in the enclosing type.
- if (named.ContainingType is { IsGenericType: true } containing)
- {
- CollectReferencedParameters(containing, set);
- }
-
- foreach (ITypeSymbol arg in named.TypeArguments)
- {
- CollectReferencedParameters(arg, set);
- }
- return;
- }
- }
-
///
/// Checks whether the type is recognized as a union.
///
diff --git a/src/libraries/System.Text.Json/gen/Resources/Strings.resx b/src/libraries/System.Text.Json/gen/Resources/Strings.resx
index 53e5b8b6da5c33..b62d90eecbacef 100644
--- a/src/libraries/System.Text.Json/gen/Resources/Strings.resx
+++ b/src/libraries/System.Text.Json/gen/Resources/Strings.resx
@@ -231,13 +231,25 @@
the base type's arguments do not match the derived type's base specification
+
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+
+
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+
the type parameter '{0}' of the derived type is not bound by the base type's argumentsthe constraint on type parameter '{0}' is not satisfied by '{1}'
+
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+
the derived type matches the base type through multiple ancestors
+
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+
diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf
index 37a3849df5a877..481dd32a5314a6 100644
--- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf
+++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf
@@ -1,4 +1,4 @@
-
+
@@ -142,11 +142,31 @@
the derived type matches the base type through multiple ancestors
+
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+
+
+
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+
+ the constraint on type parameter '{0}' is not satisfied by '{1}'the constraint on type parameter '{0}' is not satisfied by '{1}'
+
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+
+
+
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+
+ the derived type is not assignable to the base typethe derived type is not assignable to the base type
diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf
index c4c87bcad068ff..52278a1b8050e9 100644
--- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf
+++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf
@@ -1,4 +1,4 @@
-
+
@@ -142,11 +142,31 @@
the derived type matches the base type through multiple ancestors
+
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+
+
+
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+
+ the constraint on type parameter '{0}' is not satisfied by '{1}'the constraint on type parameter '{0}' is not satisfied by '{1}'
+
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+
+
+
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+
+ the derived type is not assignable to the base typethe derived type is not assignable to the base type
diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf
index 35353d13ea067d..cced94257f4d07 100644
--- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf
+++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf
@@ -1,4 +1,4 @@
-
+
@@ -142,11 +142,31 @@
the derived type matches the base type through multiple ancestors
+
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+
+
+
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+
+ the constraint on type parameter '{0}' is not satisfied by '{1}'the constraint on type parameter '{0}' is not satisfied by '{1}'
+
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+
+
+
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+
+ the derived type is not assignable to the base typethe derived type is not assignable to the base type
diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf
index a7e186f4481687..d70082d3166db8 100644
--- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf
+++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf
@@ -1,4 +1,4 @@
-
+
@@ -142,11 +142,31 @@
the derived type matches the base type through multiple ancestors
+
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+
+
+
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+
+ the constraint on type parameter '{0}' is not satisfied by '{1}'the constraint on type parameter '{0}' is not satisfied by '{1}'
+
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+
+
+
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+
+ the derived type is not assignable to the base typethe derived type is not assignable to the base type
diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf
index 96df32b2a3b3db..60876e9d9a7a10 100644
--- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf
+++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf
@@ -1,4 +1,4 @@
-
+
@@ -142,11 +142,31 @@
the derived type matches the base type through multiple ancestors
+
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+
+
+
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+
+ the constraint on type parameter '{0}' is not satisfied by '{1}'the constraint on type parameter '{0}' is not satisfied by '{1}'
+
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+
+
+
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+
+ the derived type is not assignable to the base typethe derived type is not assignable to the base type
diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf
index 1daa27cd4d1a9c..d6fdda07369cd3 100644
--- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf
+++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf
@@ -1,4 +1,4 @@
-
+
@@ -142,11 +142,31 @@
the derived type matches the base type through multiple ancestors
+
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+
+
+
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+
+ the constraint on type parameter '{0}' is not satisfied by '{1}'the constraint on type parameter '{0}' is not satisfied by '{1}'
+
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+
+
+
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+
+ the derived type is not assignable to the base typethe derived type is not assignable to the base type
diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf
index 4674d4b373ec76..036ed21fd120b9 100644
--- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf
+++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf
@@ -1,4 +1,4 @@
-
+
@@ -142,11 +142,31 @@
the derived type matches the base type through multiple ancestors
+
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+
+
+
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+
+ the constraint on type parameter '{0}' is not satisfied by '{1}'the constraint on type parameter '{0}' is not satisfied by '{1}'
+
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+
+
+
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+
+ the derived type is not assignable to the base typethe derived type is not assignable to the base type
diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf
index 8e935683af738e..8bd68fb8a7d254 100644
--- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf
+++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf
@@ -1,4 +1,4 @@
-
+
@@ -142,11 +142,31 @@
the derived type matches the base type through multiple ancestors
+
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+
+
+
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+
+ the constraint on type parameter '{0}' is not satisfied by '{1}'the constraint on type parameter '{0}' is not satisfied by '{1}'
+
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+
+
+
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+
+ the derived type is not assignable to the base typethe derived type is not assignable to the base type
diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf
index 6bfde0ea0a9625..fbfcd81775c05c 100644
--- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf
+++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf
@@ -1,4 +1,4 @@
-
+
@@ -142,11 +142,31 @@
the derived type matches the base type through multiple ancestors
+
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+
+
+
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+
+ the constraint on type parameter '{0}' is not satisfied by '{1}'the constraint on type parameter '{0}' is not satisfied by '{1}'
+
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+
+
+
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+
+ the derived type is not assignable to the base typethe derived type is not assignable to the base type
diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf
index 02588202f0d75e..203be7f140154a 100644
--- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf
+++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf
@@ -1,4 +1,4 @@
-
+
@@ -142,11 +142,31 @@
the derived type matches the base type through multiple ancestors
+
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+
+
+
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+
+ the constraint on type parameter '{0}' is not satisfied by '{1}'the constraint on type parameter '{0}' is not satisfied by '{1}'
+
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+
+
+
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+
+ the derived type is not assignable to the base typethe derived type is not assignable to the base type
diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf
index 10a1129fee1aea..80937e2d60ae0f 100644
--- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf
+++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf
@@ -1,4 +1,4 @@
-
+
@@ -142,11 +142,31 @@
the derived type matches the base type through multiple ancestors
+
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+
+
+
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+
+ the constraint on type parameter '{0}' is not satisfied by '{1}'the constraint on type parameter '{0}' is not satisfied by '{1}'
+
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+
+
+
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+
+ the derived type is not assignable to the base typethe derived type is not assignable to the base type
diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf
index 91dd5b9d9f9e93..7d09aaf5061cdd 100644
--- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf
+++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf
@@ -1,4 +1,4 @@
-
+
@@ -142,11 +142,31 @@
the derived type matches the base type through multiple ancestors
+
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+
+
+
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+
+ the constraint on type parameter '{0}' is not satisfied by '{1}'the constraint on type parameter '{0}' is not satisfied by '{1}'
+
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+
+
+
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+
+ the derived type is not assignable to the base typethe derived type is not assignable to the base type
diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf
index 23e2ae9e44198f..c60e49b4022d3d 100644
--- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf
+++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf
@@ -1,4 +1,4 @@
-
+
@@ -142,11 +142,31 @@
the derived type matches the base type through multiple ancestors
+
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+
+
+
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+
+ the constraint on type parameter '{0}' is not satisfied by '{1}'the constraint on type parameter '{0}' is not satisfied by '{1}'
+
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+
+
+
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+
+ the derived type is not assignable to the base typethe derived type is not assignable to the base type
diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx
index 2f6ded325a9cb8..f0c701d7af1b78 100644
--- a/src/libraries/System.Text.Json/src/Resources/Strings.resx
+++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx
@@ -677,6 +677,12 @@
the base type's arguments do not match the derived type's base specification
+
+ the unification of the derived type with the base type binds a derived type parameter to a target other than a base type parameter
+
+
+ the derived type does not apply uniformly to every specialization of the base type because it pins one or more base type arguments
+
the type parameter '{0}' of the derived type is not bound by the base type's arguments
@@ -686,6 +692,12 @@
a type argument violates a generic constraint on the derived type
+
+ the derived type's type parameter '{0}' declares constraints that differ from the constraints on the corresponding base type parameter '{1}'
+
+
+ a closed derived type cannot serve a generic base type uniformly; the registration pins a specific specialization of the base
+
The polymorphic type '{0}' has already specified a type discriminator '{1}'.
diff --git a/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs b/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs
index def1570be7975c..0e099065e5f5e4 100644
--- a/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs
+++ b/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs
@@ -319,5 +319,143 @@ public static bool TryUnifyWith(this Type pattern, Type target, IDictionary
+ /// Returns if 's declared constraints
+ /// match 's declared constraints exactly, after applying
+ /// to type-constraint references. Used by the polymorphism
+ /// resolver to validate that every closure of the base type that respects the base's
+ /// constraints would also be a valid closure of the open derived type.
+ ///
+ /// The substitution is applied to nested type-parameter positions, so for F-bounded
+ /// constraints a derived where T : IComparable<T> matches a base
+ /// where U : IComparable<U> only after the substitution
+ /// { T -> U } has been applied to both occurrences.
+ ///
+ /// The compile-time-only notnull constraint is not surfaced through the
+ /// standard reflection API, so it is invisible here. As a consequence, two
+ /// registrations whose constraints differ only in the presence of notnull
+ /// are accepted as equivalent.
+ ///
+ /// IMPORTANT: This implementation MIRRORS
+ /// System.Text.Json.SourceGeneration.RoslynExtensions.AreConstraintsEquivalent.
+ /// Any change to the equivalence rules must be applied on both sides.
+ ///
+ /// Known intentional asymmetry with the source-gen mirror: the C# unmanaged
+ /// constraint is encoded as a modreq and is not surfaced through the standard
+ /// reflection API, so this check cannot enforce that a derived unmanaged
+ /// constraint is matched by a base unmanaged constraint. The polymorphism
+ /// resolver falls back on to surface any
+ /// remaining constraint violations at closure time.
+ ///
+ [RequiresUnreferencedCode("Reflects over derived and base generic parameter constraint types.")]
+ [RequiresDynamicCode("Substitutes type parameters in constraint types.")]
+ public static bool AreConstraintsEquivalent(
+ Type derivedParam,
+ Type baseParam,
+ IReadOnlyDictionary substitution)
+ {
+ Debug.Assert(derivedParam.IsGenericParameter);
+ Debug.Assert(baseParam.IsGenericParameter);
+
+ const GenericParameterAttributes Mask = GenericParameterAttributes.SpecialConstraintMask;
+ if ((derivedParam.GenericParameterAttributes & Mask) != (baseParam.GenericParameterAttributes & Mask))
+ {
+ return false;
+ }
+
+ Type[] derivedConstraints = derivedParam.GetGenericParameterConstraints();
+ Type[] baseConstraints = baseParam.GetGenericParameterConstraints();
+ if (derivedConstraints.Length != baseConstraints.Length)
+ {
+ return false;
+ }
+
+ if (derivedConstraints.Length == 0)
+ {
+ return true;
+ }
+
+ // Compare type-constraint sets order-independently. Substitute each derived
+ // constraint into base-parameter terms via the canonical mapping, then check
+ // that the resulting multiset matches the base's constraint set. Length parity
+ // was already established above, so removing each substituted derived constraint
+ // from a fresh copy of the base set proves equality iff every Remove succeeds.
+ var baseSet = new HashSet(baseConstraints);
+ foreach (Type derivedConstraint in derivedConstraints)
+ {
+ Type substituted = SubstituteTypeParameters(derivedConstraint, substitution);
+ if (!baseSet.Remove(substituted))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Walks a tree and substitutes any occurrence of a type parameter
+ /// (whose key is present in ) with its mapped value.
+ ///
+ [RequiresUnreferencedCode("Reflects over the type tree to build a substituted constraint type.")]
+ [RequiresDynamicCode("Constructs a new substituted generic type via MakeGenericType.")]
+ private static Type SubstituteTypeParameters(Type type, IReadOnlyDictionary substitution)
+ {
+ if (type.IsGenericParameter)
+ {
+ return substitution.TryGetValue(type, out Type? mapped) ? mapped : type;
+ }
+
+ if (type.IsArray)
+ {
+ Type element = type.GetElementType()!;
+ Type substitutedElement = SubstituteTypeParameters(element, substitution);
+ if (substitutedElement == element)
+ {
+ return type;
+ }
+
+#if NET
+ return type.IsSZArray ? substitutedElement.MakeArrayType() : substitutedElement.MakeArrayType(type.GetArrayRank());
+#else
+ int rank = type.GetArrayRank();
+ return rank == 1 ? substitutedElement.MakeArrayType() : substitutedElement.MakeArrayType(rank);
+#endif
+ }
+
+ if (type.IsPointer)
+ {
+ Type element = type.GetElementType()!;
+ Type substitutedElement = SubstituteTypeParameters(element, substitution);
+ return substitutedElement == element ? type : substitutedElement.MakePointerType();
+ }
+
+ if (type.IsByRef)
+ {
+ Type element = type.GetElementType()!;
+ Type substitutedElement = SubstituteTypeParameters(element, substitution);
+ return substitutedElement == element ? type : substitutedElement.MakeByRefType();
+ }
+
+ if (type.IsGenericType)
+ {
+ Type[] args = type.GetGenericArguments();
+ Type[]? newArgs = null;
+ for (int i = 0; i < args.Length; i++)
+ {
+ Type substitutedArg = SubstituteTypeParameters(args[i], substitution);
+ if (substitutedArg != args[i])
+ {
+ newArgs ??= (Type[])args.Clone();
+ newArgs[i] = substitutedArg;
+ }
+ }
+
+ return newArgs is null ? type : type.GetGenericTypeDefinition().MakeGenericType(newArgs);
+ }
+
+ return type;
+ }
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs
index ef694d8351dda7..97cefc14f0f01a 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs
@@ -124,7 +124,7 @@ private static void ResolveOpenGenericDerivedTypes(Type baseType, IList
- /// Reflection-side resolver: closes against the
- /// constructed base type identified by (,
- /// ) via structural unification.
+ /// Reflection-side resolver: validates that applies
+ /// uniformly to every specialization of the open generic
+ /// , and (when it does) produces the closed
+ /// derived type for the specific closure identified by
+ /// .
+ ///
+ /// "Uniform" means: there is a single canonical substitution mapping each derived
+ /// type parameter to a base type parameter that simultaneously satisfies every
+ /// matching ancestor of the derived type, with every derived constraint implied by
+ /// (i.e. weaker-than-or-equal-to) the constraints on the corresponding base parameter.
+ /// Per-ancestor unifications are computed independently and then verified to coincide
+ /// -- this is what catches asymmetric multi-interface implementations like
+ /// D<U1, U2> : IBase<U1, U2>, IBase<U2, U1>, where each
+ /// ancestor admits a unifier on its own but no single canonical answer covers both.
+ /// Registrations that pin a particular specialization
+ /// (e.g. Derived<T> : Base<T, int>) are rejected: such registrations
+ /// would silently work for one base specialization and break for another, which we
+ /// treat as a misregistration regardless of which specialization is currently being
+ /// constructed.
///
/// IMPORTANT: This implementation MIRRORS the source-gen resolver
- /// JsonSourceGenerator.Parser.TryResolveOpenGenericDerivedType in
- /// gen/JsonSourceGenerator.Parser.cs. Both implementations -- the structural
- /// unbound pre-check, the per-ancestor unification, and the ambiguity
- /// detection -- must be kept in lockstep so that reflection and source-gen
- /// produce the same closed type for the same registration. Any algorithmic
- /// change here must be applied in the source-gen mirror as well.
+ /// RoslynExtensions.TryResolveOpenGenericDerivedType in
+ /// gen/Helpers/RoslynExtensions.cs. Both implementations -- the per-ancestor
+ /// unification, the canonical-substitution consistency check, and the
+ /// constraint-subsumption rules -- must be kept in lockstep so that reflection and
+ /// source-gen produce the same closed type for the same registration.
///
- /// Known intentional asymmetry with the source-gen mirror: source-gen rejects a
- /// managed value type (e.g. a struct containing reference fields) supplied for a
- /// where T : unmanaged constraint because emitting such a closed type would
- /// produce a C# compile error (CS8377). The reflection resolver, by contrast,
- /// delegates constraint validation to , which only
- /// enforces the underlying value-type part of the constraint at runtime (the
- /// unmanaged modreq is not surfaced through standard reflection metadata).
- /// As a result, reflection accepts managed structs for unmanaged-constrained
- /// derived types where source-gen rejects them. This divergence cannot be bridged
- /// without emitting invalid C# code on the source-gen side.
+ /// Known intentional asymmetry with the source-gen mirror: standard reflection does
+ /// not surface the unmanaged generic constraint (it is encoded as a modreq),
+ /// so reflection's constraint-subsumption check cannot enforce that a derived
+ /// unmanaged constraint is matched by a base unmanaged constraint.
+ /// Source-gen, which inspects Roslyn's constraint metadata directly, can and does
+ /// enforce this. Reflection therefore falls back on
+ /// to surface any remaining constraint violations at closure time.
///
[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
@@ -192,10 +225,10 @@ private static bool TryResolveOpenGenericDerivedType(
closedDerivedType = null;
failureReason = null;
- // Find every ancestor of the open derived type whose generic type definition matches
- // the base type definition. For classes there is at most one such ancestor, but for
- // interfaces a derived type can implement the same interface definition multiple times
- // with different type arguments (e.g. Derived : IBase, IBase>).
+ // Every ancestor of the open derived type whose generic type definition matches
+ // the base type definition. For classes there is at most one such ancestor; for
+ // interfaces a derived type can implement the same interface definition multiple
+ // times with different type arguments (e.g. Derived : IBase, IBase>).
List matchingBases = new();
foreach (Type match in openDerivedType.GetMatchingGenericBaseTypes(baseTypeDefinition))
{
@@ -208,135 +241,133 @@ private static bool TryResolveOpenGenericDerivedType(
return false;
}
- // The full set of generic parameters we must bind includes the parameters of the
- // derived type itself plus any parameters declared by enclosing generic types
- // (e.g. Outer.Derived needs T bound from the outer class).
- // Type.GetGenericArguments() on an open generic type returns this complete set.
+ // The complete set of derived parameters that must be bound (enclosing + leaf
+ // flattened: Type.GetGenericArguments() on a nested generic returns enclosing+leaf).
Type[] requiredParams = openDerivedType.GetGenericArguments();
+ Type[] baseParams = baseTypeDefinition.GetGenericArguments();
+ var baseParamSet = new HashSet(baseParams);
- // Structural unbound pre-check: every required parameter must appear at least once
- // somewhere in some matching ancestor's type arguments. If a parameter never appears
- // at all, no closed base could ever bind it -- the derived definition is malformed
- // regardless of which closed base it is registered against.
- HashSet referencedParams = new();
- foreach (Type mb in matchingBases)
- {
- foreach (Type arg in mb.GetGenericArguments())
- {
- CollectReferencedParameters(arg, referencedParams);
- }
- }
- foreach (Type required in requiredParams)
+ // Per-ancestor independent substitutions; the uniform answer must be a single
+ // canonical substitution agreed upon by every ancestor.
+ Dictionary? canonical = null;
+
+ foreach (Type ancestor in matchingBases)
{
- if (!referencedParams.Contains(required))
+ var substitution = new Dictionary(requiredParams.Length);
+ if (!ancestor.TryUnifyWith(baseTypeDefinition, substitution))
{
- failureReason = SR.Format(SR.Polymorphism_OpenGeneric_Reason_UnboundParameter, required.Name);
+ // No unifier exists. Some position pins a concrete type (e.g. Base)
+ // or a constructed pattern (e.g. Base>) that cannot match the base
+ // type parameter at that position (the rigid target).
+ failureReason = SR.Polymorphism_OpenGeneric_Reason_NonUniformPinning;
return false;
}
- }
-
- Type[]? successfulArgs = null;
- int successCount = 0;
-
- foreach (Type matchingBase in matchingBases)
- {
- Type[] matchingBaseArgs = matchingBase.GetGenericArguments();
- Debug.Assert(matchingBaseArgs.Length == baseTypeArgs.Length,
- "matchingBase and baseTypeArgs share the same generic type definition, so arity must match.");
- var substitution = new Dictionary(requiredParams.Length);
- bool unified = true;
- for (int i = 0; i < matchingBaseArgs.Length; i++)
+ foreach (Type p in requiredParams)
{
- if (!matchingBaseArgs[i].TryUnifyWith(baseTypeArgs[i], substitution))
+ if (!substitution.TryGetValue(p, out Type? mapped))
{
- unified = false;
- break;
+ // E.g. D : IBase -- U2 is not bound by this ancestor.
+ failureReason = SR.Format(SR.Polymorphism_OpenGeneric_Reason_UnboundParameter, p.Name);
+ return false;
}
- }
-
- if (!unified)
- {
- continue;
- }
- // Unification succeeded for every position. Every required parameter of the
- // derived type definition must be bound by this ancestor; otherwise the
- // resulting closed type would have unbound type arguments (an unspeakable type).
- // A sibling ancestor may still bind this parameter, so failure here is not fatal.
- Type[] closedArgs = new Type[requiredParams.Length];
- bool allBound = true;
- for (int i = 0; i < requiredParams.Length; i++)
- {
- if (!substitution.TryGetValue(requiredParams[i], out Type? boundArg))
+ if (!mapped.IsGenericParameter || !baseParamSet.Contains(mapped))
{
- allBound = false;
- break;
+ // Defensive: a unifier exists but it would map a derived parameter to
+ // something other than one of the base's own type parameters (i.e. the
+ // result isn't a pure renaming). With the rigid-target unification used
+ // here this is essentially unreachable -- TryUnifyWith binds derived
+ // parameters only against the base definition's own type arguments,
+ // which are all base parameters -- but we report it separately from
+ // NonUniformPinning so any future relaxation of TryUnifyWith surfaces
+ // with a precise diagnostic instead of getting silently lumped under
+ // pinning.
+ failureReason = SR.Polymorphism_OpenGeneric_Reason_NonUniformUnification;
+ return false;
}
-
- closedArgs[i] = boundArg;
}
- if (!allBound)
+ if (canonical is null)
{
- continue;
+ canonical = substitution;
}
-
- successCount++;
- if (successCount == 1)
+ else if (!SubstitutionsEqual(canonical, substitution))
{
- successfulArgs = closedArgs;
+ // Two ancestors agree on independent bindings but produce different
+ // (derived -> base) mappings, e.g. D : IBase, IBase.
+ // There is no single canonical answer for an arbitrary base closure.
+ failureReason = SR.Polymorphism_OpenGeneric_Reason_AmbiguousMatch;
+ return false;
}
- else
+ }
+
+ Debug.Assert(canonical is not null);
+
+ // Constraint equivalence: every derived parameter's constraints must exactly
+ // match the constraints on the mapped base parameter (after substitution) so
+ // that any valid closure of the base also yields a valid closure of the derived.
+ // See ReflectionExtensions.AreConstraintsEquivalent for the rationale behind
+ // exact match (vs one-sided subsumption).
+ foreach (Type derivedParam in requiredParams)
+ {
+ Type mappedBaseParam = canonical[derivedParam];
+ if (!ReflectionExtensions.AreConstraintsEquivalent(derivedParam, mappedBaseParam, canonical))
{
- failureReason = SR.Polymorphism_OpenGeneric_Reason_AmbiguousMatch;
+ failureReason = SR.Format(SR.Polymorphism_OpenGeneric_Reason_ConstraintMismatch,
+ derivedParam.Name, mappedBaseParam.Name);
return false;
}
}
- if (successCount == 0 || successfulArgs is null)
+ // Closure construction: substitute the canonical mapping then specialize each
+ // base parameter slot to the actual closed-base type argument.
+ var baseParamPosition = new Dictionary(baseParams.Length);
+ for (int i = 0; i < baseParams.Length; i++)
{
- failureReason = SR.Polymorphism_OpenGeneric_Reason_UnificationFailed;
- return false;
+ baseParamPosition[baseParams[i]] = i;
+ }
+
+ Type[] closedArgs = new Type[requiredParams.Length];
+ for (int i = 0; i < requiredParams.Length; i++)
+ {
+ Type mappedBaseParam = canonical[requiredParams[i]];
+ closedArgs[i] = baseTypeArgs[baseParamPosition[mappedBaseParam]];
}
try
{
- closedDerivedType = openDerivedType.MakeGenericType(successfulArgs);
+ closedDerivedType = openDerivedType.MakeGenericType(closedArgs);
return true;
}
catch (Exception ex) when (ex is ArgumentException or TypeLoadException)
{
- // Constraint violation or load failure (e.g. unmanaged constraint, which is
- // not observable via the standard reflection constraint metadata). We use a
- // structured reason rather than ex.Message so that the outer template — which
- // appends its own trailing period — never produces a double period.
+ // Defense in depth: MakeGenericType can still throw for constraints the
+ // subsumption check above cannot observe (e.g. derived `unmanaged`
+ // constraint, which is not surfaced by standard reflection metadata).
+ // Use a structured reason rather than ex.Message so the outer template --
+ // which appends its own trailing period -- never produces a double period.
failureReason = SR.Polymorphism_OpenGeneric_Reason_ConstraintViolation;
return false;
}
}
- private static void CollectReferencedParameters(Type pattern, HashSet set)
+ private static bool SubstitutionsEqual(Dictionary a, Dictionary b)
{
- if (pattern.IsGenericParameter)
+ if (a.Count != b.Count)
{
- set.Add(pattern);
- return;
- }
-
- if (pattern.HasElementType)
- {
- CollectReferencedParameters(pattern.GetElementType()!, set);
- return;
+ return false;
}
- if (pattern.IsGenericType)
+ foreach (KeyValuePair kvp in a)
{
- foreach (Type arg in pattern.GetGenericArguments())
+ if (!b.TryGetValue(kvp.Key, out Type? other) || other != kvp.Value)
{
- CollectReferencedParameters(arg, set);
+ return false;
}
}
+
+ return true;
}
[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
diff --git a/src/libraries/System.Text.Json/tests/Common/JsonCreationHandlingTests.Object.cs b/src/libraries/System.Text.Json/tests/Common/JsonCreationHandlingTests.Object.cs
index bd91642bbe7f70..ace332ee1710ca 100644
--- a/src/libraries/System.Text.Json/tests/Common/JsonCreationHandlingTests.Object.cs
+++ b/src/libraries/System.Text.Json/tests/Common/JsonCreationHandlingTests.Object.cs
@@ -548,8 +548,8 @@ await Assert.ThrowsAsync(
async () => await Serializer.DeserializeWrapper(json, options));
}
- [JsonDerivedType(typeof(BaseClassWithPolymorphism), "base")]
- [JsonDerivedType(typeof(DerivedClass_DerivingFrom_BaseClassWithPolymorphism), "derived")]
+ [JsonDerivedType(typeof(BaseClassRecursive), "base")]
+ [JsonDerivedType(typeof(DerivedClass_DerivingFrom_BaseClassRecursive), "derived")]
public class BaseClassRecursive
{
[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PolymorphismTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PolymorphismTests.cs
index a8ae6c0991b189..198b2ec5e9b3b0 100644
--- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PolymorphismTests.cs
+++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PolymorphismTests.cs
@@ -75,25 +75,116 @@ public static void CollectionPolymorphismOptions_AreGenerated()
}
[Fact]
- public static void OpenGenericDerivedType_PartiallyConcrete_RoundTrips()
+ public static void OpenGenericDerivedType_NonUniformPinning_IsDroppedFromMetadata()
{
- // SourceGenOpenGenericDerived : SourceGenOpenGenericBase registered on
- // SourceGenOpenGenericBase. Position 0 (T) unifies to string;
- // position 1 (concrete int) matches. The generated metadata for the closed base
- // must contain SourceGenOpenGenericDerived as the resolved derived type.
+ // SourceGenOpenGenericDerived : SourceGenOpenGenericBase pins T2 to
+ // int -- non-uniform w.r.t. SourceGenOpenGenericBase. Source-gen
+ // emits SYSLIB1229 (suppressed on the fixture decoration) and drops the
+ // registration from the generated metadata; the closed base has no
+ // PolymorphismOptions and serializes plainly.
JsonTypeInfo typeInfo = PolymorphismTestsContext.Default.SourceGenOpenGenericBaseStringInt32;
- JsonPolymorphismOptions options = Assert.IsType(typeInfo.PolymorphismOptions);
- JsonDerivedType derivedType = Assert.Single(options.DerivedTypes);
- Assert.Equal(typeof(SourceGenOpenGenericDerived), derivedType.DerivedType);
- Assert.Equal("derived", derivedType.TypeDiscriminator);
+ Assert.Null(typeInfo.PolymorphismOptions);
SourceGenOpenGenericBase value = new SourceGenOpenGenericDerived { Extra = "hello" };
string json = JsonSerializer.Serialize(value, typeInfo);
- Assert.Contains("\"$type\":\"derived\"", json);
+ Assert.DoesNotContain("$type", json);
+ }
+
+ [Fact]
+ public static void ClosedDerivedOnGenericBase_IsDroppedOnIntSpec()
+ {
+ // Under the uniform-applicability rule, closed-derived registrations on an
+ // OPEN generic base (here SourceGenSpecAnimal_Cat : SourceGenSpecAnimal
+ // and Dog : SourceGenSpecAnimal) are non-uniform -- they can never
+ // apply to every closure of the open base. Source-gen drops them from emitted
+ // metadata (with SYSLIB1229 suppressed on the fixture) so the closed base has
+ // no PolymorphismOptions and serializes plainly.
+ JsonTypeInfo typeInfo = PolymorphismTestsContext.Default.SourceGenSpecAnimalInt;
+ Assert.Null(typeInfo.PolymorphismOptions);
+
+ SourceGenSpecAnimal cat = new SourceGenSpecAnimal_Cat { Name = "Felix", Lives = 9 };
+ string json = JsonSerializer.Serialize(cat, typeInfo);
+ Assert.DoesNotContain("$type", json);
+ }
+
+ [Fact]
+ public static void ClosedDerivedOnGenericBase_IsDroppedOnStringSpec()
+ {
+ // Companion to ClosedDerivedOnGenericBase_IsDroppedOnIntSpec for the string
+ // closure. Same rejection regardless of which closure is constructed.
+ JsonTypeInfo typeInfo = PolymorphismTestsContext.Default.SourceGenSpecAnimalString;
+ Assert.Null(typeInfo.PolymorphismOptions);
+
+ SourceGenSpecAnimal dog = new SourceGenSpecAnimal_Dog { Name = "Rex", Breed = "Husky" };
+ string json = JsonSerializer.Serialize(dog, typeInfo);
+ Assert.DoesNotContain("$type", json);
+ }
+
+ [Fact]
+ public static void ClosedDerivedOnGenericBase_IsDroppedOnUnrelatedSpec()
+ {
+ // The bool closure of SourceGenSpecAnimal -- no closed-derived registration
+ // would have matched even under per-closure filtering. The emitted typeInfo has
+ // no PolymorphismOptions and serializes plainly.
+ JsonTypeInfo typeInfo = PolymorphismTestsContext.Default.SourceGenSpecAnimalBool;
+ Assert.Null(typeInfo.PolymorphismOptions);
+
+ string json = JsonSerializer.Serialize(new SourceGenSpecAnimal { Name = "Cookie" }, typeInfo);
+ Assert.DoesNotContain("$type", json);
+ Assert.Contains("\"Name\":\"Cookie\"", json);
+ }
+
+ [Fact]
+ public static void OpenDerivedWithNarrowerConstraint_IsDroppedOnUnsatisfyingSpec()
+ {
+ // SourceGenConstrainedDerived where T : struct, registered on
+ // SourceGenConstrainedBase. The derived's struct constraint is narrower
+ // than the base's (none), so the registration is non-uniform under B-strict.
+ // Source-gen emits SYSLIB1229 (suppressed on the fixture) and drops the
+ // registration; every closure of the base has null PolymorphismOptions.
+ JsonTypeInfo typeInfo = PolymorphismTestsContext.Default.SourceGenConstrainedBaseString;
+ Assert.Null(typeInfo.PolymorphismOptions);
+
+ string json = JsonSerializer.Serialize(new SourceGenConstrainedBase { Value = "hi" }, typeInfo);
+ Assert.DoesNotContain("$type", json);
+ }
+
+ [Fact]
+ public static void OpenDerivedWithNarrowerConstraint_IsDroppedOnSatisfyingSpec()
+ {
+ // Same fixture as above, applied to a closure (int) where the derived's
+ // struct constraint happens to hold. Under per-closure filtering this would
+ // have been accepted; under B-strict uniform applicability it is rejected
+ // uniformly so that introducing a new closure (e.g. ) cannot break a
+ // working serialization site.
+ JsonTypeInfo typeInfo = PolymorphismTestsContext.Default.SourceGenConstrainedBaseInt;
+ Assert.Null(typeInfo.PolymorphismOptions);
+
+ string json = JsonSerializer.Serialize(new SourceGenConstrainedBase { Value = 1 }, typeInfo);
+ Assert.DoesNotContain("$type", json);
+ }
- var result = JsonSerializer.Deserialize(json, typeInfo);
- var d = Assert.IsType>(result);
- Assert.Equal("hello", d.Extra);
+ [Fact]
+ public static void OpenDerivedWithExtraUnboundParameter_BadArmIsDroppedFromEmittedMetadata()
+ {
+ // Two registrations on the same base: SourceGenExtraParam_Cat is uniform
+ // and resolves to SourceGenExtraParam_Cat; SourceGenExtraParam_Cat
+ // has an extra unbound T2 that the single-parameter base cannot pin down, so
+ // source-gen emits SYSLIB1229 (suppressed below) and drops it from the
+ // generated metadata. Only the well-formed "cat" arm survives in the emitted
+ // typeInfo.
+ //
+ // This is the documented source-gen-vs-reflection asymmetry: source-gen drops
+ // bad arms per-attribute, reflection throws on the first bad arm.
+ JsonTypeInfo typeInfo = PolymorphismTestsContext.Default.SourceGenExtraParamAnimalInt;
+ JsonPolymorphismOptions options = Assert.IsType(typeInfo.PolymorphismOptions);
+ JsonDerivedType derivedType = Assert.Single(options.DerivedTypes);
+ Assert.Equal(typeof(SourceGenExtraParam_Cat), derivedType.DerivedType);
+ Assert.Equal("cat", derivedType.TypeDiscriminator);
+
+ SourceGenExtraParamAnimal value = new SourceGenExtraParam_Cat { Name = "Felix", Tag = 7 };
+ string json = JsonSerializer.Serialize(value, typeInfo);
+ Assert.Contains("\"$type\":\"cat\"", json);
}
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
@@ -101,12 +192,24 @@ public static void OpenGenericDerivedType_PartiallyConcrete_RoundTrips()
[JsonSerializable(typeof(SourceGenClassifiedAnimal))]
[JsonSerializable(typeof(SourceGenPolymorphicIntList))]
[JsonSerializable(typeof(SourceGenOpenGenericBase))]
+ [JsonSerializable(typeof(SourceGenSpecAnimal), TypeInfoPropertyName = "SourceGenSpecAnimalInt")]
+ [JsonSerializable(typeof(SourceGenSpecAnimal), TypeInfoPropertyName = "SourceGenSpecAnimalString")]
+ [JsonSerializable(typeof(SourceGenSpecAnimal), TypeInfoPropertyName = "SourceGenSpecAnimalBool")]
+ [JsonSerializable(typeof(SourceGenConstrainedBase), TypeInfoPropertyName = "SourceGenConstrainedBaseString")]
+ [JsonSerializable(typeof(SourceGenConstrainedBase), TypeInfoPropertyName = "SourceGenConstrainedBaseInt")]
+ [JsonSerializable(typeof(SourceGenExtraParamAnimal), TypeInfoPropertyName = "SourceGenExtraParamAnimalInt")]
internal sealed partial class PolymorphismTestsContext : JsonSerializerContext
{
}
}
+ // SourceGenOpenGenericDerived : SourceGenOpenGenericBase pins the second
+ // base parameter to a concrete type -- non-uniform under B-strict. Source-gen emits
+ // SYSLIB1229 at compile time; suppress it here so we can verify the runtime drops the
+ // registration and the closed base serializes plainly.
+#pragma warning disable SYSLIB1229
[JsonDerivedType(typeof(SourceGenOpenGenericDerived<>), "derived")]
+#pragma warning restore SYSLIB1229
public class SourceGenOpenGenericBase
{
public T1? Value1 { get; set; }
@@ -118,6 +221,64 @@ public sealed class SourceGenOpenGenericDerived : SourceGenOpenGenericBase
+ {
+ public string? Name { get; set; }
+ }
+
+ public sealed class SourceGenSpecAnimal_Cat : SourceGenSpecAnimal { public int Lives { get; set; } }
+ public sealed class SourceGenSpecAnimal_Dog : SourceGenSpecAnimal { public string? Breed { get; set; } }
+
+ // Constraint-on-derived: the source generator emits SYSLIB1229 for the
+ // SourceGenConstrainedBase specialization because string does not satisfy
+ // the `where T : struct` constraint. The warning is benign here -- the bad
+ // registration is dropped from generated metadata and the closed base serializes
+ // as a plain (non-polymorphic) type.
+#pragma warning disable SYSLIB1229
+ [JsonDerivedType(typeof(SourceGenConstrainedDerived<>), "derived")]
+#pragma warning restore SYSLIB1229
+ public class SourceGenConstrainedBase
+ {
+ public T? Value { get; set; }
+ }
+
+ public sealed class SourceGenConstrainedDerived : SourceGenConstrainedBase where T : struct
+ {
+ public int Extra { get; set; }
+ }
+
+ [JsonDerivedType(typeof(SourceGenExtraParam_Cat<>), "cat")]
+ // SourceGenExtraParam_Cat has an extra unbound T2 that the single-parameter
+ // base cannot pin down. The source generator emits SYSLIB1229; suppress it here so
+ // we can verify the runtime drops the bad arm and keeps the well-formed "cat" arm.
+#pragma warning disable SYSLIB1229
+ [JsonDerivedType(typeof(SourceGenExtraParam_Cat<,>), "cat2")]
+#pragma warning restore SYSLIB1229
+ public class SourceGenExtraParamAnimal
+ {
+ public T? Tag { get; set; }
+ }
+
+ public class SourceGenExtraParam_Cat : SourceGenExtraParamAnimal
+ {
+ public string? Name { get; set; }
+ }
+
+ public class SourceGenExtraParam_Cat : SourceGenExtraParamAnimal
+ {
+ public T2? Extra { get; set; }
+ }
+
[JsonPolymorphic(
TypeDiscriminatorPropertyName = "$kind",
IgnoreUnrecognizedTypeDiscriminators = true,
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorDiagnosticsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorDiagnosticsTests.cs
index 8dfd87b307bccb..81002eb8ff11d3 100644
--- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorDiagnosticsTests.cs
+++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorDiagnosticsTests.cs
@@ -1264,11 +1264,13 @@ public class MyDerived : MyBase
}
[Fact]
- public void OpenGenericDerivedType_PartiallyConcrete_Resolves()
+ public void OpenGenericDerivedType_PartiallyConcrete_WarnsWithSYSLIB1229()
{
// Derived : Base registered on Base:
- // position 0 (T) unifies with string, position 1 (concrete int) matches.
- // Expected: closed type is MyDerived, no diagnostic.
+ // the derived pins position 1 (T2) to a concrete int, which makes the
+ // registration non-uniform w.r.t. the open Base. Under the
+ // uniform-applicability rule the source generator emits SYSLIB1229
+ // regardless of which closure was registered.
string source = """
using System.Text.Json.Serialization;
@@ -1294,9 +1296,10 @@ public class MyDerived : MyBase
""";
Compilation compilation = CompilationHelper.CreateCompilation(source);
- JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
+ JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true);
- Assert.Empty(result.Diagnostics.Where(d => d.Id == "SYSLIB1229"));
+ Diagnostic diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "SYSLIB1229");
+ Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
}
[Fact]
@@ -1332,6 +1335,181 @@ public class MyDerived : MyBase
Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
}
+ [Fact]
+ public void ClosedDerivedType_NotAssignableToNonGenericBase_WarnsWithSYSLIB1229()
+ {
+ // A closed derived type that has no inheritance relationship to a non-generic
+ // base used to flow through the source generator unchanged and only fail at
+ // first serialization, when PolymorphicTypeResolver applies its
+ // IsAssignableFrom check and throws InvalidOperationException. The
+ // post-condition assignability check in TryResolveDerivedType lifts that
+ // failure to a compile-time SYSLIB1229 so misregistrations are surfaced when
+ // the metadata is generated.
+ string source = """
+ using System.Text.Json.Serialization;
+
+ namespace HelloWorld
+ {
+ [JsonSerializable(typeof(MyBase))]
+ internal partial class JsonContext : JsonSerializerContext
+ {
+ }
+
+ [JsonDerivedType(typeof(Unrelated), "unrelated")]
+ public class MyBase
+ {
+ public int Value { get; set; }
+ }
+
+ public class Unrelated
+ {
+ public string? Name { get; set; }
+ }
+ }
+ """;
+
+ Compilation compilation = CompilationHelper.CreateCompilation(source);
+ JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true);
+
+ Diagnostic diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "SYSLIB1229");
+ Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
+ string message = diagnostic.GetMessage();
+ Assert.Contains("Unrelated", message);
+ Assert.Contains("MyBase", message);
+ Assert.Contains("not assignable", message);
+ }
+
+ [Fact]
+ public void ClosedDerivedType_AssignableToNonGenericBase_CompilesSuccessfully()
+ {
+ // The standard happy path for the closed-derived / non-generic-base arm:
+ // confirm the new post-condition assignability check does not regress valid
+ // registrations.
+ string source = """
+ using System.Text.Json.Serialization;
+
+ namespace HelloWorld
+ {
+ [JsonSerializable(typeof(Animal))]
+ internal partial class JsonContext : JsonSerializerContext
+ {
+ }
+
+ [JsonDerivedType(typeof(Cat), "cat")]
+ [JsonDerivedType(typeof(Dog), "dog")]
+ public class Animal
+ {
+ public string? Name { get; set; }
+ }
+
+ public class Cat : Animal { public int Lives { get; set; } }
+ public class Dog : Animal { public bool Bark { get; set; } }
+ }
+ """;
+
+ Compilation compilation = CompilationHelper.CreateCompilation(source);
+ JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true);
+
+ Assert.Empty(result.Diagnostics.Where(d => d.Id == "SYSLIB1229"));
+ }
+
+ [Fact]
+ public void ClosedDerivedType_NonGenericInterfaceBase_AssignableViaInterface_CompilesSuccessfully()
+ {
+ // Variant of the happy path where the non-generic base is an interface and
+ // the derived type satisfies the base via an interface implementation. The
+ // IsAssignableFrom helper walks AllInterfaces in addition to the base-type
+ // chain, so this should compile without diagnostics.
+ string source = """
+ using System.Text.Json.Serialization;
+
+ namespace HelloWorld
+ {
+ [JsonSerializable(typeof(IShape))]
+ internal partial class JsonContext : JsonSerializerContext
+ {
+ }
+
+ [JsonDerivedType(typeof(Circle), "circle")]
+ public interface IShape
+ {
+ }
+
+ public class Circle : IShape
+ {
+ public double Radius { get; set; }
+ }
+ }
+ """;
+
+ Compilation compilation = CompilationHelper.CreateCompilation(source);
+ JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true);
+
+ Assert.Empty(result.Diagnostics.Where(d => d.Id == "SYSLIB1229"));
+ }
+
+ [Fact]
+ public void ClosedDerivedType_NotAssignableToInterfaceBase_WarnsWithSYSLIB1229()
+ {
+ // Closed derived that does not implement the declared interface base.
+ string source = """
+ using System.Text.Json.Serialization;
+
+ namespace HelloWorld
+ {
+ [JsonSerializable(typeof(IShape))]
+ internal partial class JsonContext : JsonSerializerContext
+ {
+ }
+
+ [JsonDerivedType(typeof(NotAShape), "no")]
+ public interface IShape
+ {
+ }
+
+ public class NotAShape
+ {
+ }
+ }
+ """;
+
+ Compilation compilation = CompilationHelper.CreateCompilation(source);
+ JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true);
+
+ Diagnostic diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "SYSLIB1229");
+ Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
+ Assert.Contains("not assignable", diagnostic.GetMessage());
+ }
+
+ [Fact]
+ public void ClosedDerivedType_ValueTypeRegisteredOnReferenceTypeBase_WarnsWithSYSLIB1229()
+ {
+ // Value type with no inheritance relationship to a class base.
+ string source = """
+ using System.Text.Json.Serialization;
+
+ namespace HelloWorld
+ {
+ [JsonSerializable(typeof(MyBase))]
+ internal partial class JsonContext : JsonSerializerContext
+ {
+ }
+
+ [JsonDerivedType(typeof(int), "i")]
+ public class MyBase
+ {
+ }
+ }
+ """;
+
+ Compilation compilation = CompilationHelper.CreateCompilation(source);
+ JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true);
+
+ Diagnostic diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "SYSLIB1229");
+ Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
+ Assert.Contains("not assignable", diagnostic.GetMessage());
+ }
+
[Fact]
public void OpenGenericDerivedType_SYSLIB1229_IsPragmaSuppressible()
{
@@ -1368,9 +1546,12 @@ public class MyDerived : MyBase>
}
[Fact]
- public void OpenGenericDerivedType_WrappedArgWithMatchingBase_CompilesSuccessfully()
+ public void OpenGenericDerivedType_WrappedArgWithMatchingBase_WarnsWithSYSLIB1229()
{
- // Derived : Base> registered on Base> unifies to Derived.
+ // Derived : Base> registered on Base>:
+ // the derived pins the leaf shape to List<>. The registration does not apply
+ // to closures of Base whose T is not a List<>, so it is rejected under the
+ // uniform-applicability rule.
string source = """
#nullable enable
using System.Collections.Generic;
@@ -1396,8 +1577,9 @@ public class MyDerived : MyBase>
""";
Compilation compilation = CompilationHelper.CreateCompilation(source);
- JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
- Assert.Empty(result.Diagnostics);
+ JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true);
+ Diagnostic diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "SYSLIB1229");
+ Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
}
[Fact]
@@ -1432,9 +1614,11 @@ public class MyDerived : MyBase
}
[Fact]
- public void OpenGenericDerivedType_PartialConcretization_CompilesSuccessfully()
+ public void OpenGenericDerivedType_PartialConcretization_WarnsWithSYSLIB1229()
{
- // Derived : Base registered on Base unifies to Derived.
+ // Derived : Base registered on Base:
+ // the derived pins T2 to int. The registration is rejected under the
+ // uniform-applicability rule regardless of which closure was registered.
string source = """
#nullable enable
using System.Text.Json.Serialization;
@@ -1458,15 +1642,17 @@ public class MyDerived : MyBase
""";
Compilation compilation = CompilationHelper.CreateCompilation(source);
- JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
- Assert.Empty(result.Diagnostics);
+ JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true);
+ Diagnostic diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "SYSLIB1229");
+ Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
}
[Fact]
- public void OpenGenericDerivedType_ArrayTypeArg_CompilesSuccessfully()
+ public void OpenGenericDerivedType_ArrayTypeArg_WarnsWithSYSLIB1229()
{
- // Derived : Base registered on Base unifies to Derived.
- // Exercises the IArrayTypeSymbol branch of structural unification.
+ // Derived : Base registered on Base: pins the leaf shape to
+ // an array. The registration does not apply to closures of Base whose T
+ // is not an array, so it is rejected under the uniform-applicability rule.
string source = """
#nullable enable
using System.Text.Json.Serialization;
@@ -1490,8 +1676,9 @@ public class MyDerived : MyBase
""";
Compilation compilation = CompilationHelper.CreateCompilation(source);
- JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
- Assert.Empty(result.Diagnostics);
+ JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true);
+ Diagnostic diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "SYSLIB1229");
+ Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
}
[Fact]
@@ -1586,9 +1773,11 @@ public class MyDerived : MyBase where T : struct
}
[Fact]
- public void OpenGenericDerivedType_ReferenceTypeConstraintSatisfied_CompilesSuccessfully()
+ public void OpenGenericDerivedType_ReferenceTypeConstraintMismatch_WarnsWithSYSLIB1229()
{
- // Derived : Base where T : class, registered on Base.
+ // Derived : Base where T : class. Base has no constraint, so the
+ // derived's constraints don't match the base's exactly. Rejected under
+ // the uniform-applicability rule.
string source = """
#nullable enable
using System.Text.Json.Serialization;
@@ -1612,14 +1801,17 @@ public class MyDerived : MyBase where T : class
""";
Compilation compilation = CompilationHelper.CreateCompilation(source);
- JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
- Assert.Empty(result.Diagnostics);
+ JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true);
+ Diagnostic diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "SYSLIB1229");
+ Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
}
[Fact]
- public void OpenGenericDerivedType_InterfaceConstraintSatisfied_CompilesSuccessfully()
+ public void OpenGenericDerivedType_InterfaceConstraintMismatch_WarnsWithSYSLIB1229()
{
- // Derived : Base where T : System.IComparable, registered on Base.
+ // Derived : Base where T : IComparable. Base has no constraint, so
+ // the derived's constraints don't match the base's exactly. Rejected under
+ // the uniform-applicability rule.
string source = """
#nullable enable
using System.Text.Json.Serialization;
@@ -1643,15 +1835,17 @@ public class MyDerived : MyBase where T : System.IComparable
""";
Compilation compilation = CompilationHelper.CreateCompilation(source);
- JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
- Assert.Empty(result.Diagnostics);
+ JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true);
+ Diagnostic diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "SYSLIB1229");
+ Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
}
[Fact]
- public void OpenGenericDerivedType_ArrayInsideGenericConstraint_CompilesSuccessfully()
+ public void OpenGenericDerivedType_ArrayInsideGenericConstraint_WarnsWithSYSLIB1229()
{
- // where T : IEnumerable with T=List, U=int.
- // Exercises array substitution into a generic constraint type.
+ // Derived : Base where T : IEnumerable. Base has no
+ // constraint on T1, so the derived's constraint is stricter. Rejected under
+ // the uniform-applicability rule.
string source = """
#nullable enable
using System.Collections.Generic;
@@ -1676,16 +1870,18 @@ public class MyDerived : MyBase where T : IEnumerable
""";
Compilation compilation = CompilationHelper.CreateCompilation(source);
- JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
- Assert.Empty(result.Diagnostics);
+ JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true);
+ Diagnostic diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "SYSLIB1229");
+ Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
}
[Fact]
- public void OpenGenericDerivedType_NestedInGenericOuter_CompilesSuccessfully()
+ public void OpenGenericDerivedType_NestedInGenericOuter_WarnsWithSYSLIB1229()
{
- // Outer.Middle.Leaf : Base<(T, U)> registered on Base<(int, string)>.
- // Exercises ConstructEnclosing rebinding a non-generic intermediate type
- // through a constructed generic outer.
+ // Outer.Middle.Leaf : Base<(T, U)>. The derived pins the closed base's
+ // single type argument to a tuple shape, so it does not apply to closures of
+ // Base whose T is not a (_, _) tuple. Rejected under the
+ // uniform-applicability rule.
string source = """
#nullable enable
using System.Text.Json.Serialization;
@@ -1715,8 +1911,9 @@ public class Leaf : Base<(T, U)>
""";
Compilation compilation = CompilationHelper.CreateCompilation(source);
- JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
- Assert.Empty(result.Diagnostics);
+ JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true);
+ Diagnostic diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "SYSLIB1229");
+ Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
}
[Fact]
@@ -1786,17 +1983,12 @@ public class Impl : IDerived { }
}
[Fact]
- public void OpenGenericDerivedType_MultipleInterfaceConstructions_NonAmbiguousResolution_CompilesSuccessfully()
+ public void OpenGenericDerivedType_MultipleInterfaceConstructions_DisagreeingSubstitutions_WarnsWithSYSLIB1229()
{
- // Impl reaches IBase<> twice: once via its own type-parameterized base (IBase)
- // and once via inheritance from the non-generic IntBase (IBase). When resolved
- // against the closed base IBase, only the IBase leg unifies (T=string);
- // the IBase leg is incompatible. Resolution must succeed and produce
- // Impl. The both-legs-match scenario (which IS ambiguous) is covered by
- // OpenGenericDerivedType_AmbiguousMatch. Indirecting the IBase leg through a
- // non-generic base class avoids C# CS0695 -- a class cannot directly declare two
- // constructions of the same generic interface that could unify under any
- // substitution.
+ // Impl reaches IBase<> via two ancestors: directly as IBase and via
+ // IntBase : IBase. The two ancestors produce disagreeing substitutions
+ // for the base's single type parameter (one binds it to T, the other to int).
+ // Rejected under the uniform-applicability rule.
string source = """
using System.Text.Json.Serialization;
@@ -1817,8 +2009,9 @@ public class Impl : IntBase, IBase { }
""";
Compilation compilation = CompilationHelper.CreateCompilation(source);
- JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
- Assert.Empty(result.Diagnostics);
+ JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true);
+ Diagnostic diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "SYSLIB1229");
+ Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
}
[Fact]
@@ -1947,15 +2140,13 @@ public class NestedDerivedMismatch : NestedBase.NestedBox
}
[Fact]
- public void OpenGenericDerivedType_NestedGenericTypeParameterInEnclosing_CompilesSuccessfully()
+ public void OpenGenericDerivedType_NestedGenericTypeParameterInEnclosing_WarnsWithSYSLIB1229()
{
- // Pattern: NestedDerived : NestedBase.NestedBox>.
- // Closed base: NestedBase.NestedBox>.
- // T appears only in the ENCLOSING type's argument list. Pre-fix source-gen
- // ignored ContainingType and reported SYSLIB1229 because T was never bound by
- // the leaf-only TryUnifyWith walk. With the ContainingType walk in place,
- // unification succeeds with T=string and the resolver closes NestedDerived to
- // NestedDerived. No diagnostic expected.
+ // Pattern: NestedDerivedParamInEnclosing : NestedBaseB.NestedBoxB>.
+ // The derived pins the closed base's single type argument to an
+ // NestedOuterB<>.NestedBoxB shape. It does not apply to closures of
+ // NestedBaseB whose T is not of that shape, so it is rejected under the
+ // uniform-applicability rule.
string source = """
using System.Text.Json.Serialization;
@@ -1979,19 +2170,18 @@ public class NestedDerivedParamInEnclosing : NestedBaseB.Nest
""";
Compilation compilation = CompilationHelper.CreateCompilation(source);
- JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
- Assert.Empty(result.Diagnostics);
+ JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation, disableDiagnosticValidation: true);
+ Diagnostic diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "SYSLIB1229");
+ Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
}
[Fact]
- public void OpenGenericDerivedType_CovariantInterfaceConstraintSatisfied_CompilesSuccessfully()
+ public void OpenGenericDerivedType_CovariantInterfaceConstraintMismatch_WarnsWithSYSLIB1229()
{
- // where T : IEnumerable