Skip to content
Merged
24 changes: 22 additions & 2 deletions src/Type/Php/ClosureBindDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\MethodReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\BenevolentUnionType;
use PHPStan\Type\ClosureType;
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
use PHPStan\Type\NullType;
use PHPStan\Type\Type;

#[AutowiredService]
Expand All @@ -27,12 +30,29 @@ public function isStaticMethodSupported(MethodReflection $methodReflection): boo

public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type
{
$closureType = $scope->getType($methodCall->getArgs()[0]->value);
$args = $methodCall->getArgs();
if (!isset($args[0])) {
return null;
}

$closureType = $scope->getType($args[0]->value);
if (!($closureType instanceof ClosureType)) {
return null;
}

return $closureType;
if ($closureType->isStaticClosure()->yes()) {
$newThisIsNull = isset($args[1]) ? $scope->getType($args[1]->value)->isNull() : TrinaryLogic::createYes();
if ($newThisIsNull->yes()) {
return $closureType;
}
if ($newThisIsNull->no()) {
return new NullType();
}

return new BenevolentUnionType([$closureType, new NullType()]);
}

return new BenevolentUnionType([$closureType, new NullType()]);
}

}
18 changes: 17 additions & 1 deletion src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\MethodReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\BenevolentUnionType;
use PHPStan\Type\ClosureType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\NullType;
use PHPStan\Type\Type;

#[AutowiredService]
Expand All @@ -32,7 +35,20 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
return null;
}

return $closureType;
if ($closureType->isStaticClosure()->yes()) {
$args = $methodCall->getArgs();
$newThisIsNull = isset($args[0]) ? $scope->getType($args[0]->value)->isNull() : TrinaryLogic::createYes();
if ($newThisIsNull->yes()) {
return $closureType;
}
if ($newThisIsNull->no()) {
return new NullType();
}

return new BenevolentUnionType([$closureType, new NullType()]);
}

return new BenevolentUnionType([$closureType, new NullType()]);
}

}
7 changes: 4 additions & 3 deletions tests/PHPStan/Analyser/AnalyserIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -473,13 +473,14 @@ public function testBug4734(): void
{
// false positive
$errors = $this->runAnalyse(__DIR__ . '/data/bug-4734.php');
$this->assertCount(5, $errors); // could be 3
$this->assertCount(6, $errors); // could be 4

$this->assertSame('Static property Bug4734\Foo::$httpMethodParameterOverride (bool) is never assigned false so the property type can be changed to true.', $errors[0]->getMessage()); // should not error
$this->assertSame('Property Bug4734\Foo::$httpMethodParameterOverride2 (bool) is never assigned false so the property type can be changed to true.', $errors[1]->getMessage()); // should not error
$this->assertSame('Unsafe access to private property Bug4734\Foo::$httpMethodParameterOverride through static::.', $errors[2]->getMessage());
$this->assertSame('Access to an undefined static property static(Bug4734\Foo)::$httpMethodParameterOverride3.', $errors[3]->getMessage());
$this->assertSame('Access to an undefined property Bug4734\Foo::$httpMethodParameterOverride4.', $errors[4]->getMessage());
$this->assertSame('Trying to invoke null but it\'s not a callable.', $errors[3]->getMessage()); // binding a static closure to an object returns null
$this->assertSame('Access to an undefined static property static(Bug4734\Foo)::$httpMethodParameterOverride3.', $errors[4]->getMessage());
$this->assertSame('Access to an undefined property Bug4734\Foo::$httpMethodParameterOverride4.', $errors[5]->getMessage());
}

#[RequiresPhp('>= 8.1.0')]
Expand Down
46 changes: 46 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-5009.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types = 1);

namespace Bug5009;

use Closure;
use function PHPStan\Testing\assertType;

$foo = function (): void {};
$bar = $foo->bindTo(null);
assertType('((Closure(): void)|null)', $bar);

$baz = Closure::bind($foo, null);
assertType('((Closure(): void)|null)', $baz);

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

$staticBound = Closure::bind($foo, $newThis);
assertType('((Closure(): void)|null)', $staticBound);

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

