Skip to content

Closure::bind() scope binding improvements #4081

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: 2.1.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
use PHPStan\Node\Printer\ExprPrinter;
use PHPStan\Node\PropertyAssignNode;
use PHPStan\Parser\ArrayMapArgVisitor;
use PHPStan\Parser\ClosureBindArgVisitor;
use PHPStan\Parser\NewAssignedToPropertyVisitor;
use PHPStan\Parser\Parser;
use PHPStan\Php\PhpVersion;
Expand Down Expand Up @@ -2025,10 +2026,27 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu
if ($this->hasExpressionType($node)->yes()) {
return $this->expressionTypes[$exprString]->getType();
}

$scopeClassReflection = null;
$isInClass = $this->isInClass();
if ($node->class->hasAttribute(ClosureBindArgVisitor::SCOPE_ATTRIBUTE_NAME)) {
/** @var ?Expr $scopeClassNode */
$scopeClassNode = $node->class->getAttribute(ClosureBindArgVisitor::SCOPE_ATTRIBUTE_NAME);
if ($scopeClassNode instanceof Expr\ClassConstFetch
&& $scopeClassNode->class instanceof Name
&& $scopeClassNode->name instanceof Node\Identifier
&& $scopeClassNode->name->toLowerString() === 'class'
) {
$classScopeName = $this->resolveName($scopeClassNode->class);
$isInClass = true;
$scopeClassReflection = $this->reflectionProvider->getClass($classScopeName);
}
}

return $this->initializerExprTypeResolver->getClassConstFetchTypeByReflection(
$node->class,
$node->name->name,
$this->isInClass() ? $this->getClassReflection() : null,
$isInClass ? $scopeClassReflection ?? $this->getClassReflection() : null,
fn (Expr $expr): Type => $this->getType($expr),
);
}
Expand Down
38 changes: 38 additions & 0 deletions src/Parser/ClosureBindArgVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@

use Override;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\NodeVisitorAbstract;
use PHPStan\DependencyInjection\AutowiredService;
use function array_key_exists;
use function array_shift;
use function array_unshift;
use function count;

#[AutowiredService]
Expand All @@ -15,6 +20,11 @@ final class ClosureBindArgVisitor extends NodeVisitorAbstract

public const ATTRIBUTE_NAME = 'closureBindArg';

public const SCOPE_ATTRIBUTE_NAME = 'closureBindScope';

/** @var list<?Expr> */
private array $scopeStack = [];

#[Override]
public function enterNode(Node $node): ?Node
{
Expand All @@ -30,7 +40,35 @@ public function enterNode(Node $node): ?Node
if (count($args) > 1) {
$args[0]->setAttribute(self::ATTRIBUTE_NAME, true);
}

// null means default scope "static"
array_unshift($this->scopeStack, $args[2]->value ?? null);
}

if ($node instanceof Name
&& array_key_exists(0, $this->scopeStack)
&& $node->isSpecialClassName()
) {
$node->setAttribute(self::SCOPE_ATTRIBUTE_NAME, $this->scopeStack[0]);
}

return null;
}

#[Override]
public function leaveNode(Node $node): ?Node
{
if (
$node instanceof Node\Expr\StaticCall
&& $node->class instanceof Node\Name
&& $node->class->toLowerString() === 'closure'
&& $node->name instanceof Identifier
&& $node->name->toLowerString() === 'bind'
&& !$node->isFirstClassCallable()
) {
array_shift($this->scopeStack);
}

return null;
}

Expand Down
7 changes: 7 additions & 0 deletions src/Reflection/ClassReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
use function array_unique;
use function array_values;
use function count;
use function debug_backtrace;
use function implode;
use function in_array;
use function is_bool;
Expand All @@ -69,6 +70,7 @@
use function reset;
use function sprintf;
use function strtolower;
use function var_dump;

