From bc4bf7676041e2d48edd5c83efee4294ff19d848 Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 14 Feb 2026 11:37:40 +0100 Subject: [PATCH 1/4] Fix inconsistent type inference for anonymous functions in class constants --- src/Analyser/NodeScopeResolver.php | 13 +++++++++++++ tests/PHPStan/Analyser/nsrt/bug-14105.php | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14105.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 8a807ea745..5e48395b29 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1062,6 +1062,19 @@ private function processStmtNode( }); $this->processStmtNodesInternal($stmt, $classLikeStatements, $classScope, $storage, $classStatementsGatherer, $context); + foreach ($stmt->stmts as $classLikeStmt) { + if (!$classLikeStmt instanceof Node\Stmt\ClassConst || !$classLikeStmt->isPublic()) { + continue; + } + + foreach ($classLikeStmt->consts as $const) { + $scope = $scope->assignExpression( + new Expr\ClassConstFetch(new Name\FullyQualified($classReflection->getName()), $const->name), + $classScope->getType($const->value), + $classScope->getNativeType($const->value), + ); + } + } $this->callNodeCallback($nodeCallback, new ClassPropertiesNode($stmt, $this->readWritePropertiesExtensionProvider, $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls(), $classStatementsGatherer->getReturnStatementsNodes(), $classStatementsGatherer->getPropertyAssigns(), $classReflection), $classScope, $storage); $this->callNodeCallback($nodeCallback, new ClassMethodsNode($stmt, $classStatementsGatherer->getMethods(), $classStatementsGatherer->getMethodCalls(), $classReflection), $classScope, $storage); $this->callNodeCallback($nodeCallback, new ClassConstantsNode($stmt, $classStatementsGatherer->getConstants(), $classStatementsGatherer->getConstantFetches(), $classReflection), $classScope, $storage); diff --git a/tests/PHPStan/Analyser/nsrt/bug-14105.php b/tests/PHPStan/Analyser/nsrt/bug-14105.php new file mode 100644 index 0000000000..56247f6fba --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14105.php @@ -0,0 +1,19 @@ += 8.2 + +namespace Bug14105; + +use function PHPStan\Testing\assertType; + +final readonly class Foo +{ + const func = static function (int $num): array { + return ['num' => $num]; + }; +} + +const func = static function (int $num): array { + return ['num' => $num]; +}; + +assertType('Closure(int): array{num: int}', Foo::func); +assertType('Closure(int): array{num: int}', func); From 5cbf5ba73ee888bed3411d62cb12b3f227f2241e Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 14 Feb 2026 13:53:12 +0100 Subject: [PATCH 2/4] Fix private self::const closure type inference --- src/Analyser/NodeScopeResolver.php | 11 +++++++++-- tests/PHPStan/Analyser/nsrt/bug-14105.php | 12 ++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 5e48395b29..1f0f4ced28 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2225,10 +2225,17 @@ public function leaveNode(Node $node): ?ExistingArrayDimFetch if ($scope->getClassReflection() === null) { throw new ShouldNotHappenException(); } + $constType = $scope->getType($const->value); + $constNativeType = $scope->getNativeType($const->value); $scope = $scope->assignExpression( new Expr\ClassConstFetch(new Name\FullyQualified($scope->getClassReflection()->getName()), $const->name), - $scope->getType($const->value), - $scope->getNativeType($const->value), + $constType, + $constNativeType, + ); + $scope = $scope->assignExpression( + new Expr\ClassConstFetch(new Name('self'), $const->name), + $constType, + $constNativeType, ); } } elseif ($stmt instanceof Node\Stmt\EnumCase) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-14105.php b/tests/PHPStan/Analyser/nsrt/bug-14105.php index 56247f6fba..a68fbd8d75 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14105.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14105.php @@ -11,6 +11,18 @@ }; } +final readonly class PrivateFoo +{ + private const func = static function (int $num): array { + return ['num' => $num]; + }; + + public function test(): void + { + assertType('Closure(int): array{num: int}', self::func); + } +} + const func = static function (int $num): array { return ['num' => $num]; }; From deac291ac926af68d3249b42b794785172b314cd Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 14 Feb 2026 13:54:56 +0100 Subject: [PATCH 3/4] Preserve self::const types in static method scopes --- src/Analyser/MutatingScope.php | 4 ++-- tests/PHPStan/Analyser/nsrt/bug-14105.php | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index f4dfa47e6e..434cb423a2 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -5485,7 +5485,7 @@ private function getConstantTypes(): array $constantTypes = []; foreach ($this->expressionTypes as $exprString => $typeHolder) { $expr = $typeHolder->getExpr(); - if (!$expr instanceof ConstFetch) { + if (!$expr instanceof ConstFetch && !$expr instanceof Expr\ClassConstFetch) { continue; } $constantTypes[$exprString] = $typeHolder; @@ -5520,7 +5520,7 @@ private function getNativeConstantTypes(): array $constantTypes = []; foreach ($this->nativeExpressionTypes as $exprString => $typeHolder) { $expr = $typeHolder->getExpr(); - if (!$expr instanceof ConstFetch) { + if (!$expr instanceof ConstFetch && !$expr instanceof Expr\ClassConstFetch) { continue; } $constantTypes[$exprString] = $typeHolder; diff --git a/tests/PHPStan/Analyser/nsrt/bug-14105.php b/tests/PHPStan/Analyser/nsrt/bug-14105.php index a68fbd8d75..d03b1063d4 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14105.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14105.php @@ -17,7 +17,12 @@ return ['num' => $num]; }; - public function test(): void + public static function testStatic(): void + { + assertType('Closure(int): array{num: int}', self::func); + } + + public function testNonStatic(): void { assertType('Closure(int): array{num: int}', self::func); } From 2397e54e1cc17b95488728d6dc3b2bbb84689252 Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 14 Feb 2026 13:55:51 +0100 Subject: [PATCH 4/4] Update initializer expr test for self::const inference --- tests/PHPStan/Analyser/nsrt/initializer-expr-type-resolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/initializer-expr-type-resolver.php b/tests/PHPStan/Analyser/nsrt/initializer-expr-type-resolver.php index 7a5daede04..82a6d52460 100644 --- a/tests/PHPStan/Analyser/nsrt/initializer-expr-type-resolver.php +++ b/tests/PHPStan/Analyser/nsrt/initializer-expr-type-resolver.php @@ -14,7 +14,7 @@ class Foo public function doFoo(): void { - assertType('*ERROR*', self::COALESCE_SPECIAL); // could be 42 + assertType('42', self::COALESCE_SPECIAL); assertType("0|1|2|'foo'", self::COALESCE); assertType("'bar'|'foo'|true", self::TERNARY_SHORT); assertType("'bar'|'foo'", self::TERNARY_FULL);