Repository navigation
Resolve static in parameter and writable property types to the called-on type without the new Foo() exactness - #6946
Conversation
…ed-on type without the `new Foo()` exactness - `CalledOnTypeUnresolvedMethodPrototypeReflection::transformStaticType()` now takes an `$isParameterType` flag. For parameter types (`getType()` and `getPhpDocType()`) it replaces `static` with `TemplateTypeHelper::removeFinalByKeywordOverrides($calledOnType)`. Without this, `$b = new B(); $b->method(new A($b))` with `@param A<covariant static>` expected `A<covariant B(exact)>`, but the inferred `A<B>` never carries exactness, so it was rejected. - `CalledOnTypeUnresolvedPropertyPrototypeReflection` does the same for the writable type, so `$b->prop = new A($b)` is accepted for `@var A<covariant static>`. - Return, readable, self-out, out, closure-this, throw and assert types keep the exact flavour, so `(new Foo())->fluent()` stays exact. - turbo-ext: `PrototypeTransformer` gains an `inputFn`, used for parameter types and the writable property type. The CalledOnType natives pass the flag through the native callback state and strip through `TemplateTypeHelper::removeFinalByKeywordOverrides()`. The Callback natives use the same fn for both. - Covered: instance method calls, static method calls on `$b::`, first-class callables (`$b->method(...)`) and property assignments. The invariant `A<static>` parameter and nested `static` in return types already worked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VincentLanglet
left a comment
There was a problem hiding this comment.
Compare the strategy with the one in #6487
…ariant type arguments The "exactly this class" flavour cannot be written in a PHPDoc type argument, and the invariant branch of isValidVariance() already ignores it through equals(). Stripping it from both sides in the covariant and contravariant branches covers every way the flavour reaches a type argument - `static` resolved in a parameter type, `static` resolved in a return type passed on to another parameter, get_class() narrowing - in one place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… exactness The variance comparison now ignores the flavour, so the prototype reflections (and their turbo-ext mirrors) no longer need a separate input-side transform. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
I compared the two strategies, switched this PR to #6487's approach, and pushed two commits. What I compared. This PR stripped the I ran #6487's test data against this PR. Everything passed except one case: a #6487's approach handles every case, including that one. It's also much smaller: about 25 lines instead of about 150, mostly C++. Commits:
Verification:
The PR title and description still describe the old approach, so they need updating. |
staabm
left a comment
There was a problem hiding this comment.
lgtm - I am not a generics variance expert though
Summary
Since 2.2.13, a
new Foo()expression has an object type with a "final-by-keyword override", meaning it is exactly this class. When a method was called on such a value,staticin its parameter types was replaced with that exact type. So@param A<covariant static>becameA<covariant B(exact)>.The argument
new A($b)infersTas plainB, because inferred template arguments always drop the exactness (#15166). ExactBis only a maybe supertype ofB, so the call was rejected:The same call inside a method taking
B $b, where$bis not exact, was accepted.With this change,
staticin types that values are checked against (parameter types and writable property types) is replaced with the called-on type without the exactness. Output-side types (return, readable property, out, self-out, closure-this, throw and assert types) keep it, so(new Foo())->fluent()is still known to be exactlyFoo.Changes
src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php:transformStaticType()takes an$isParameterTypeflag. ParametergetType()andgetPhpDocType()resolvestaticandGenericStaticTypeagainstTemplateTypeHelper::removeFinalByKeywordOverrides($this->calledOnType).src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php: same change for the writable type.TypeTraits.h:PrototypeTransformergains aninputFn/transformInput().TypeTraits.cpp:protoParameter()and the writable type inprotoTransformProperty()usetransformInput().CalledOnTypeUnresolved{Method,Property}PrototypeReflection.cpp: pass the flag as the native callback's state and strip throughTemplateTypeHelper::removeFinalByKeywordOverrides().CallbackUnresolved{Method,Property}PrototypeReflection.cpp: use the same function for both, so their behaviour is unchanged.Analogous cases
Fixed, each with a test:
$b::staticMethod(...)$b->method(...)called afterwards$b->prop = new A($b)with@var A<covariant static>Probed and already correct:
@param A<static>staticin a return type passed on to@param A<B>@param staticNot touched (separate, pre-existing issue): in constructor parameters (
new B(new A($b))),staticis never resolved and is reported asA<covariant static(B)>. This happens even when$bis not exact, so it is unrelated to thenewexactness.Root cause
Commit a042651 removed the
new Foo()exactness from inferred template arguments, on the reasoning that no written PHPDoc type can carry it. But resolvingstaticagainst the called-on type does put it into written parameter and property types. A covariant template argument then compared exactBagainst plainBand got a maybe. The fix strips the exactness wherever a resolvedstaticends up in an input-side type, in both the method and the property prototype reflections and in their native mirrors.Test
These tests fail without the fix and pass with it, both with and without the turbo extension loaded:
tests/PHPStan/Rules/Methods/data/bug-15235.php: the issue's reproducer, plus a static method call and a first-class callable. Used byCallMethodsRuleTest,CallStaticMethodsRuleTestandCallCallablesRuleTest.tests/PHPStan/Rules/Properties/data/bug-15235.php: property assignment, inTypesAssignedToPropertiesRuleTest.Fixes phpstan/phpstan#15235
🤖 Generated with Claude Code