/**
* @api
Expand Down Expand Up @@ -1066,13 +1068,18 @@ public function getParentClassesNames(): array
public function hasConstant(string $name): bool
{
if (!$this->getNativeReflection()->hasConstant($name)) {
var_dump([
$this->displayName =>
debug_backtrace(0, 1),
]);
return false;
}

$reflectionConstant = $this->getNativeReflection()->getReflectionConstant($name);
if ($reflectionConstant === false) {
return false;
}
//var_dump($this->reflectionProvider->hasClass($reflectionConstant->getDeclaringClass()->getName()));

return $this->reflectionProvider->hasClass($reflectionConstant->getDeclaringClass()->getName());
}
Expand Down
4 changes: 4 additions & 0 deletions src/Reflection/InitializerExprTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
use function min;
use function sprintf;
use function strtolower;
use function var_dump;
use const INF;

#[AutowiredService]
Expand Down Expand Up @@ -2011,6 +2012,7 @@ function (Type $type, callable $traverse): Type {
continue;
}

//var_dump(compact('referencedClass'));
$constantClassReflection = $this->getReflectionProvider()->getClass($referencedClass);
if (!$constantClassReflection->hasConstant($constantName)) {
continue;
Expand All @@ -2022,6 +2024,7 @@ function (Type $type, callable $traverse): Type {
}

$resolvingName = sprintf('%s::%s', $constantClassReflection->getName(), $constantName);

if (array_key_exists($resolvingName, $this->currentlyResolvingClassConstant)) {
$types[] = new MixedType();
continue;
Expand Down Expand Up @@ -2078,6 +2081,7 @@ function (Type $type, callable $traverse): Type {
unset($this->currentlyResolvingClassConstant[$resolvingName]);
$types[] = $constantType;
}
var_dump([$constantName => $types]);

if (count($types) > 0) {
return TypeCombinator::union(...$types);
Expand Down
3 changes: 3 additions & 0 deletions src/Type/ObjectType.php
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,9 @@ public function hasConstant(string $constantName): TrinaryLogic
if ($class === null) {
return TrinaryLogic::createNo();
}
// var_dump([
// $this->describe(VerbosityLevel::precise()) => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3),
// ]);

return TrinaryLogic::createFromBoolean(
$class->hasConstant($constantName),
Expand Down
3 changes: 3 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;
Expand Down Expand Up @@ -26,7 +26,10 @@
*/
private static function findTestFiles(): iterable
{
yield __DIR__ . '/nsrt/bug-x.php';
return;

foreach (self::findTestDataFilesFromDirectory(__DIR__ . '/nsrt') as $testFile) {

Check failure on line 32 in tests/PHPStan/Analyser/NodeScopeResolverTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.4, ubuntu-latest)

Unreachable statement - code above always terminates.

Check failure on line 32 in tests/PHPStan/Analyser/NodeScopeResolverTest.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.3)

Unreachable statement - code above always terminates.

Check failure on line 32 in tests/PHPStan/Analyser/NodeScopeResolverTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.3, ubuntu-latest)

Unreachable statement - code above always terminates.

Check failure on line 32 in tests/PHPStan/Analyser/NodeScopeResolverTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.2, ubuntu-latest)

Unreachable statement - code above always terminates.

Check failure on line 32 in tests/PHPStan/Analyser/NodeScopeResolverTest.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.2)

Unreachable statement - code above always terminates.

Check failure on line 32 in tests/PHPStan/Analyser/NodeScopeResolverTest.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.4)

Unreachable statement - code above always terminates.

Check failure on line 32 in tests/PHPStan/Analyser/NodeScopeResolverTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, ubuntu-latest)

Unreachable statement - code above always terminates.

Check failure on line 32 in tests/PHPStan/Analyser/NodeScopeResolverTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1, ubuntu-latest)

Unreachable statement - code above always terminates.

Check failure on line 32 in tests/PHPStan/Analyser/NodeScopeResolverTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.3, windows-latest)

Unreachable statement - code above always terminates.

Check failure on line 32 in tests/PHPStan/Analyser/NodeScopeResolverTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.2, windows-latest)

Unreachable statement - code above always terminates.

Check failure on line 32 in tests/PHPStan/Analyser/NodeScopeResolverTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.4, windows-latest)

Unreachable statement - code above always terminates.

Check failure on line 32 in tests/PHPStan/Analyser/NodeScopeResolverTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1, windows-latest)

Unreachable statement - code above always terminates.

Check failure on line 32 in tests/PHPStan/Analyser/NodeScopeResolverTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, windows-latest)

Unreachable statement - code above always terminates.

Check failure on line 32 in tests/PHPStan/Analyser/NodeScopeResolverTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.4, ubuntu-latest)

Unreachable statement - code above always terminates.

Check failure on line 32 in tests/PHPStan/Analyser/NodeScopeResolverTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.4, windows-latest)

Unreachable statement - code above always terminates.
yield $testFile;
}

Expand Down
35 changes: 35 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-x.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php declare(strict_types = 1);

use function PHPStan\Testing\assertType;

class Foo
{
protected const A = 'Foo';

/** @return positive-int */
function bbb(): int
{
return 1;
}
}

final class Bar extends Foo
{
protected const A = 'Bar';

/** @return 2 */
#[\Override]
function bbb(): int
{
return 2;
}
}

assertType("'Foo'", Closure::bind(static fn () => Foo::A, null, Foo::class)());
//assertType("'Foo'", Closure::bind(static fn () => self::A, null, Foo::class)());
assertType("'Foo'", Closure::bind(static fn () => Foo::A, null, Bar::class)());
//assertType("'Foo'", Closure::bind(static fn () => parent::A, null, Bar::class)());
assertType("'Foo'", Closure::bind(static fn () => Bar::A, null, Bar::class)());
//assertType("'Bar'", Closure::bind(static fn () => self::A, null, Bar::class)());

// assertType("'Foo'", Closure::bind(static fn () => Closure::bind(static fn () => self::A, null, Foo::class)(), null, Bar::class)());
Loading