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 arguments the 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 type the 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 type the 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 type the 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 type the 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 type the 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 type the 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 type the 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 type the 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 type the 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 type the 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 type the 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 type the 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 type the 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. Closing T to List satisfies the - // constraint ONLY via IEnumerable covariance (IEnumerable is - // assignable to IEnumerable only by virtue of 'out T'). Pre-fix - // source-gen used identity-based interface containment for the constraint - // check and would have reported SYSLIB1229. With Compilation.HasImplicitConversion - // in place, source-gen matches reflection's behavior and accepts. + // ConstraintImpl : ConstraintBase where T : IEnumerable. + // The derived has constraints the base doesn't. Even though specific closures + // (e.g. List via variance) would satisfy the constraint, the + // registration is rejected under the uniform-applicability rule. string source = """ using System.Collections.Generic; using System.Text.Json.Serialization; @@ -2011,8 +2201,38 @@ public class ConstraintImpl : ConstraintBase 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_DiagnosticEmittedOncePerOpenBase_EvenWithMultipleClosures() + { + string source = """ + using System.Text.Json.Serialization; + + namespace HelloWorld + { + [JsonSerializable(typeof(MyBase))] + [JsonSerializable(typeof(MyBase))] + [JsonSerializable(typeof(MyBase))] + internal partial class JsonContext : JsonSerializerContext { } + + [JsonDerivedType(typeof(MyDerived<>), "d")] + public class MyBase { } + + // Pins T to a specific specialization (string) of the base — non-uniform, + // so SYSLIB1229 fires. It should fire exactly once for the (MyBase<>, MyDerived<>) + // pair, not once per closure of MyBase<>. + public class MyDerived : 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); } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.CustomTypeHierarchies.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.CustomTypeHierarchies.cs index b9f948d193aae5..d8add3a9565c0e 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.CustomTypeHierarchies.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.CustomTypeHierarchies.cs @@ -2787,16 +2787,16 @@ public class OpenGenericBase_ComplexArg public class OpenGenericDerived_ComplexArg : OpenGenericBase_ComplexArg; [Fact] - public async Task OpenGenericDerivedType_WrappedTypeArg_Works() + public async Task OpenGenericDerivedType_WrappedTypeArg_ThrowsForNonUniformPinning() { - // Derived : Base> registered on Base> unifies to Derived. - OpenGenericBase_Wrapped> value = new OpenGenericDerived_Wrapped { Data = ["a", "b"] }; - string json = await Serializer.SerializeWrapper(value); - JsonTestHelper.AssertJsonEqual("""{"$type":"derived","Data":["a","b"]}""", json); - - var result = await Serializer.DeserializeWrapper>>(json); - Assert.IsType>(result); - Assert.Equal(new[] { "a", "b" }, ((OpenGenericDerived_Wrapped)result).Data); + // Derived : Base> structurally pins the base's only type parameter to + // List; this can never apply for a base specialization whose type argument is + // not itself a List<>. Under the uniform-applicability rule the registration + // is rejected at metadata-resolution time regardless of which base specialization + // is actually constructed. + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper>>( + new OpenGenericDerived_Wrapped { Data = ["a", "b"] })); } [JsonDerivedType(typeof(OpenGenericDerived_Wrapped<>), "derived")] @@ -2890,19 +2890,23 @@ public class OpenGenericBase_GroundMismatch; public class OpenGenericDerived_GroundMismatch : OpenGenericBase_GroundMismatch; [Fact] - public async Task OpenGenericDerivedType_PartiallyConcrete_Works() + public async Task OpenGenericDerivedType_PartiallyConcrete_ThrowsForNonUniformPinning() { - // Derived : Base registered on Base: - // position 0 (T) unifies with string, position 1 (concrete int) matches. - // Expected: closed derived is OpenGenericDerived_PartiallyConcrete, and - // round-trip serialization emits and reads the $type discriminator. - OpenGenericBase_PartiallyConcrete value = new OpenGenericDerived_PartiallyConcrete { Extra = "hello" }; - string json = await Serializer.SerializeWrapper(value); - JsonTestHelper.AssertJsonEqual("""{"$type":"derived","Extra":"hello","Value1":null,"Value2":0}""", json); + // Derived : Base pins position 1 of the base to a concrete type. Even + // though the registration could "work" for Base closures, it does not + // apply to arbitrary closures of Base. Under the uniform rule we reject + // the registration regardless of which closure is currently being constructed, + // so registrations cannot silently work for one specialization and break for + // another. + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper>( + new OpenGenericDerived_PartiallyConcrete { Extra = "hello" })); - var result = await Serializer.DeserializeWrapper>(json); - var derived = Assert.IsType>(result); - Assert.Equal("hello", derived.Extra); + // The exact same registration is rejected uniformly regardless of which closure + // happens to be constructed first; a Base closure throws the same way. + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper>( + new OpenGenericBase_PartiallyConcrete())); } [JsonDerivedType(typeof(OpenGenericDerived_PartiallyConcrete<>), "derived")] @@ -2960,25 +2964,18 @@ public class OpenGenericBase_Programmatic public class OpenGenericDerived_Programmatic : OpenGenericBase_Programmatic; [Fact] - public async Task OpenGenericDerivedType_MixedWithRegularDerivedType_Works() + public async Task OpenGenericDerivedType_MixedWithClosedDerivedOnGenericBase_Throws() { - // Validates that both regular and open generic derived types coexist. - OpenGenericBase_Mixed openValue = new OpenGenericDerived_Mixed { Value = 1 }; - OpenGenericBase_Mixed regularValue = new RegularDerived_Mixed { Value = 2, Extra = "extra" }; - - string openJson = await Serializer.SerializeWrapper(openValue); - string regularJson = await Serializer.SerializeWrapper(regularValue); - - JsonTestHelper.AssertJsonEqual("""{"$type":"open","Value":1}""", openJson); - JsonTestHelper.AssertJsonEqual("""{"$type":"regular","Value":2,"Extra":"extra"}""", regularJson); - - var openResult = await Serializer.DeserializeWrapper>(openJson); - var regularResult = await Serializer.DeserializeWrapper>(regularJson); - - Assert.IsType>(openResult); - Assert.IsType(regularResult); - Assert.Equal(1, openResult.Value); - Assert.Equal("extra", ((RegularDerived_Mixed)regularResult).Extra); + // The closed RegularDerived_Mixed : OpenGenericBase_Mixed registration pins + // the base to a specific specialization. Although it could conceivably apply only + // to closures that pick T = int, the shared attribute attached to the open base + // definition is necessarily inherited by every closure -- so a registration that + // only applies to one specialization is rejected at metadata-resolution time. + // The open OpenGenericDerived_Mixed : OpenGenericBase_Mixed registration is + // uniform, but the rejection of the closed sibling throws before it surfaces. + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper>( + new OpenGenericDerived_Mixed { Value = 1 })); } [JsonDerivedType(typeof(OpenGenericDerived_Mixed<>), "open")] @@ -3024,16 +3021,15 @@ public class OpenGenericImpl_InterfaceHierarchy : IOpenGenericDerived_Interfa } [Fact] - public async Task OpenGenericDerivedType_InterfaceBaseWithWrappedTypeArg_Works() + public async Task OpenGenericDerivedType_InterfaceBaseWithWrappedTypeArg_ThrowsForNonUniformPinning() { - // Impl implements IBase> registered on IBase> unifies to Impl. - IOpenGenericBase_InterfaceWrapped> value = new OpenGenericImpl_InterfaceWrapped { Data = ["a", "b"] }; - string json = await Serializer.SerializeWrapper(value); - JsonTestHelper.AssertJsonEqual("""{"$type":"impl","Data":["a","b"]}""", json); - - var result = await Serializer.DeserializeWrapper>>(json); - Assert.IsType>(result); - Assert.Equal(new[] { "a", "b" }, ((OpenGenericImpl_InterfaceWrapped)result).Data); + // Impl implements IBase>: the interface ancestor pins the base's only + // type parameter to a List<>. Even though IBase> could be served by + // Impl, the registration only applies to closures whose argument is itself + // a List<>, so it is rejected uniformly. + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper>>( + new OpenGenericImpl_InterfaceWrapped { Data = ["a", "b"] })); } [JsonDerivedType(typeof(OpenGenericImpl_InterfaceWrapped<>), "impl")] @@ -3048,16 +3044,15 @@ public class OpenGenericImpl_InterfaceWrapped : IOpenGenericBase_InterfaceWra } [Fact] - public async Task OpenGenericDerivedType_ArrayTypeArg_Works() + public async Task OpenGenericDerivedType_ArrayTypeArg_ThrowsForNonUniformPinning() { - // Derived : Base registered on Base unifies to Derived. - OpenGenericBase_ArrayArg value = new OpenGenericDerived_ArrayArg { Values = [1, 2, 3] }; - string json = await Serializer.SerializeWrapper(value); - JsonTestHelper.AssertJsonEqual("""{"$type":"derived","Values":[1,2,3]}""", json); - - var result = await Serializer.DeserializeWrapper>(json); - Assert.IsType>(result); - Assert.Equal(new[] { 1, 2, 3 }, ((OpenGenericDerived_ArrayArg)result).Values); + // Derived : Base pins the base's only type parameter to an array shape. + // The registration cannot apply to closures whose argument is not itself an array, + // so it is rejected at metadata-resolution time regardless of which closure is + // currently being constructed. + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper>( + new OpenGenericDerived_ArrayArg { Values = [1, 2, 3] })); } [JsonDerivedType(typeof(OpenGenericDerived_ArrayArg<>), "derived")] @@ -3093,16 +3088,15 @@ public class OpenGenericDerived_Reordered : OpenGenericBase_Reordered : Base registered on Base unifies to Derived. - OpenGenericBase_Partial value = new OpenGenericDerived_Partial { Value = "hello" }; - string json = await Serializer.SerializeWrapper(value); - JsonTestHelper.AssertJsonEqual("""{"$type":"derived","Value":"hello"}""", json); - - var result = await Serializer.DeserializeWrapper>(json); - Assert.IsType>(result); - Assert.Equal("hello", ((OpenGenericDerived_Partial)result).Value); + // Companion of OpenGenericDerivedType_PartiallyConcrete_ThrowsForNonUniformPinning + // on a base whose second parameter is unused -- proves that the rejection is purely + // structural at registration time and does not depend on whether the pinned base + // parameter is referenced by the closure's property bag. + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper>( + new OpenGenericDerived_Partial { Value = "hello" })); } [JsonDerivedType(typeof(OpenGenericDerived_Partial<>), "derived")] @@ -3114,15 +3108,15 @@ public class OpenGenericDerived_Partial : OpenGenericBase_Partial } [Fact] - public async Task OpenGenericDerivedType_KeyValuePairArg_Works() + public async Task OpenGenericDerivedType_KeyValuePairArg_ThrowsForNonUniformPinning() { - // Derived : Base> registered on Base> unifies to Derived. - OpenGenericBase_KvpArg> value = new OpenGenericDerived_KvpArg { Pair = new KeyValuePair("k", 99) }; - string json = await Serializer.SerializeWrapper(value); - JsonTestHelper.AssertJsonEqual("""{"$type":"derived","Pair":{"Key":"k","Value":99}}""", json); - - var result = await Serializer.DeserializeWrapper>>(json); - Assert.IsType>(result); + // Derived : Base> pins the base's only type parameter + // to a KeyValuePair shape AND pins the Key half of that pair to string. + // The registration cannot apply uniformly, so it is rejected at metadata- + // resolution time. + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper>>( + new OpenGenericDerived_KvpArg { Pair = new KeyValuePair("k", 99) })); } [JsonDerivedType(typeof(OpenGenericDerived_KvpArg<>), "derived")] @@ -3134,15 +3128,15 @@ public class OpenGenericDerived_KvpArg : OpenGenericBase_KvpArg : Base>, Leaf : Mid; registered on Base> unifies to Leaf. - OpenGenericBase_MultiLevel> value = new OpenGenericLeaf_MultiLevel { Items = [10, 20] }; - string json = await Serializer.SerializeWrapper(value); - JsonTestHelper.AssertJsonEqual("""{"$type":"leaf","Items":[10,20]}""", json); - - var result = await Serializer.DeserializeWrapper>>(json); - Assert.IsType>(result); + // The Leaf -> Mid -> Base> chain is uniform in the Leaf->Mid hop + // but non-uniform in the Mid->Base hop (Mid pins Base's parameter to List). + // Uniformity is required end-to-end, so the registration is rejected even though + // the leaf's own immediate base is uniform. + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper>>( + new OpenGenericLeaf_MultiLevel { Items = [10, 20] })); } [JsonDerivedType(typeof(OpenGenericLeaf_MultiLevel<>), "leaf")] @@ -3156,14 +3150,14 @@ public class OpenGenericLeaf_MultiLevel : OpenGenericMid_MultiLevel } [Fact] - public async Task OpenGenericDerivedType_TupleSyntax_Works() + public async Task OpenGenericDerivedType_TupleSyntax_ThrowsForNonUniformPinning() { - // Derived : Base<(T1, T2)> registered on Base<(int, string)> unifies to Derived. - OpenGenericBase_Tuple<(int, string)> value = new OpenGenericDerived_Tuple { Pair = (5, "x") }; - string json = await Serializer.SerializeWrapper(value); - - var result = await Serializer.DeserializeWrapper>(json); - Assert.IsType>(result); + // Derived : Base<(T1, T2)> pins the base's only type parameter to a + // ValueTuple<,> shape (`(T1, T2)` is sugar for `ValueTuple`). The + // registration is non-uniform and rejected at metadata-resolution time. + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper>( + new OpenGenericDerived_Tuple { Pair = (5, "x") })); } [JsonDerivedType(typeof(OpenGenericDerived_Tuple<,>), "derived")] @@ -3203,12 +3197,19 @@ public class OpenGenericBase_Unbound; public class OpenGenericDerived_Unbound : OpenGenericBase_Unbound; [Fact] - public async Task OpenGenericDerivedType_ConstraintViolation_ThrowsInvalidOperationException() + public async Task OpenGenericDerivedType_ConstraintMismatch_ThrowsForAllSpecializations() { - // Derived : Base where T : struct, registered on Base. - // Constraint fails → InvalidOperationException. - var value = new OpenGenericBase_StructConstraint(); - await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + // The derived adds a `where T : struct` constraint that the base does not have. + // This is rejected uniformly: even though the constraint happens to be satisfied + // for some closures (e.g. Base), the registration is non-uniform and + // would mysteriously stop applying on closures the constraint excludes. Reject + // at metadata-resolution time. + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper>( + new OpenGenericBase_StructConstraint())); + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper>( + new OpenGenericBase_StructConstraint())); } [JsonDerivedType(typeof(OpenGenericDerived_StructConstraint<>), "derived")] @@ -3236,14 +3237,16 @@ public class OpenGenericBase_NullableArg public class OpenGenericDerived_NullableArg : OpenGenericBase_NullableArg; [Fact] - public async Task OpenGenericDerivedType_DuplicateClosedAndOpenRegistration_ThrowsInvalidOperationException() + public async Task OpenGenericDerivedType_DuplicateClosedAndOpenRegistration_ThrowsForClosedDerivedOnGenericBase() { - // Base has BOTH a closed-form Derived registration AND an open-form - // Derived<> registration. The open form closes to Derived, producing a - // duplicate derived-type registration. The existing dup-detection in - // PolymorphicTypeResolver must surface this as InvalidOperationException. - OpenGenericBase_DuplicateDerivedRegistrations value = new OpenGenericDerived_DuplicateDerivedRegistrations(); - await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + // The closed Derived registration on the open generic base is rejected + // outright under the uniform-registration rule (a closed derived registered on + // a generic base only applies to one specialization, which the shared attribute + // declaration cannot express). The throw masks any downstream duplicate-id check + // that would otherwise have surfaced. + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper>( + new OpenGenericDerived_DuplicateDerivedRegistrations())); } [JsonDerivedType(typeof(OpenGenericDerived_DuplicateDerivedRegistrations), "closed")] @@ -3337,24 +3340,21 @@ public class OpenGenericImpl_Diamond : IOpenGenericDerived_Diamond } [Fact] - public async Task OpenGenericDerivedType_MultipleInterfaceConstructions_NonAmbiguousResolution_Works() - { - // Impl reaches IBase<> twice: once via its own type-parameterized interface - // (IBase) and once via inheritance from the non-generic IntBase (IBase). - // When the closed base is IBase, only the IBase ancestor unifies - // (T=string); the IBase ancestor is incompatible. Resolution must succeed - // and produce Impl. (The both-legs-match scenario is the ambiguous one, - // covered separately.) 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. - IOpenGenericBase_MultiCtor value = new OpenGenericImpl_MultiCtor { Item = "hello" }; - string json = await Serializer.SerializeWrapper(value); - - JsonTestHelper.AssertJsonEqual("""{"$type":"impl","Item":"hello"}""", json); - - var result = await Serializer.DeserializeWrapper>(json); - var impl = Assert.IsType>(result); - Assert.Equal("hello", impl.Item); + public async Task OpenGenericDerivedType_MultipleInterfaceConstructions_NonUniformPinning_Throws() + { + // Impl reaches IBase<> via two ancestors: directly (IBase, uniform) and + // transitively through OpenGenericImpl_MultiCtor_IntBase : IBase (pins to + // int). Uniform applicability requires every matching ancestor to agree on a + // canonical substitution mapping each derived parameter to a base parameter; the + // IBase ancestor pins the base parameter to a concrete type, so the + // registration is rejected uniformly even for closures where the IBase leg + // alone would have been a clean match. 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. + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper>( + new OpenGenericImpl_MultiCtor { Item = "hello" })); } [JsonDerivedType(typeof(OpenGenericImpl_MultiCtor<>), "impl")] @@ -3367,6 +3367,592 @@ public class OpenGenericImpl_MultiCtor : OpenGenericImpl_MultiCtor_IntBase, I public T? Item { get; set; } } + // ---- Uniform-applicability tests (PR #129294, B-strict rule) ---- + // + // Under the uniform-applicability rule a derived registration on a generic base must + // apply to EVERY closed specialization of that base. Registrations that only apply to + // some specializations -- closed derived types pinning a specific spec, open derived + // types that pin a base parameter to a concrete type, or open derived types that + // narrow the base's constraints -- are rejected at metadata-resolution time so that + // serialization cannot silently work for one closure and break for another. + + // Scenario 1: Closed derived types on an OPEN generic base. The same [JsonDerivedType] + // attribute lives on the open base definition and is shared across every closed + // specialization, so a closed derived (which necessarily pins a single specialization) + // can never apply uniformly. Rejected. + + [JsonDerivedType(typeof(SpecAnimal_Cat), "cat")] + [JsonDerivedType(typeof(SpecAnimal_Dog), "dog")] + public class SpecAnimal + { + public string? Name { get; set; } + } + + public sealed class SpecAnimal_Cat : SpecAnimal { public int Lives { get; set; } } + public sealed class SpecAnimal_Dog : SpecAnimal { public string? Breed { get; set; } } + + [Theory] + [InlineData(typeof(SpecAnimal))] + [InlineData(typeof(SpecAnimal))] + [InlineData(typeof(SpecAnimal))] + public async Task ClosedDerivedOnGenericBase_ThrowsForEverySpecialization(Type closedBaseType) + { + // Whichever closure of SpecAnimal the user constructs, resolution fails + // because the closed derived registrations are non-uniform. There is no + // "happy" closure that suppresses the rejection. + object baseValue = Activator.CreateInstance(closedBaseType)!; + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper(baseValue, closedBaseType)); + } + + // Scenario 2: Closed derived on a generic base, with [JsonPolymorphic] also declared + // (no behavior difference under uniform applicability -- the rejection is the same). + + [JsonPolymorphic] + [JsonDerivedType(typeof(SpecAnimal_Cat), "cat")] + [JsonDerivedType(typeof(SpecAnimal_Dog), "dog")] + public class SpecAnimalExplicit + { + public string? Name { get; set; } + } + + [Fact] + public async Task ClosedDerivedOnGenericBase_WithExplicitPolymorphicAttribute_Throws() + { + // Companion to ClosedDerivedOnGenericBase_ThrowsForEverySpecialization. The + // explicit [JsonPolymorphic] attribute does not change the rejection -- whether + // the user opted in via attribute or got an implicit options shape from + // [JsonDerivedType] alone, the same uniform-applicability rule applies. + SpecAnimalExplicit animal = new() { Name = "Cookie" }; + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper(animal)); + } + + // Scenario 3: Open derived with a narrower constraint than the base. The derived + // happens to satisfy the constraint for some closures (e.g. List) but not + // others (e.g. string). Rejected uniformly so it can't silently start failing on a + // new closure. + + [JsonDerivedType(typeof(ConstrainedDerived<>), "derived")] + public class ConstrainedBase + { + public T? Value { get; set; } + } + + public class ConstrainedDerived : ConstrainedBase where T : IEnumerable + { + public int Extra { get; set; } + } + + [Theory] + [InlineData(typeof(ConstrainedBase))] + [InlineData(typeof(ConstrainedBase>))] + public async Task OpenDerivedWithNarrowerConstraint_ThrowsForAllSpecializations(Type closedBaseType) + { + // Both "string" (constraint fails) and "List" (constraint holds) closures + // throw the same way -- uniform applicability requires the constraint to hold + // for EVERY closure, not just the one in front of us. + object baseValue = Activator.CreateInstance(closedBaseType)!; + InvalidOperationException ex = await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper(baseValue, closedBaseType)); + + // The message must identify both the registration and the base so users can + // diagnose the registration without diving into source. + Assert.Contains("ConstrainedDerived", ex.Message); + Assert.Contains("ConstrainedBase", ex.Message); + } + + // Scenario 4: Open derived definitions with EXTRA unbound type parameters + // (Cat registered on Animal). The extra-parameter variant is not closeable + // against any specialization of the base and surfaces a hard diagnostic. The + // well-formed variant (Cat) is verified separately on its own base type so the + // failing registration doesn't poison shared metadata. + + [JsonDerivedType(typeof(ExtraParam_Cat<>), "cat")] + public class ExtraParamAnimal + { + public T? Tag { get; set; } + } + + [JsonDerivedType(typeof(ExtraParam_Cat<,>), "cat2")] + public class ExtraParamAnimalWithBadRegistration + { + public T? Tag { get; set; } + } + + public class ExtraParam_Cat : ExtraParamAnimal + { + public string? Name { get; set; } + } + + public class ExtraParam_Cat : ExtraParamAnimalWithBadRegistration + { + public T2? Extra { get; set; } + } + + [Fact] + public async Task OpenDerivedWithoutExtraParameter_Resolves() + { + // Sanity-check companion to the throw test below: ExtraParam_Cat with no + // extra unbound parameter is uniform -- it closes cleanly against any + // specialization of ExtraParamAnimal. + ExtraParamAnimal value = new ExtraParam_Cat { Name = "Felix", Tag = 7 }; + string json = await Serializer.SerializeWrapper(value); + JsonTestHelper.AssertJsonEqual("""{"$type":"cat","Name":"Felix","Tag":7}""", json); + + var roundTripped = await Serializer.DeserializeWrapper>(json); + var typedCat = Assert.IsType>(roundTripped); + Assert.Equal("Felix", typedCat.Name); + Assert.Equal(7, typedCat.Tag); + } + + [Fact] + public async Task OpenDerivedWithExtraUnboundParameter_ThrowsInvalidOperationException() + { + // ExtraParam_Cat has an unbound T2 that the single-parameter base + // ExtraParamAnimalWithBadRegistration cannot pin down. The derived + // definition is structurally malformed for this base hierarchy regardless of + // which closed base is supplied. + ExtraParamAnimalWithBadRegistration value = new(); + InvalidOperationException ex = await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper(value)); + + Assert.Contains("ExtraParam_Cat", ex.Message); + Assert.Contains("ExtraParamAnimalWithBadRegistration", ex.Message); + } + + // Scenario 5: Mixed open/closed registrations on the same open base. Closed + // registrations always pin a specialization, so they reject the whole metadata + // resolution -- the well-formed open arm cannot save the registration. + + [JsonDerivedType(typeof(MixedClosedInt), "closed-int")] + [JsonDerivedType(typeof(MixedClosedString), "closed-string")] + [JsonDerivedType(typeof(MixedOpenOk<>), "open-ok")] + public class MixedBase + { + public T? Value { get; set; } + } + + public sealed class MixedClosedInt : MixedBase { public int A { get; set; } } + public sealed class MixedClosedString : MixedBase { public string? B { get; set; } } + + public class MixedOpenOk : MixedBase { public T? C { get; set; } } + + [Theory] + [InlineData(typeof(MixedBase))] + [InlineData(typeof(MixedBase))] + [InlineData(typeof(MixedBase))] + public async Task MixedRegistrations_ClosedSiblingPoisonsTheWholeBase(Type closedBaseType) + { + // The presence of closed derived registrations (MixedClosedInt, MixedClosedString) + // on the generic base poisons the entire base metadata regardless of which closure + // is constructed and regardless of the presence of a well-formed open arm. + object value = closedBaseType == typeof(MixedBase) ? new MixedOpenOk() + : closedBaseType == typeof(MixedBase) ? new MixedOpenOk() + : (object)new MixedOpenOk(); + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper(value, closedBaseType)); + } + + // ---- Pattern catalog (B-strict additions) ---- + // + // Comprehensive coverage of the uniform-applicability rule across pinning, + // collapse, reorder, multi-ancestor, constraint subsumption, and edge-case patterns. + + // ---- Pattern A: parameter collapse ---- + + [JsonDerivedType(typeof(Catalog_ParamCollapse_Derived<>), "d")] + public class Catalog_ParamCollapse_Base + { + public T1? V1 { get; set; } + public T2? V2 { get; set; } + } + + public class Catalog_ParamCollapse_Derived : Catalog_ParamCollapse_Base; + + [Fact] + public async Task Catalog_ParameterCollapse_ThrowsForNonUniformPinning() + { + // Derived : Base "collapses" both base parameters to the same derived + // parameter. For arbitrary Base closures where T1 != T2 the derived + // cannot apply, so the registration is rejected uniformly. + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper(new Catalog_ParamCollapse_Base())); + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper(new Catalog_ParamCollapse_Base())); + } + + // ---- Pattern B: parameter reorder ---- + + [JsonDerivedType(typeof(Catalog_Reorder_Derived<,>), "d")] + public class Catalog_Reorder_Base + { + public T1? V1 { get; set; } + public T2? V2 { get; set; } + } + + public class Catalog_Reorder_Derived : Catalog_Reorder_Base + { + public U1? Left { get; set; } + public U2? Right { get; set; } + } + + [Fact] + public async Task Catalog_ParameterReorder_IsUniform() + { + // Derived : Base maps each base parameter to a distinct derived + // parameter -- the substitution is bijective and applies to every closure. + Catalog_Reorder_Base value = new Catalog_Reorder_Derived + { + Left = "L", + Right = 7, + }; + string json = await Serializer.SerializeWrapper(value); + JsonTestHelper.AssertJsonEqual("""{"$type":"d","Left":"L","Right":7,"V1":0,"V2":null}""", json); + + var roundTripped = await Serializer.DeserializeWrapper>(json); + Assert.IsType>(roundTripped); + } + + // ---- Pattern C: constraint equivalence matrix ---- + + // C1: derived adds `where T : struct` where base has no constraint -- rejected. + [JsonDerivedType(typeof(Catalog_C1_Derived<>), "d")] + public class Catalog_C1_Base; + public class Catalog_C1_Derived : Catalog_C1_Base where T : struct; + + // C2: derived adds `where T : class` where base has no constraint -- rejected. + [JsonDerivedType(typeof(Catalog_C2_Derived<>), "d")] + public class Catalog_C2_Base; + public class Catalog_C2_Derived : Catalog_C2_Base where T : class; + + // C3: derived adds `where T : new()` where base has no constraint -- rejected. + [JsonDerivedType(typeof(Catalog_C3_Derived<>), "d")] + public class Catalog_C3_Base; + public class Catalog_C3_Derived : Catalog_C3_Base where T : new(); + + // C4: derived adds `where T : IDisposable` where base has no constraint -- rejected. + [JsonDerivedType(typeof(Catalog_C4_Derived<>), "d")] + public class Catalog_C4_Base; + public class Catalog_C4_Derived : Catalog_C4_Base where T : IDisposable; + + // C5: derived's constraint is identical to base's constraint -- accepted. (Under C# + // language rules, derived must impose at-least-as-strict constraints as base, so + // "exactly matching" is the only configuration that is BOTH compilable and + // uniform. Tighter constraints on derived produce non-uniformity.) + [JsonDerivedType(typeof(Catalog_C5_Derived<>), "d")] + public class Catalog_C5_Base where T : class + { + public T? Value { get; set; } + } + public class Catalog_C5_Derived : Catalog_C5_Base where T : class + { + public T? Extra { get; set; } + } + + // C8: derived's interface constraint references its own derived param substituted + // to the base param -- accepted as long as the base has the equivalent constraint. + [JsonDerivedType(typeof(Catalog_C8_Derived<>), "d")] + public class Catalog_C8_Base where T : IComparable; + public class Catalog_C8_Derived : Catalog_C8_Base where T : IComparable; + + [Theory] + [InlineData(typeof(Catalog_C1_Base))] // derived adds struct constraint + [InlineData(typeof(Catalog_C1_Base))] + [InlineData(typeof(Catalog_C2_Base))] // derived adds class constraint + [InlineData(typeof(Catalog_C2_Base))] + [InlineData(typeof(Catalog_C3_Base))] // derived adds new() constraint + [InlineData(typeof(Catalog_C4_Base))] // derived adds IDisposable constraint + public async Task Catalog_ConstraintMismatch_ThrowsForAllSpecializations(Type closedBaseType) + { + object value = Activator.CreateInstance(closedBaseType)!; + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper(value, closedBaseType)); + } + + [Fact] + public async Task Catalog_ConstraintIdentical_IsUniform() + { + // C5: derived's `class` constraint matches base's `class` constraint exactly. + Catalog_C5_Base value = new Catalog_C5_Derived { Value = "v", Extra = "e" }; + string json = await Serializer.SerializeWrapper(value); + JsonTestHelper.AssertJsonEqual("""{"$type":"d","Extra":"e","Value":"v"}""", json); + } + + [Fact] + public async Task Catalog_SelfReferentialInterfaceConstraint_IsUniform() + { + // C8: derived's `IComparable` constraint, after substituting T with the base's + // T, exactly matches the base's `IComparable` constraint. + Catalog_C8_Base value = new Catalog_C8_Derived(); + string json = await Serializer.SerializeWrapper(value); + Assert.Contains("\"$type\":\"d\"", json); + } + + // ---- Pattern D: multi-ancestor interface implementations ---- + + // D1: Impl reaches IBase<> via two ancestors -- directly as IBase and + // transitively through Helper : IBase. Each ancestor produces a substitution + // that leaves the other ancestor's parameter free, so neither is uniform in + // isolation and they disagree -- rejected. + + [JsonDerivedType(typeof(Catalog_D1_Impl<,>), "d")] + public interface ICatalog_D1_Base; + + public class Catalog_D1_Helper : ICatalog_D1_Base; + public class Catalog_D1_Impl : Catalog_D1_Helper, ICatalog_D1_Base; + + [Fact] + public async Task Catalog_MultipleInterfaceAncestors_EachLeavesOtherParamUnbound_Throws() + { + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper>(new Catalog_D1_Impl())); + } + + // D2: Impl reaches IBase<,> via two ancestors with reordered substitutions + // (direct: T1=U1,T2=U2; via Helper : IBase: T1=U2,T2=U1). The two + // ancestors produce complete but disagreeing substitutions -- rejected. + + [JsonDerivedType(typeof(Catalog_D2_Impl<,>), "d")] + public interface ICatalog_D2_Base; + + public class Catalog_D2_Helper : ICatalog_D2_Base; + public class Catalog_D2_Impl : Catalog_D2_Helper, ICatalog_D2_Base; + + [Fact] + public async Task Catalog_MultipleInterfaceAncestors_AmbiguousSubstitution_Throws() + { + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper>(new Catalog_D2_Impl())); + } + + // D3: Impl reaches IBase<> via two ancestors -- directly as IBase (uniform) + // and transitively through HelperList : IBase> (pins to List<>). The + // pinning ancestor breaks uniformity even though the direct ancestor is fine. + + [JsonDerivedType(typeof(Catalog_D3_Impl<>), "d")] + public interface ICatalog_D3_Base; + + public class Catalog_D3_HelperList : ICatalog_D3_Base>; + public class Catalog_D3_Impl : Catalog_D3_HelperList, ICatalog_D3_Base; + + [Fact] + public async Task Catalog_MultipleInterfaceAncestors_OneLegPinsConstructedShape_Throws() + { + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper>(new Catalog_D3_Impl())); + } + + // D4: D : IBase, IBase -- two identical ancestors. Uniform because the + // canonical substitutions agree on (U -> T). + + [JsonDerivedType(typeof(Catalog_D4_Impl<>), "d")] + public interface ICatalog_D4_BaseA; + + [JsonDerivedType(typeof(Catalog_D4_Impl<>), "d")] + public interface ICatalog_D4_BaseB; + + public class Catalog_D4_Impl : ICatalog_D4_BaseA, ICatalog_D4_BaseB; + + [Fact] + public async Task Catalog_MultipleDistinctInterfaceBases_UniformImpl_Works() + { + ICatalog_D4_BaseA viaA = new Catalog_D4_Impl(); + ICatalog_D4_BaseB viaB = (Catalog_D4_Impl)viaA; + + string jsonA = await Serializer.SerializeWrapper(viaA); + string jsonB = await Serializer.SerializeWrapper(viaB); + Assert.Contains("\"$type\":\"d\"", jsonA); + Assert.Contains("\"$type\":\"d\"", jsonB); + } + + // ---- Pattern E: nested enclosing types ---- + + // E1: Outer + Outer.Inner : Outer -- the inner's enclosing T is implicitly + // the outer's T. Uniform. + + [JsonDerivedType(typeof(Catalog_E1_Outer<>.Inner), "inner")] + public class Catalog_E1_Outer + { + public T? Value { get; set; } + + public class Inner : Catalog_E1_Outer; + } + + [Fact] + public async Task Catalog_NestedTypeUnderGenericEnclosing_IsUniform() + { + Catalog_E1_Outer value = new Catalog_E1_Outer.Inner { Value = 7 }; + string json = await Serializer.SerializeWrapper(value); + Assert.Contains("\"$type\":\"inner\"", json); + } + + // E2: Outer.Inner : SomeBase -- enclosing type is closed but a different + // type parameter U is bound through SomeBase. Uniform w.r.t. SomeBase<>. + + [JsonDerivedType(typeof(Catalog_E2_Outer.Inner<>), "inner")] + public class Catalog_E2_SomeBase + { + public T? Value { get; set; } + } + + public class Catalog_E2_Outer + { + public class Inner : Catalog_E2_SomeBase; + } + + [Fact] + public async Task Catalog_NestedDerivedUnderNonGenericEnclosing_IsUniform() + { + Catalog_E2_SomeBase value = new Catalog_E2_Outer.Inner { Value = 9 }; + string json = await Serializer.SerializeWrapper(value); + Assert.Contains("\"$type\":\"inner\"", json); + } + + // ---- Pattern F: transitive inheritance through generic mid ---- + + // F1: Leaf : Mid : Base -- uniform at every hop. Accepted. + + [JsonDerivedType(typeof(Catalog_F1_Leaf<>), "leaf")] + public class Catalog_F1_Base; + public class Catalog_F1_Mid : Catalog_F1_Base; + public class Catalog_F1_Leaf : Catalog_F1_Mid + { + public T? Value { get; set; } + } + + [Fact] + public async Task Catalog_TransitiveInheritance_UniformAtEveryHop_Works() + { + Catalog_F1_Base value = new Catalog_F1_Leaf { Value = 11 }; + string json = await Serializer.SerializeWrapper(value); + Assert.Contains("\"$type\":\"leaf\"", json); + } + + // F2: Leaf : Mid : Base where Mid : Base -- Leaf goes through a + // non-generic Mid that pins Base. Leaf doesn't bind T from Base at all, + // so T is unbound w.r.t. the base. Rejected (UnboundParameter). + + [JsonDerivedType(typeof(Catalog_F2_Leaf<>), "leaf")] + public class Catalog_F2_Base; + public class Catalog_F2_Mid : Catalog_F2_Base; + public class Catalog_F2_Leaf : Catalog_F2_Mid; + + [Fact] + public async Task Catalog_TransitiveThroughNonGenericMid_LeafParamUnboundFromBase_Throws() + { + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper>(new Catalog_F2_Leaf())); + } + + // ---- Pattern G: array element pinning vs whole-arg pinning ---- + + // G: D : Base pins T to an array shape -- rejected (same as Wrapped earlier + // but kept for catalog completeness). + + [JsonDerivedType(typeof(Catalog_G_Derived<>), "d")] + public class Catalog_G_Base; + public class Catalog_G_Derived : Catalog_G_Base; + + [Fact] + public async Task Catalog_ArrayShapePinning_Throws() + { + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper>(new Catalog_G_Derived())); + } + + // ---- Pattern H: user's original PR-thread example ---- + // + // The motivating example from the inline-review thread: a two-parameter base with a + // derived that pins only one of the base's parameters to a concrete type. Rejected + // uniformly regardless of which closure of the base is constructed. + + [JsonDerivedType(typeof(Catalog_H_Derived<>), "d")] + public class Catalog_H_Base; + public class Catalog_H_Derived : Catalog_H_Base; + + [Theory] + [InlineData(typeof(Catalog_H_Base))] + [InlineData(typeof(Catalog_H_Base))] + public async Task Catalog_PartialPinning_TwoParameterBase_Throws(Type closedBaseType) + { + // Even on Base (where the pinning happens to be consistent), the + // registration is rejected -- uniformity requires the substitution to be valid + // for every closure, not just one. + object value = Activator.CreateInstance(closedBaseType)!; + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper(value, closedBaseType)); + } + + // ---- Pattern I: open derived registered on non-generic base ---- + + [JsonDerivedType(typeof(Catalog_I_OpenDerived<>), "d")] + public class Catalog_I_NonGenericBase; + public class Catalog_I_OpenDerived : Catalog_I_NonGenericBase; + + [Fact] + public async Task Catalog_OpenDerivedOnNonGenericBase_Throws() + { + // No closure of an open derived can be assignable to a non-generic base, so the + // registration is rejected at metadata-resolution time with NotAssignable. + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper(new Catalog_I_NonGenericBase())); + } + + // ---- Pattern J: open derived structurally unrelated to base ---- + + [JsonDerivedType(typeof(Catalog_J_Unrelated<>), "d")] + public class Catalog_J_Base; + public class Catalog_J_Unrelated; // No inheritance relationship. + + [Fact] + public async Task Catalog_OpenDerivedNotAssignableToBase_Throws() + { + await Assert.ThrowsAsync( + () => Serializer.SerializeWrapper(new Catalog_J_Base())); + } + + // ---- Pattern K: programmatic API parity ---- + + public class Catalog_K_Base + { + public T? Value { get; set; } + } + + public class Catalog_K_NonUniform_Derived : Catalog_K_Base; + + [Fact] + public async Task Catalog_ProgrammaticApi_NonUniformRegistration_Throws() + { + // The same B-strict rejection applies whether the registration originates from + // [JsonDerivedType] attribute or from a programmatic resolver modifier. + var options = new JsonSerializerOptions + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + typeInfo => + { + if (typeInfo.Type == typeof(Catalog_K_Base)) + { + typeInfo.PolymorphismOptions = new JsonPolymorphismOptions + { + DerivedTypes = + { + new JsonDerivedType(typeof(Catalog_K_NonUniform_Derived<>), "d"), + }, + }; + } + }, + }, + }, + }; + + Catalog_K_Base value = new(); + Assert.Throws(() => JsonSerializer.Serialize(value, options)); + } + #endregion #region Generic Variance Tests @@ -3511,19 +4097,17 @@ public class NestedOuter { public class NestedBox { public TInne public class NestedDerivedEnclosingMismatch : NestedBase.NestedBox>; [Fact] - public async Task NestedGeneric_TypeParameterInEnclosing_Resolves() + public async Task NestedGeneric_TypeParameterInEnclosing_ThrowsForNonUniformPinning() { // Pattern: NestedDerivedParamInEnclosing : NestedBaseB.NestedBoxB>. - // Target: NestedBaseB.NestedBoxB>. - // T appears only in the ENCLOSING type's argument list. Reflection has always - // resolved this correctly because GetGenericArguments() flattens. Pre-B2-fix - // source-gen would have false-rejected because TryUnifyWith only walked leaf - // TypeArguments (T was never bound). + // The derived pins the leaf NestedBoxB's TInner to a concrete int. This makes + // the registration non-uniform -- it only applies to closures of NestedBaseB + // whose argument is shaped as ...NestedBoxB, not e.g. ...NestedBoxB. + // Under the uniform-applicability rule the registration is rejected. NestedBaseB.NestedBoxB> value = new NestedDerivedParamInEnclosing { Item = new NestedOuterB.NestedBoxB { Inner = 42 } }; - string json = await Serializer.SerializeWrapper(value); - JsonTestHelper.AssertJsonEqual("""{"$type":"nestedB","Item":{"Inner":42}}""", json); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); } [JsonDerivedType(typeof(NestedDerivedParamInEnclosing<>), "nestedB")] @@ -3537,15 +4121,15 @@ public class NestedDerivedParamInEnclosing : NestedBaseB.Nest // changed to use Compilation.HasImplicitConversion for the same parity. [Fact] - public async Task Variance_CovariantInterfaceConstraintSatisfied_Resolves() + public async Task Variance_CovariantInterfaceConstraintSatisfied_ThrowsForConstraintMismatch() { - // Constraint: where T : IEnumerable. Closing T to List satisfies - // the constraint ONLY via IEnumerable covariance (IEnumerable is - // assignable to IEnumerable only by virtue of 'out T'). Reflection - // resolves successfully; serialization emits the discriminator. + // ConstraintImpl : ConstraintBase where T : IEnumerable. + // The derived has constraints the base doesn't (IEnumerable). Even + // though specific closures (e.g. List via variance) would satisfy the + // constraint, the registration is rejected under the uniform-applicability + // rule because it does not apply to every specialization of the base. ConstraintBase> value = new ConstraintImpl> { Items = new List { "hello" } }; - string json = await Serializer.SerializeWrapper(value); - JsonTestHelper.AssertJsonEqual("""{"$type":"impl","Items":["hello"]}""", json); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); } [JsonDerivedType(typeof(ConstraintImpl<>), "impl")]