Ignore the new Foo() exactness flavour when comparing generic type arguments for variance - #6487
Open
phpstan-bot wants to merge 1 commit into
Open
phpstan-bot wants to merge 1 commit into
phpstan-bot wants to merge 1 commit into
Conversation
…arguments for variance * `TemplateTypeVariance::isValidVariance()` now routes the covariant and contravariant comparisons through a new private `compareTypeArguments()` that drops the final-by-keyword override from both type arguments before calling `isSuperTypeOf()`. The invariant branch never had the problem because `equals()` already ignores the flavour. * Fixes the reported case (`@param A<covariant static>` called on a `new B()` receiver) and, with the same change, the analogous ones found by probing the family: a receiver narrowed by `get_class($b) === B::class`, class-declared `@template-covariant` / `@template-contravariant` parameters, built-in generics (`Traversable<int, static>`), the flavour arriving on the argument side through a `@return Wrapper<static>` factory, generic property assignment (`@var A<covariant static>`), and type arguments nested inside unions, arrays and other generic types. * Probed and found already correct, so left alone: non-generic `@param static`, invariant type arguments, `array`/`iterable`/array-shape/callable parameter types, `class-string<static>`, and `instanceof` narrowing - the flavour is still preserved everywhere else, so `(new B())->returnsStatic() instanceof Foo` stays impossible. * Regression tests in `tests/PHPStan/Rules/Methods/data/bug-15235.php` and `tests/PHPStan/Rules/Properties/data/bug-15235.php`; without the fix they report 8 false positives.
| */ | ||
| private static function compareTypeArguments(Type $super, Type $sub): IsSuperTypeOfResult | ||
| { | ||
| return TemplateTypeHelper::removeFinalByKeywordOverrides($super) |
Contributor
There was a problem hiding this comment.
Since this is almost a one liner, this is not worth introducing a private method.
Collaborator
Author
There was a problem hiding this comment.
Still running — I'll report once both complete.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Calling a method whose parameter is written as
@param A<covariant static>on a receiverwhose type carries the "exactly this class" flavour (
$b = new B()) reported a falsepositive:
The same call on a plain
Bparameter was fine, so the error only appeared for receiversPHPStan knows are exactly
B. Variance comparisons between generic type arguments nowignore that flavour, which is what the invariant comparison already did.
Changes
src/Type/Generic/TemplateTypeVariance.php: the covariant and contravariant branches ofisValidVariance()call a new privatecompareTypeArguments()which strips thefinal-by-keyword override from both sides (
TemplateTypeHelper::removeFinalByKeywordOverrides())before
isSuperTypeOf().tests/PHPStan/Rules/Methods/data/bug-15235.php+CallMethodsRuleTest::testBug15235().tests/PHPStan/Rules/Properties/data/bug-15235.php+TypesAssignedToPropertiesRuleTest::testBug15235().Analogous cases covered by the same one-line change, each with its own case in the test data
(all 8 of them fail without the fix):
@param A<covariant static>on anew B()receiver (the report),get_class($b) === B::class(the flavour also comesfrom
IdenticalNarrowingHelper, not just fromnew),@template-covariant(@param Covariant<static>),@param \Traversable<int, static>),@return Wrapper<static>factory called on an exact receiver and passed to a@param Wrapper<contravariant E>,@var A<covariant static>,@var Covariant<static>),array<int, Covariant<static>>and insideanother generic type.
Probed and found already correct, so no change and no test kept: non-generic
@param static(
ObjectType::accepts()is name-based), invariant type arguments (equals()ignores theflavour),
array/iterable/array-shape/callableparameter types (they compare withaccepts()),class-string<static>, and the naked class part of a generic comparison (itsresult is discarded once there are type arguments).
Root cause
ClassReflection::asFinal()puts an "exactly this class" flavour on the type of anew Foo()expression (and onget_class($x) === Foo::classnarrowing) so that checks knowthe value cannot be a subclass instance.
Foowith the flavour is therefore only amaybe-supertype of a plain
Foo.Nothing written in a PHPDoc can carry that flavour, so any comparison between a flavoured
type and a written one has to ignore it. phpstan/phpstan#15166 established this for inferred
template arguments, which
TemplateTypeTrait::inferTemplateTypes()strips. This issue is theother direction: the flavour reaches a written type when
staticinside it is substitutedby the called-on type (
CalledOnTypeUnresolvedMethodPrototypeReflection::transformStaticType()and its property counterpart), producing an expected
A<covariant B-exact>that no writtenA<B>can match.@return A<static>puts it on the value side the same way.Rather than stripping it at every substitution site - which would also lose the precision
that makes
(new B())->returnsStatic() instanceof Fooimpossible - the fix is at the singleplace where type arguments are compared for variance.
TemplateTypeVariance::isValidVariance()is the only comparison that was flavour-sensitive: the invariant branch uses
equals(), whichignores the flavour, while the covariant and contravariant branches used
isSuperTypeOf(),which does not. That made covariance stricter than invariance for the very same pair of types.
Test
CallMethodsRuleTest::testBug15235()runstests/PHPStan/Rules/Methods/data/bug-15235.php,which starts from the reproducer in the issue (including its
D::inconsistent()counter-casethat never errored) and adds the analogous constructs listed above.
TypesAssignedToPropertiesRuleTest::testBug15235()runs the property-assignment counterpart. Without the source change the two tests report 8
false positives; with it both expect no errors. Full
make testsandmake phpstanare green.Fixes phpstan/phpstan#15235