Skip to content

Do not collapse non-generic types into covariant generic supertypes in unions#5761

Closed
phpstan-bot wants to merge 1 commit into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-nd6omkg
Closed

Do not collapse non-generic types into covariant generic supertypes in unions#5761
phpstan-bot wants to merge 1 commit into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-nd6omkg

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

Fixes phpstan/phpstan#10008

When creating a union of mysqli_result and Traversable<array{email: string, adaid: int<-32768, 32767>}>, PHPStan incorrectly collapsed the union to just Traversable<mixed, array{...}>, losing the mysqli_result type entirely.

Root cause

mysqli_result implements IteratorAggregate (and thus Traversable) without declaring type arguments, so its ancestor resolves to Traversable<mixed, mixed>. In GenericObjectType::isSuperTypeOfInternal, the covariant variance check (isValidVariance) has an early return that treats $b instanceof MixedType as universally compatible (returns yes). This caused Traversable<int, array{...}>->isSuperTypeOf(mysqli_result) to return yes, which let TypeCombinator::compareTypesInUnion collapse mysqli_result into the generic Traversable.

Fix

Added a targeted check in GenericObjectType::isSuperTypeOfInternal: when the checked type is a non-generic class whose ancestor has all-mixed type args for covariant template parameters, return maybe instead of yes. This prevents union collapsing while preserving the existing behavior for:

  • Generic classes with unresolved templates (sealed class subtypes like BarCov extends FooCov<T>)
  • The accepts() path (which is intentionally more lenient)
  • Invariant and contravariant template parameters

Test plan

  • Added tests/PHPStan/Analyser/nsrt/bug-10008.php reproducing the original issue
  • Verified all 12,154 tests pass (make tests)
  • Verified PHPStan self-analysis passes (make phpstan)
  • Specifically verified Bug14412 (sealed class match exhaustiveness) still works correctly

…n unions

When a non-generic class (like mysqli_result) implements a generic interface
(like Traversable) without declaring type arguments, its ancestor resolves
to all-mixed type args. The covariant variance check in isValidVariance
treats mixed as universally compatible, causing GenericObjectType::isSuperTypeOf
to return yes — which lets TypeCombinator collapse the concrete type into
the generic supertype in unions.

Fix: in GenericObjectType::isSuperTypeOfInternal, detect when the checked type
is a non-generic class whose ancestor has all-mixed covariant type args, and
return maybe instead of yes. This prevents union collapsing while preserving
the existing behavior for generic classes (like sealed class subtypes) that
have mixed args due to their own unresolved template parameters.

Closes phpstan/phpstan#10008
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants