Resolve template return type T when a class-string<T> parameter is narrowed to a constant class-string#5864
Open
phpstan-bot wants to merge 1 commit into
Open
Conversation
…s narrowed to a constant class-string - `FunctionReturnTypeCheck::checkReturnType()` now resolves template types in the declared `@return` type using the current scope, via a new `specifyTemplateTypesFromScope()` helper. - For each parameter whose declared type is a `class-string<T>`, if it has been narrowed to a constant class-string in the current scope (e.g. inside `if ($className === \stdClass::class)`), `T` is inferred to that exact class and substituted into the return type. Returning a value of that class then satisfies `@return T`. - The pin is only applied for `class-string` parameters narrowed to constant class-strings, where `===`/assignment guarantees the caller's `T` exactly. Non-constant narrowing (e.g. `is_a()`) and bare-template parameters are intentionally left untouched, since subtypes can still pass and pinning would be unsound. - Works for unions of constant class-strings (`T` becomes the union) and is shared by the function and method return-type rules. Anonymous functions/arrow functions are skipped because their parameters do not come from `$scope->getFunction()`. - Added regression tests in tests/PHPStan/Rules/Functions/data/bug-2955.php and tests/PHPStan/Rules/Methods/data/bug-2955.php covering the pinned case, the still-wrong case (returning a different class), and the not-pinned `is_a()` case. Closes phpstan/phpstan#2955
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
Given a generic function/method with a
class-string<T>parameter, PHPStan lost the connection between the parameter andTonce the parameter was narrowed to a constant class-string. In the reported snippet, insideif ($className === \stdClass::class)the function returns astdClass(e.g.(object) [], which isobject{}&stdClass) for@return T, and PHPStan wrongly reported "should return T of object but returns stdClass". In that branchTis known to be exactlystdClass, so this is a false positive.The fix resolves the declared return type's template types from the current scope: when a
class-string<T>parameter has been pinned to a constant class-string,Tis substituted with that exact class before checking the returned value.Changes
src/Rules/FunctionReturnTypeCheck.php:specifyTemplateTypesFromScope(), called at the top ofcheckReturnType().isClassString()->yes(), if its current scope type consists solely of constant strings (getConstantStrings()covers the whole type), it infers the template type map viaGenericClassStringType::inferTemplateTypes()and substitutes the pinned types into the return type withTypeTraverser.$scope->getFunction()).tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php+tests/PHPStan/Rules/Functions/data/bug-2955.php— function regression test.tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php+tests/PHPStan/Rules/Methods/data/bug-2955.php— method regression test (the check is shared by both rules).Root cause
Narrowing
class-string<T>via=== Foo::classreplaces the parameter's type with the constant'Foo', discarding the template typeT. The return-type rule used the function's static@return Tsignature with no scope awareness, soTstayed unresolved andTemplateTypeArgumentStrategy::accepts()(correctly, in general) reported "not always the same as T". The missing piece was re-pinningTfrom the narrowed parameter.The pin is only sound for constant class-strings:
class-string<T> === Foo::classguarantees the caller'sTis exactlyFoo(any subclass would fail the===). It is deliberately not applied to:@param T $xwith$x === 5does not pinTtoint(5)), andis_a($className, Base::class, true), where subclasses still pass and returningnew Base()would be unsound.Both of these continue to report as before, which the regression tests assert.
Test
bug-2955.php(functions):test()(returns(object) []) andtest2()(returnsnew \stdClass()) now report no errors;returnsWrongClass()still errors but with the resolved type ("should return stdClass but returns Foo");notPinnedByIsA()still errors viais_a()("should return T of object but returns Foo").bug-2955.php(methods):Factory::make()reports no error;Factory::makeWrong()still errors with the resolvedstdClassreturn type.make tests,make phpstan, andmake cs-fixall pass.Fixes phpstan/phpstan#2955