$static = static function (): void {};
$boundStatic = $static->bindTo($newThis);
assertType('null', $boundStatic);

$boundStaticNull = $static->bindTo(null);
assertType('static-Closure(): void', $boundStaticNull);

$staticBound2 = Closure::bind($static, $newThis);
assertType('null', $staticBound2);

$staticBoundNull = Closure::bind($static, null);
assertType('static-Closure(): void', $staticBoundNull);

/** @var \stdClass|null $maybeNull */
$maybeNull = null;
$boundMaybe = $foo->bindTo($maybeNull);
assertType('((Closure(): void)|null)', $boundMaybe);

$staticBoundMaybe = $static->bindTo($maybeNull);
assertType('((static-Closure(): void)|null)', $staticBoundMaybe);
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@

$newThis = new class {};
$boundClosure = $closure->bindTo($newThis);
assertType('Closure(object): true', $boundClosure);
assertType('((Closure(object): true)|null)', $boundClosure);

$staticallyBoundClosure = \Closure::bind($closure, $newThis);
assertType('Closure(object): true', $staticallyBoundClosure);
assertType('((Closure(object): true)|null)', $staticallyBoundClosure);

$returnType = $closure->call($newThis, new class {});
assertType('true', $returnType);

$staticallyBoundClosureCaseInsensitive = \closure::bind($closure, $newThis);
assertType('Closure(object): true', $staticallyBoundClosureCaseInsensitive);
assertType('((Closure(object): true)|null)', $staticallyBoundClosureCaseInsensitive);
16 changes: 8 additions & 8 deletions tests/PHPStan/Analyser/nsrt/closure-static-type.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,37 +26,37 @@ public function doFoo(): void
public function doBindTo(): void
{
$static = static function (): void {};
assertType('static-Closure(): void', $static->bindTo($this));
assertType('null', $static->bindTo($this));

$nonStatic = function (): void {};
assertType('Closure(): void', $nonStatic->bindTo($this));
assertType('((Closure(): void)|null)', $nonStatic->bindTo($this));
}

public function doBind(): void
{
$static = static function (): void {};
assertType('static-Closure(): void', Closure::bind($static, $this));
assertType('null', Closure::bind($static, $this));

$nonStatic = function (): void {};
assertType('Closure(): void', Closure::bind($nonStatic, $this));
assertType('((Closure(): void)|null)', Closure::bind($nonStatic, $this));
}

/**
* @param Closure(): void $unknownClosure
*/
public function doUnknown(Closure $unknownClosure): void
{
assertType('Closure(): void', $unknownClosure->bindTo($this));
assertType('Closure(): void', Closure::bind($unknownClosure, $this));
assertType('((Closure(): void)|null)', $unknownClosure->bindTo($this));
assertType('((Closure(): void)|null)', Closure::bind($unknownClosure, $this));
}

public function doFromCallable(): void
{
$fn = Closure::fromCallable(static function (): void {});
assertType('static-Closure(): void', $fn->bindTo($this));
assertType('null', $fn->bindTo($this));

$fn2 = Closure::fromCallable(function (): void {});
assertType('Closure(): void', $fn2->bindTo($this));
assertType('((Closure(): void)|null)', $fn2->bindTo($this));
}

}
5 changes: 5 additions & 0 deletions tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,11 @@ public function testNullCoalesceInGlobalScope(): void
]);
}

public function testBug5009(): void
{
$this->analyse([__DIR__ . '/data/bug-5009.php'], []);
}

public function testBug5933(): void
{
$this->analyse([__DIR__ . '/data/bug-5933.php'], []);
Expand Down
18 changes: 18 additions & 0 deletions tests/PHPStan/Rules/Variables/data/bug-5009.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php declare(strict_types = 1);

namespace Bug5009;

use Closure;

class Test
{
protected Closure $callback;

/**
* @param Closure(): void $callback
*/
public function __construct(Closure $callback)
{
$this->callback = $callback->bindTo($this, $this) ?? $callback;
}
}
Loading