Include null in return type of Closure::bindTo() and Closure::bind() dynamic return type extensions#5675
Conversation
…nd()` dynamic return type extensions - ClosureBindToDynamicReturnTypeExtension now returns TypeCombinator::union($closureType, new NullType()) instead of just $closureType - ClosureBindDynamicReturnTypeExtension (for static Closure::bind()) gets the same fix - Updated existing tests that asserted the buggy non-nullable return type - Updated AnalyserIntegrationTest::testBug4734 to expect the 4 new "Trying to invoke ... null" errors Closes phpstan/phpstan#5009
VincentLanglet
left a comment
There was a problem hiding this comment.
Given the fact it's __benevolent<Closure|null> the dynamic return type should not lose the benevolentUnion
…urn types The function signature map declares these as `__benevolent<Closure|null>`, so the dynamic return type extensions should preserve the benevolent union semantics instead of creating a strict union. This avoids false positives for code that invokes the result without null-checking, matching the behavior of the default signature map. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Done. The change addresses VincentLanglet's review by using |
VincentLanglet
left a comment
There was a problem hiding this comment.
Is it possible to exclude null in the return type when we're sure it cannot be returned ?
…is null When $newThis is null, bindTo()/bind() only changes the closure's scope without binding $this, which always succeeds. Null is only returned when binding a static closure with a non-null $newThis. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Pushed. Here's a summary of the change: When When Fully excluding |
VincentLanglet
left a comment
There was a problem hiding this comment.
Fully excluding null for non-static closures with non-null $newThis would require tracking static-ness in ClosureType, which is a larger change — ClosureType currently has no $isStatic property. That could be a follow-up improvement (adding an optional ?TrinaryLogic $isStatic parameter to the @api constructor would be BC-safe).
Try the implementation
…dTo()/bind() return type Add an optional `?TrinaryLogic $isStatic` parameter to ClosureType's constructor to track whether a closure was declared with the `static` keyword. ClosureTypeResolver now passes this from the AST node. The bind extensions use this to exclude null from the return type when the closure is known to be non-static (binding always succeeds for non-static closures regardless of $newThis). For static or possibly-static closures with non-null $newThis, the return type remains BenevolentUnionType(Closure|null) since PHP returns null when binding an object to a static closure. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Pushed successfully. Here's a summary of what was done: Added Changes:
|
Summary
Closure::bindTo()andClosure::bind()can returnnullwhen binding fails (e.g., binding a static closure to an object, or binding to an internal class). The dynamic return type extensions for these methods were stripping thenullfrom the return type, causing PHPStan to miss potential null-pointer issues.Changes
src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php— returnTypeCombinator::union($closureType, new NullType())instead of just$closureTypesrc/Type/Php/ClosureBindDynamicReturnTypeExtension.php— same fix for the staticClosure::bind()methodtests/PHPStan/Analyser/nsrt/closure-return-type-extensions.php— updated assertions to expect(Closure(...))|nulltests/PHPStan/Analyser/AnalyserIntegrationTest.php— updatedtestBug4734to expect 4 new "Trying to invoke (Closure(): void)|null but it might not be a callable" errors (these are correct new diagnostics)tests/PHPStan/Analyser/nsrt/bug-5009.php— new regression testRoot cause
Both
ClosureBindToDynamicReturnTypeExtensionandClosureBindDynamicReturnTypeExtensionreturned the bareClosureTypewhen the caller was a known closure, which preserved the closure's parameter/return type information but dropped thenullthat PHP's actual return type includes. The function signature map has__benevolent<Closure|null>as the default, but this was completely overridden by the extension returning a non-nullable type.Analogous cases probed
Closure::fromCallable()— always returnsClosure, never null. Not affected.Closure::call()— returns the closure's return value, not a closure. Not analogous.Test
tests/PHPStan/Analyser/nsrt/bug-5009.php— verifies thatbindTo()andbind()with various arguments all return(Closure(): void)|nulltests/PHPStan/Analyser/nsrt/closure-return-type-extensions.phpto assert the nullable returnFixes phpstan/phpstan#5009