Skip to content

Commit e76d963

Browse files
phpstan-botclaude
authored andcommitted
Track static-ness of closures in ClosureType to exclude null from bindTo()/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>
1 parent 6f41577 commit e76d963

3 files changed

Lines changed: 21 additions & 4 deletions

File tree

src/Type/Php/ClosureBindDynamicReturnTypeExtension.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection,
3535
return null;
3636
}
3737

38+
if ($closureType->isStaticClosure()->no()) {
39+
return $closureType;
40+
}
41+
3842
if (isset($args[1]) && $scope->getType($args[1]->value)->isNull()->yes()) {
3943
return $closureType;
4044
}

src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
3434
return null;
3535
}
3636

37+
if ($closureType->isStaticClosure()->no()) {
38+
return $closureType;
39+
}
40+
3741
$args = $methodCall->getArgs();
3842
if (isset($args[0]) && $scope->getType($args[0]->value)->isNull()->yes()) {
3943
return $closureType;

tests/PHPStan/Analyser/nsrt/bug-5009.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@
1616

1717
$newThis = new \stdClass();
1818
$bound = $foo->bindTo($newThis);
19-
assertType('((Closure(): void)|null)', $bound);
19+
assertType('Closure(): void', $bound);
2020

2121
$staticBound = Closure::bind($foo, $newThis);
22-
assertType('((Closure(): void)|null)', $staticBound);
22+
assertType('Closure(): void', $staticBound);
2323

2424
$bound2 = $foo->bindTo($newThis, 'stdClass');
25-
assertType('((Closure(): void)|null)', $bound2);
25+
assertType('Closure(): void', $bound2);
2626

2727
$static = static function (): void {};
2828
$boundStatic = $static->bindTo($newThis);
@@ -31,7 +31,16 @@
3131
$boundStaticNull = $static->bindTo(null);
3232
assertType('Closure(): void', $boundStaticNull);
3333

34+
$staticBound2 = Closure::bind($static, $newThis);
35+
assertType('((Closure(): void)|null)', $staticBound2);
36+
37+
$staticBoundNull = Closure::bind($static, null);
38+
assertType('Closure(): void', $staticBoundNull);
39+
3440
/** @var \stdClass|null $maybeNull */
3541
$maybeNull = null;
3642
$boundMaybe = $foo->bindTo($maybeNull);
37-
assertType('((Closure(): void)|null)', $boundMaybe);
43+
assertType('Closure(): void', $boundMaybe);
44+
45+
$staticBoundMaybe = $static->bindTo($maybeNull);
46+
assertType('((Closure(): void)|null)', $staticBoundMaybe);

0 commit comments

Comments
 (0)