From 54456c5755202ee5df91f11d38c5c87c5f5b7347 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 5 Sep 2025 17:55:45 +0200 Subject: [PATCH 1/2] Add ConstFetchNode support in array shape --- src/PhpDoc/TypeNodeResolver.php | 75 ++++++++++++++++++++---- tests/PHPStan/Analyser/nsrt/bug-6989.php | 22 +++++++ 2 files changed, 87 insertions(+), 10 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-6989.php diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index bbd3d0929a..ced0b7804b 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -20,6 +20,7 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprTrueNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; +use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode; @@ -1050,16 +1051,7 @@ private function resolveArrayShapeNode(ArrayShapeNode $typeNode, NameScope $name } foreach ($typeNode->items as $itemNode) { - $offsetType = null; - if ($itemNode->keyName instanceof ConstExprIntegerNode) { - $offsetType = new ConstantIntegerType((int) $itemNode->keyName->value); - } elseif ($itemNode->keyName instanceof IdentifierTypeNode) { - $offsetType = new ConstantStringType($itemNode->keyName->name); - } elseif ($itemNode->keyName instanceof ConstExprStringNode) { - $offsetType = new ConstantStringType($itemNode->keyName->value); - } elseif ($itemNode->keyName !== null) { - throw new ShouldNotHappenException('Unsupported key node type: ' . get_class($itemNode->keyName)); - } + $offsetType = $this->resolveArrayShapeOffsetType($itemNode, $nameScope); $builder->setOffsetValueType($offsetType, $this->resolve($itemNode->valueType, $nameScope), $itemNode->optional); } @@ -1081,6 +1073,69 @@ private function resolveArrayShapeNode(ArrayShapeNode $typeNode, NameScope $name return $arrayType; } + private function resolveArrayShapeOffsetType(ArrayShapeItemNode $itemNode, NameScope $nameScope): ?Type + { + if ($itemNode->keyName instanceof ConstExprIntegerNode) { + return new ConstantIntegerType((int) $itemNode->keyName->value); + } elseif ($itemNode->keyName instanceof IdentifierTypeNode) { + return new ConstantStringType($itemNode->keyName->name); + } elseif ($itemNode->keyName instanceof ConstExprStringNode) { + return new ConstantStringType($itemNode->keyName->value); + } elseif ($itemNode->keyName instanceof ConstFetchNode) { + $constExpr = $itemNode->keyName; + if ($constExpr->className === '') { + throw new ShouldNotHappenException(); // global constant should get parsed as class name in IdentifierTypeNode + } + + if ($nameScope->getClassName() !== null) { + switch (strtolower($constExpr->className)) { + case 'static': + case 'self': + $className = $nameScope->getClassName(); + break; + + case 'parent': + if ($this->getReflectionProvider()->hasClass($nameScope->getClassName())) { + $classReflection = $this->getReflectionProvider()->getClass($nameScope->getClassName()); + if ($classReflection->getParentClass() === null) { + return new ErrorType(); + + } + + $className = $classReflection->getParentClass()->getName(); + } + break; + } + } + + if (!isset($className)) { + $className = $nameScope->resolveStringName($constExpr->className); + } + + if (!$this->getReflectionProvider()->hasClass($className)) { + return new ErrorType(); + } + $classReflection = $this->getReflectionProvider()->getClass($className); + + $constantName = $constExpr->name; + if (!$classReflection->hasConstant($constantName)) { + return new ErrorType(); + } + + $reflectionConstant = $classReflection->getNativeReflection()->getReflectionConstant($constantName); + if ($reflectionConstant === false) { + return new ErrorType(); + } + $declaringClass = $reflectionConstant->getDeclaringClass(); + + return $this->initializerExprTypeResolver->getType($reflectionConstant->getValueExpression(), InitializerExprContext::fromClass($declaringClass->getName(), $declaringClass->getFileName() ?: null)); + } elseif ($itemNode->keyName !== null) { + throw new ShouldNotHappenException('Unsupported key node type: ' . get_class($itemNode->keyName)); + } + + return null; + } + private function resolveObjectShapeNode(ObjectShapeNode $typeNode, NameScope $nameScope): Type { $properties = []; diff --git a/tests/PHPStan/Analyser/nsrt/bug-6989.php b/tests/PHPStan/Analyser/nsrt/bug-6989.php new file mode 100644 index 0000000000..5d53c314d5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6989.php @@ -0,0 +1,22 @@ + Date: Sat, 6 Sep 2025 18:10:03 +0200 Subject: [PATCH 2/2] Update tests --- tests/PHPStan/Analyser/nsrt/bug-6989.php | 37 +++++++++++++++++++++--- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-6989.php b/tests/PHPStan/Analyser/nsrt/bug-6989.php index 5d53c314d5..3ea5dbe4b3 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6989.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6989.php @@ -9,14 +9,43 @@ class MyClass public const MY_KEY = 'key'; /** - * @param array{static::MY_KEY: string} $items + * @param array{static::MY_KEY: string} $items1 + * @param array{self::MY_KEY: string} $items2 + * @param array{MyClass::MY_KEY: string} $items3 * * @return string */ - public function myMethod(array $items): array + public function myMethod(array $items1, array $items2, array $items3): array { - assertType('array{key: string}', $items); + assertType('array{key: string}', $items1); + assertType('array{key: string}', $items2); + assertType('array{key: string}', $items3); - return $items[static::MY_KEY]; + return $items1[static::MY_KEY]; + } +} + +class ParentClass extends MyClass +{ + public const MY_KEY = 'different_key'; + + /** + * @param array{static::MY_KEY: string} $items1 + * @param array{self::MY_KEY: string} $items2 + * @param array{MyClass::MY_KEY: string} $items3 + * @param array{ParentClass::MY_KEY: string} $items4 + * @param array{parent::MY_KEY: string} $items5 + * + * @return string + */ + public function myMethod2(array $items1, array $items2, array $items3, array $items4, array $items5): array + { + assertType('array{different_key: string}', $items1); + assertType('array{different_key: string}', $items2); + assertType('array{key: string}', $items3); + assertType('array{different_key: string}', $items4); + assertType('array{key: string}', $items5); + + return $items1[static::MY_KEY]; } }