diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 051d15d960..c8fbea438c 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -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; @@ -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), ); } diff --git a/src/Parser/ClosureBindArgVisitor.php b/src/Parser/ClosureBindArgVisitor.php index ed94333cad..682d89766e 100644 --- a/src/Parser/ClosureBindArgVisitor.php +++ b/src/Parser/ClosureBindArgVisitor.php @@ -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] @@ -15,6 +20,11 @@ final class ClosureBindArgVisitor extends NodeVisitorAbstract public const ATTRIBUTE_NAME = 'closureBindArg'; + public const SCOPE_ATTRIBUTE_NAME = 'closureBindScope'; + + /** @var list */ + private array $scopeStack = []; + #[Override] public function enterNode(Node $node): ?Node { @@ -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; } diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 45c0fd4bff..789d954b06 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -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; @@ -69,6 +70,7 @@ use function reset; use function sprintf; use function strtolower; +use function var_dump; /** * @api @@ -1066,6 +1068,10 @@ 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; } @@ -1073,6 +1079,7 @@ public function hasConstant(string $name): bool if ($reflectionConstant === false) { return false; } + //var_dump($this->reflectionProvider->hasClass($reflectionConstant->getDeclaringClass()->getName())); return $this->reflectionProvider->hasClass($reflectionConstant->getDeclaringClass()->getName()); } diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 48336cb01c..e540b5e824 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -91,6 +91,7 @@ use function min; use function sprintf; use function strtolower; +use function var_dump; use const INF; #[AutowiredService] @@ -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; @@ -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; @@ -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); diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 04441d0e03..375530b48f 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -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), diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index bc8f4ac04e..2a3b42b069 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -26,6 +26,9 @@ class NodeScopeResolverTest extends TypeInferenceTestCase */ private static function findTestFiles(): iterable { + yield __DIR__ . '/nsrt/bug-x.php'; + return; + foreach (self::findTestDataFilesFromDirectory(__DIR__ . '/nsrt') as $testFile) { yield $testFile; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-x.php b/tests/PHPStan/Analyser/nsrt/bug-x.php new file mode 100644 index 0000000000..f80b1ca8fe --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-x.php @@ -0,0 +1,35 @@ + 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)());