From f141533d9b7b3125d746261992943ee493afae1f Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Wed, 28 Oct 2026 20:47:17 +0000 Subject: [PATCH 1/2] Handle phpdoc-parser 2.4 name nodes when resolving PHPDocs phpdoc-parser 2.4 adds FullyQualifiedNameNode and RelativeNameNode for class names. They do not extend IdentifierTypeNode, and GenericTypeNode::$type, CallableTypeNode::$identifier and TypeAliasImportTagValueNode::$importedFrom can now hold them. TypeNodeResolver resolves a name node as a class against the name scope, before keywords, type aliases and template types are considered, so a RelativeNameNode named Int or T is a class, not int or a template type. The tag resolvers and IntersectionType::toPhpDocNode() no longer assume an IdentifierTypeNode there. Amp-Thread-ID: https://ampcode.com/threads/T-01a179f2-995f-7745-8d8c-4c174ba5c0ce --- src/PhpDoc/PhpDocNodeResolver.php | 8 ++-- src/PhpDoc/TypeNodeResolver.php | 20 ++++++++-- src/Type/IntersectionType.php | 2 +- tests/PHPStan/Type/TypeToPhpDocNodeTest.php | 41 +++++++++++++++++++++ turbo-ext/src/IntersectionType.cpp | 7 +++- 5 files changed, 68 insertions(+), 10 deletions(-) diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 048031ae7fb..004444161d0 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -236,7 +236,7 @@ public function resolveExtendsTags(PhpDocNode $phpDocNode, NameScope $nameScope) foreach (['@extends', '@phan-extends', '@phan-inherits', '@template-extends', '@phpstan-extends'] as $tagName) { foreach ($phpDocNode->getExtendsTagValues($tagName) as $tagValue) { - $resolved[$nameScope->resolveStringName($tagValue->type->type->name)] = new ExtendsTag( + $resolved[$nameScope->resolveStringName((string) $tagValue->type->type)] = new ExtendsTag( $this->typeNodeResolver->resolve($tagValue->type, $nameScope), ); } @@ -254,7 +254,7 @@ public function resolveImplementsTags(PhpDocNode $phpDocNode, NameScope $nameSco foreach (['@implements', '@template-implements', '@phpstan-implements'] as $tagName) { foreach ($phpDocNode->getImplementsTagValues($tagName) as $tagValue) { - $resolved[$nameScope->resolveStringName($tagValue->type->type->name)] = new ImplementsTag( + $resolved[$nameScope->resolveStringName((string) $tagValue->type->type)] = new ImplementsTag( $this->typeNodeResolver->resolve($tagValue->type, $nameScope), ); } @@ -272,7 +272,7 @@ public function resolveUsesTags(PhpDocNode $phpDocNode, NameScope $nameScope): a foreach (['@use', '@template-use', '@phpstan-use'] as $tagName) { foreach ($phpDocNode->getUsesTagValues($tagName) as $tagValue) { - $resolved[$nameScope->resolveStringName($tagValue->type->type->name)] = new UsesTag( + $resolved[$nameScope->resolveStringName((string) $tagValue->type->type)] = new UsesTag( $this->typeNodeResolver->resolve($tagValue->type, $nameScope), ); } @@ -571,7 +571,7 @@ public function resolveTypeAliasImportTags(PhpDocNode $phpDocNode, NameScope $na foreach (['@psalm-import-type', '@phpstan-import-type'] as $tagName) { foreach ($phpDocNode->getTypeAliasImportTagValues($tagName) as $typeAliasImportTagValue) { $importedAlias = $typeAliasImportTagValue->importedAlias; - $importedFrom = $nameScope->resolveStringName($typeAliasImportTagValue->importedFrom->name); + $importedFrom = $nameScope->resolveStringName((string) $typeAliasImportTagValue->importedFrom); $importedAs = $typeAliasImportTagValue->importedAs; $resolved[$importedAs ?? $importedAlias] = new TypeAliasImportTag($importedAlias, $importedFrom, $importedAs); } diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 4c52e21fe2a..832cb06d94c 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -34,6 +34,7 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode; use PHPStan\PhpDocParser\Ast\Type\InvalidTypeNode; +use PHPStan\PhpDocParser\Ast\Type\NameNode; use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode; use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode; use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode; @@ -170,7 +171,10 @@ public function resolve(TypeNode $typeNode, NameScope $nameScope): Type } } - if ($typeNode instanceof IdentifierTypeNode) { + if ($typeNode instanceof NameNode) { + return $this->resolveNameNode($typeNode, $nameScope); + + } elseif ($typeNode instanceof IdentifierTypeNode) { return $this->resolveIdentifierTypeNode($typeNode, $nameScope); } elseif ($typeNode instanceof ThisTypeNode) { @@ -215,6 +219,14 @@ public function resolve(TypeNode $typeNode, NameScope $nameScope): Type return new ErrorType(); } + /** + * A name node is always a class, even when it is named like a keyword or a template type. + */ + private function resolveNameNode(NameNode $typeNode, NameScope $nameScope): Type + { + return new ObjectType($nameScope->resolveStringName($typeNode->toCodeString())); + } + private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameScope $nameScope): Type { switch (strtolower($typeNode->name)) { @@ -692,7 +704,7 @@ private function resolveArrayTypeNode(ArrayTypeNode $typeNode, NameScope $nameSc private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $nameScope): Type { - $mainTypeName = strtolower($typeNode->type->name); + $mainTypeName = $typeNode->type instanceof NameNode ? null : strtolower($typeNode->type->name); $genericTypes = $this->resolveMultiple($typeNode->genericTypes, $nameScope); $variances = array_map( static function (string $variance): TemplateTypeVariance { @@ -861,7 +873,9 @@ static function (string $variance): TemplateTypeVariance { return new ErrorType(); } - $mainType = $this->resolveIdentifierTypeNode($typeNode->type, $nameScope); + $mainType = $typeNode->type instanceof NameNode + ? $this->resolveNameNode($typeNode->type, $nameScope) + : $this->resolveIdentifierTypeNode($typeNode->type, $nameScope); $mainTypeObjectClassNames = $mainType->getObjectClassNames(); if (count($mainTypeObjectClassNames) > 1) { if ($mainType instanceof TemplateType) { diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 758350e1b70..8052cdde3c2 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -2056,7 +2056,7 @@ public function toPhpDocNode(): TypeNode foreach ($baseTypes as $i => $type) { $typeNode = $type->toPhpDocNode(); - if ($typeNode instanceof GenericTypeNode && $typeNode->type->name === 'array') { + if ($typeNode instanceof GenericTypeNode && $typeNode->type instanceof IdentifierTypeNode && $typeNode->type->name === 'array') { $nonEmpty = false; $typeName = 'array'; foreach ($typesToDescribe as $j => $typeToDescribe) { diff --git a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php index 208d6ff91fe..3f2bb62ffb2 100644 --- a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php +++ b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php @@ -3,7 +3,13 @@ namespace PHPStan\Type; use PHPStan\Analyser\NameScope; +use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\PhpDoc\TypeStringResolver; +use PHPStan\PhpDocParser\Ast\Type\FullyQualifiedNameNode; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\RelativeNameNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; @@ -650,6 +656,41 @@ public function testLateResolvableArrayShapeToPhpDocNode(string $typeString): vo $this->assertTrue($type->equals($typeAgain)); } + /** + * @return iterable + */ + public static function dataResolveNameNodes(): iterable + { + // a NameNode is always a class, even when it is named like a keyword or a template type + yield [new RelativeNameNode('Int'), 'App\\Int']; + yield [new RelativeNameNode('T'), 'App\\T']; + yield [new RelativeNameNode('Bar'), 'Vendor\\Bar']; + yield [new FullyQualifiedNameNode('Int'), 'Int']; + yield [new GenericTypeNode(new RelativeNameNode('List'), [new IdentifierTypeNode('int')]), 'App\\List']; + yield [new GenericTypeNode(new FullyQualifiedNameNode('Bar'), [new RelativeNameNode('T')]), 'Bar']; + + // plain identifiers keep resolving keywords and template types first + yield [new IdentifierTypeNode('Int'), 'int']; + yield [new IdentifierTypeNode('T'), 'T (function doFoo(), parameter)']; + yield [new GenericTypeNode(new IdentifierTypeNode('List'), [new IdentifierTypeNode('T')]), 'list']; + } + + #[DataProvider('dataResolveNameNodes')] + public function testResolveNameNodes(TypeNode $typeNode, string $expected): void + { + $nameScope = new NameScope('App', ['bar' => 'Vendor\\Bar'], null, 'doFoo', new TemplateTypeMap([ + 'T' => TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('doFoo'), + 'T', + null, + TemplateTypeVariance::createInvariant(), + ), + ])); + + $type = self::getContainer()->getByType(TypeNodeResolver::class)->resolve($typeNode, $nameScope); + $this->assertSame($expected, $type->describe(VerbosityLevel::precise())); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/turbo-ext/src/IntersectionType.cpp b/turbo-ext/src/IntersectionType.cpp index 7eca740174f..043ef370f6d 100644 --- a/turbo-ext/src/IntersectionType.cpp +++ b/turbo-ext/src/IntersectionType.cpp @@ -2433,10 +2433,13 @@ class IntersectionType zv::Val identifier = readProperty(typeNode.raw(), PT_LC("type")); if (UNEXPECTED(identifier.isUndef())) return zv::Val(); if (UNEXPECTED(!zv::Ref(identifier.raw()).isObject())) { - zend_type_error("phpstan_turbo: GenericTypeNode::$type must be an IdentifierTypeNode"); + zend_type_error("phpstan_turbo: GenericTypeNode::$type must be an IdentifierTypeNode or a NameNode"); return zv::Val(); } - zv::Val identifierName = readProperty(identifier.raw(), PT_LC("name")); + /* a NameNode is a class, never the array keyword */ + bool isIdentifier; + if (UNEXPECTED(!isInstance(identifier.raw(), PT_CLASS_IDENTIFIER_TYPE_NODE, isIdentifier))) return zv::Val(); + zv::Val identifierName = isIdentifier ? readProperty(identifier.raw(), PT_LC("name")) : zv::Val::null(); if (UNEXPECTED(identifierName.isUndef())) return zv::Val(); if (Z_TYPE_P(identifierName.raw()) == IS_STRING && zend_string_equals_literal(Z_STR_P(identifierName.raw()), "array")) { bool nonEmpty = false; From 17f4072dcff7a3b598c067f4a5727cb39d183a0a Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Wed, 28 Oct 2026 20:47:26 +0000 Subject: [PATCH 2/2] Return name nodes for class names from Type::toPhpDocNode() Type::toPhpDocNode() returned a plain IdentifierTypeNode for both keywords (int, mixed, non-falsy-string) and class names, so consumers like Rector had to guess which is which and how to print it. ObjectType, GenericObjectType, ClosureType and GetTemplateTypeType now return phpdoc-parser's FullyQualifiedNameNode, and EnumCaseObjectType a fully qualified ConstFetchNode, so a printed node resolves to the same class in any namespace. Keywords, pure-/static-Closure and template type names stay IdentifierTypeNode. describe() output is unchanged: TypeNodeDescriber prints names without the leading backslash. The turbo extension returns the same nodes. Amp-Thread-ID: https://ampcode.com/threads/T-01a179f2-995f-7745-8d8c-4c174ba5c0ce --- .../PHPStan/Build/TurboAttributeCollector.php | 4 +- src/Type/CallableType.php | 4 +- src/Type/ClosureType.php | 11 ++- src/Type/Enum/EnumCaseObjectType.php | 2 +- src/Type/Generic/GenericObjectType.php | 4 +- src/Type/Helper/GetTemplateTypeType.php | 3 +- src/Type/LateResolvableArrayShapeType.php | 3 +- src/Type/ObjectType.php | 4 +- src/Type/OffsetAccessType.php | 5 +- src/Type/TypeNodeDescriber.php | 50 ++++++++++ tests/PHPStan/PhpDoc/TypeDescriptionTest.php | 2 + tests/PHPStan/Type/TypeToPhpDocNodeTest.php | 99 +++++++++++++++---- turbo-ext/src/CallableType.cpp | 8 +- turbo-ext/src/ClosureType.cpp | 17 ++-- turbo-ext/src/EnumCaseObjectType.cpp | 5 +- turbo-ext/src/GetTemplateTypeType.cpp | 4 +- turbo-ext/src/ObjectType.cpp | 4 +- turbo-ext/src/TypeTraits.cpp | 14 ++- turbo-ext/src/TypeTraits.h | 5 +- turbo-ext/src/support.cpp | 3 +- turbo-ext/src/support.h | 4 +- 21 files changed, 184 insertions(+), 71 deletions(-) create mode 100644 src/Type/TypeNodeDescriber.php diff --git a/build/PHPStan/Build/TurboAttributeCollector.php b/build/PHPStan/Build/TurboAttributeCollector.php index d79dba24fc4..43779900a77 100644 --- a/build/PHPStan/Build/TurboAttributeCollector.php +++ b/build/PHPStan/Build/TurboAttributeCollector.php @@ -178,6 +178,7 @@ use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeForParameterNode; use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeNode; use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; +use PHPStan\PhpDocParser\Ast\Type\FullyQualifiedNameNode; use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode; @@ -186,7 +187,6 @@ use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode; use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; -use PHPStan\PhpDocParser\Printer\Printer; use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Turbo\ShadowedByTurboExtension; use RecursiveDirectoryIterator; @@ -264,6 +264,7 @@ final class TurboAttributeCollector 'closureExpr' => Closure::class, 'arrowFunction' => ArrowFunction::class, 'identifierTypeNode' => IdentifierTypeNode::class, + 'fullyQualifiedNameNode' => FullyQualifiedNameNode::class, 'genericTypeNode' => GenericTypeNode::class, 'constTypeNode' => ConstTypeNode::class, 'constExprIntegerNode' => ConstExprIntegerNode::class, @@ -275,7 +276,6 @@ final class TurboAttributeCollector 'objectShapeNode' => ObjectShapeNode::class, 'objectShapeItemNode' => ObjectShapeItemNode::class, 'constFetchNode' => ConstFetchNode::class, - 'phpDocPrinter' => Printer::class, 'callableTypeNode' => CallableTypeNode::class, 'callableTypeParameterNode' => CallableTypeParameterNode::class, 'templateTagValueNode' => TemplateTagValueNode::class, diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index ac7ef2bc0da..b58dd0fb886 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -12,7 +12,6 @@ use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; -use PHPStan\PhpDocParser\Printer\Printer; use PHPStan\Reflection\Assertions; use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\Reflection\Callables\SimpleImpurePoint; @@ -337,7 +336,6 @@ public function describe(VerbosityLevel $level): string return $level->handle( static fn (): string => 'callable', function (): string { - $printer = new Printer(); $assertedParameterNames = []; foreach ($this->assertions->getAll() as $assertTag) { $assertedParameterNames[$assertTag->getParameter()->getParameterName()] = true; @@ -360,7 +358,7 @@ function (): string { $this->assertions, ); - return $printer->print($selfWithoutParameterNames->toPhpDocNode()); + return TypeNodeDescriber::describe($selfWithoutParameterNames->toPhpDocNode()); }, ); } diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index 77b77365c43..2bf0c3b332b 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -11,9 +11,9 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode; +use PHPStan\PhpDocParser\Ast\Type\FullyQualifiedNameNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; -use PHPStan\PhpDocParser\Printer\Printer; use PHPStan\Reflection\Assertions; use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\Reflection\Callables\SimpleImpurePoint; @@ -352,7 +352,6 @@ function (): string { private function describeCallable(): string { - $printer = new Printer(); $assertedParameterNames = []; foreach ($this->assertions->getAll() as $assertTag) { $assertedParameterNames[$assertTag->getParameter()->getParameterName()] = true; @@ -382,7 +381,7 @@ private function describeCallable(): string $this->isStatic, ); - return $printer->print($selfWithoutParameterNames->toPhpDocNode()); + return TypeNodeDescriber::describe($selfWithoutParameterNames->toPhpDocNode()); } public function isOffsetAccessLegal(): TrinaryLogic @@ -997,6 +996,10 @@ public function toPhpDocNode(): TypeNode { if ($this->isCommonCallable) { $prefix = $this->isStatic->yes() ? 'static-' : ''; + if ($prefix === '' && !$this->isPure()->yes()) { + return new FullyQualifiedNameNode(Closure::class); + } + $name = $this->isPure()->yes() ? 'pure-Closure' : 'Closure'; return new IdentifierTypeNode($prefix . $name); } @@ -1025,7 +1028,7 @@ public function toPhpDocNode(): TypeNode ?? $this->returnType->toPhpDocNode(); return new CallableTypeNode( - new IdentifierTypeNode('Closure'), + new FullyQualifiedNameNode(Closure::class), $parameters, $returnTypeNode, $templateTags, diff --git a/src/Type/Enum/EnumCaseObjectType.php b/src/Type/Enum/EnumCaseObjectType.php index f6b9b17f998..21ca3d380ea 100644 --- a/src/Type/Enum/EnumCaseObjectType.php +++ b/src/Type/Enum/EnumCaseObjectType.php @@ -250,7 +250,7 @@ public function toPhpDocNode(): TypeNode { return new ConstTypeNode( new ConstFetchNode( - $this->getClassName(), + '\\' . $this->getClassName(), $this->getEnumCaseName(), ), ); diff --git a/src/Type/Generic/GenericObjectType.php b/src/Type/Generic/GenericObjectType.php index 77e187d5927..a870460d2a4 100644 --- a/src/Type/Generic/GenericObjectType.php +++ b/src/Type/Generic/GenericObjectType.php @@ -2,8 +2,8 @@ namespace PHPStan\Type\Generic; +use PHPStan\PhpDocParser\Ast\Type\FullyQualifiedNameNode; use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; -use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; @@ -454,7 +454,7 @@ public function changeSubtractedType(?Type $subtractedType): Type public function toPhpDocNode(): TypeNode { - /** @var IdentifierTypeNode $parent */ + /** @var FullyQualifiedNameNode $parent */ $parent = parent::toPhpDocNode(); return new GenericTypeNode( $parent, diff --git a/src/Type/Helper/GetTemplateTypeType.php b/src/Type/Helper/GetTemplateTypeType.php index 1d13dd188b0..2920f52c38b 100644 --- a/src/Type/Helper/GetTemplateTypeType.php +++ b/src/Type/Helper/GetTemplateTypeType.php @@ -4,6 +4,7 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; +use PHPStan\PhpDocParser\Ast\Type\FullyQualifiedNameNode; use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; @@ -99,7 +100,7 @@ public function toPhpDocNode(): TypeNode new IdentifierTypeNode('template-type'), [ $this->type->toPhpDocNode(), - new IdentifierTypeNode($this->ancestorClassName), + new FullyQualifiedNameNode($this->ancestorClassName), new ConstTypeNode(new ConstExprStringNode($this->templateTypeName, ConstExprStringNode::SINGLE_QUOTED)), ], ); diff --git a/src/Type/LateResolvableArrayShapeType.php b/src/Type/LateResolvableArrayShapeType.php index 1a2da7ad107..d7933201dcc 100644 --- a/src/Type/LateResolvableArrayShapeType.php +++ b/src/Type/LateResolvableArrayShapeType.php @@ -11,7 +11,6 @@ use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; -use PHPStan\PhpDocParser\Printer\Printer; use PHPStan\ShouldNotHappenException; use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\Accessory\AccessoryArrayListType; @@ -171,7 +170,7 @@ public function equals(Type $type): bool public function describe(VerbosityLevel $level): string { // a shape that can be resolved never stays late-resolvable, see create() - return (new Printer())->print($this->toPhpDocNode()); + return TypeNodeDescriber::describe($this->toPhpDocNode()); } public function isResolvable(): bool diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index ec9bbbd1e25..b358bf41754 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -14,7 +14,7 @@ use PHPStan\Broker\ClassNotFoundException; use PHPStan\Internal\LruCache; use PHPStan\Php\PhpVersion; -use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\FullyQualifiedNameNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\Reflection\Callables\FunctionCallableVariant; @@ -2131,7 +2131,7 @@ public function exponentiate(Type $exponent): Type public function toPhpDocNode(): TypeNode { - return new IdentifierTypeNode($this->getClassName()); + return new FullyQualifiedNameNode($this->getClassName()); } public function toNativePhpTypeNode(NativeTypePosition $position) diff --git a/src/Type/OffsetAccessType.php b/src/Type/OffsetAccessType.php index 7ef6b306778..d069d847b3d 100644 --- a/src/Type/OffsetAccessType.php +++ b/src/Type/OffsetAccessType.php @@ -4,7 +4,6 @@ use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; -use PHPStan\PhpDocParser\Printer\Printer; use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Traits\LateResolvableTypeTrait; @@ -61,9 +60,7 @@ public function equals(Type $type): bool public function describe(VerbosityLevel $level): string { - $printer = new Printer(); - - return $printer->print($this->toPhpDocNode()); + return TypeNodeDescriber::describe($this->toPhpDocNode()); } public function isResolvable(): bool diff --git a/src/Type/TypeNodeDescriber.php b/src/Type/TypeNodeDescriber.php new file mode 100644 index 00000000000..c8b257a4f39 --- /dev/null +++ b/src/Type/TypeNodeDescriber.php @@ -0,0 +1,50 @@ +toString()); + } + + return null; + } + + }, + ]); + self::$printer ??= new Printer(); + [$typeNode] = self::$traverser->traverse([$typeNode]); + + return self::$printer->print($typeNode); + } + +} diff --git a/tests/PHPStan/PhpDoc/TypeDescriptionTest.php b/tests/PHPStan/PhpDoc/TypeDescriptionTest.php index 4b668b9af5c..5df9b9572fc 100644 --- a/tests/PHPStan/PhpDoc/TypeDescriptionTest.php +++ b/tests/PHPStan/PhpDoc/TypeDescriptionTest.php @@ -23,6 +23,7 @@ use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; +use PHPStan\Type\OffsetAccessType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; @@ -44,6 +45,7 @@ public static function dataTest(): iterable yield ['non-empty-array', new IntersectionType([new ArrayType(new IntegerType(), new StringType()), new NonEmptyArrayType()])]; yield ['class-string&literal-string', new IntersectionType([new ClassStringType(), new AccessoryLiteralStringType()])]; yield ['class-string&literal-string', new IntersectionType([new GenericClassStringType(new ObjectType('Foo')), new AccessoryLiteralStringType()])]; + yield ['Foo\\Bar[\'baz\']', new OffsetAccessType(new ObjectType('Foo\\Bar'), new ConstantStringType('baz'))]; $builder = ConstantArrayTypeBuilder::createEmpty(); $builder->setOffsetValueType(new ConstantStringType('foo'), new IntegerType()); diff --git a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php index 3f2bb62ffb2..3216a87463d 100644 --- a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php +++ b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php @@ -5,9 +5,11 @@ use PHPStan\Analyser\NameScope; use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\PhpDoc\TypeStringResolver; +use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode; use PHPStan\PhpDocParser\Ast\Type\FullyQualifiedNameNode; use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\NameNode; use PHPStan\PhpDocParser\Ast\Type\RelativeNameNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Testing\PHPStanTestCase; @@ -64,7 +66,7 @@ public static function dataToPhpDocNode(): iterable yield [ new ObjectType(stdClass::class), - 'stdClass', + '\\stdClass', ]; yield [ @@ -147,7 +149,7 @@ public static function dataToPhpDocNode(): iterable yield [ new GenericClassStringType(new ObjectType('stdClass')), - 'class-string', + 'class-string<\\stdClass>', ]; yield [ @@ -155,7 +157,7 @@ public static function dataToPhpDocNode(): iterable new ConstantIntegerType(1), new ConstantIntegerType(2), ]), - 'stdClass<1, 2>', + '\\stdClass<1, 2>', ]; yield [ @@ -168,7 +170,7 @@ public static function dataToPhpDocNode(): iterable TemplateTypeVariance::createContravariant(), TemplateTypeVariance::createBivariant(), ]), - 'stdClass', + '\\stdClass', ]; yield [ @@ -213,7 +215,7 @@ public static function dataToPhpDocNode(): iterable new StringType(), false, ), - '(object is stdClass ? int : string)', + '(object is \\stdClass ? int : string)', ]; yield [ @@ -224,7 +226,7 @@ public static function dataToPhpDocNode(): iterable new StringType(), true, ), - '(object is not stdClass ? int : string)', + '(object is not \\stdClass ? int : string)', ]; yield [ @@ -279,7 +281,7 @@ public static function dataToPhpDocNode(): iterable yield [ new IntersectionType([new GenericClassStringType(new ObjectType('Foo')), new AccessoryLiteralStringType()]), - '(class-string & literal-string)', + '(class-string<\\Foo> & literal-string)', ]; yield [ @@ -561,7 +563,7 @@ public static function dataToPhpDocNodeWithoutCheckingEquals(): iterable yield [ new ClosureType(), - 'Closure', + '\\Closure', ]; } @@ -584,25 +586,25 @@ public static function dataFromTypeStringToPhpDocNode(): iterable } yield ['callable']; - yield ['callable(Foo): Bar']; - yield ['callable(Foo=, Bar=): Bar']; - yield ['Closure(Foo=, Bar=): Bar']; + yield ['callable(\\Foo): \\Bar']; + yield ['callable(\\Foo=, \\Bar=): \\Bar']; + yield ['\\Closure(\\Foo=, \\Bar=): \\Bar']; - yield ['callable(Foo $foo): Bar']; - yield ['callable(Foo $foo=, Bar $bar=): Bar']; - yield ['Closure(Foo $foo=, Bar $bar=): Bar']; - yield ['Closure(Foo $foo=, Bar $bar=): (Closure(Foo): Bar)']; + yield ['callable(\\Foo $foo): \\Bar']; + yield ['callable(\\Foo $foo=, \\Bar $bar=): \\Bar']; + yield ['\\Closure(\\Foo $foo=, \\Bar $bar=): \\Bar']; + yield ['\\Closure(\\Foo $foo=, \\Bar $bar=): (\\Closure(\\Foo): \\Bar)']; yield ['array{a: int}']; yield ['array{a: int, ...}']; - yield ['array{a: int, ...}']; - yield ['array{a: int, ...}']; + yield ['array{a: int, ...<\\Foo>}']; + yield ['array{a: int, ...}']; yield ['array{int, int, int, ...}']; - yield ['array{int, int, int, ...}']; - yield ['array{int, int, int, ...}']; + yield ['array{int, int, int, ...<\\Foo>}']; + yield ['array{int, int, int, ...}']; yield ['list{0?: int, 1?: int, 2?: int, ...}']; - yield ['list{0?: int, 1?: int, 2?: int, ...}']; + yield ['list{0?: int, 1?: int, 2?: int, ...<\\Foo>}']; } #[DataProvider('dataFromTypeStringToPhpDocNode')] @@ -656,6 +658,63 @@ public function testLateResolvableArrayShapeToPhpDocNode(string $typeString): vo $this->assertTrue($type->equals($typeAgain)); } + /** + * @return iterable + */ + public static function dataNamesResolveInNamespace(): iterable + { + yield [new ObjectType('Foo\\Bar')]; + yield [TypeCombinator::addNull(new ObjectType('Foo\\Bar'))]; + yield [new GenericObjectType('Foo\\Bar', [new ObjectType('Foo\\Baz')])]; + yield [new GenericClassStringType(new ObjectType('Foo\\Bar'))]; + } + + #[DataProvider('dataNamesResolveInNamespace')] + public function testNamesResolveInNamespace(Type $type): void + { + // a use or namespace that would capture the name if it were printed relative + $nameScope = new NameScope('Other', ['foo' => 'Some\\Foo', 'closure' => 'Some\\Closure']); + + $typeStringResolver = self::getContainer()->getByType(TypeStringResolver::class); + $typeAgain = $typeStringResolver->resolve((string) $type->toPhpDocNode(), $nameScope); + $this->assertTrue($type->equals($typeAgain), sprintf('%s->equals(%s)', $type->describe(VerbosityLevel::precise()), $typeAgain->describe(VerbosityLevel::precise()))); + } + + public function testClosureResolvesInNamespaceWithClosureUse(): void + { + $nameScope = new NameScope('Other', ['closure' => 'Some\\Closure']); + $typeStringResolver = self::getContainer()->getByType(TypeStringResolver::class); + + $this->assertSame(['Closure'], $typeStringResolver->resolve((string) (new ClosureType())->toPhpDocNode(), $nameScope)->getObjectClassNames()); + } + + public function testNameNodes(): void + { + $this->assertInstanceOf(FullyQualifiedNameNode::class, (new ObjectType('Foo\\Bar'))->toPhpDocNode()); + $this->assertInstanceOf(FullyQualifiedNameNode::class, (new ClosureType())->toPhpDocNode()); + + $generic = (new GenericObjectType('Foo\\Bar', [new IntegerType()]))->toPhpDocNode(); + $this->assertInstanceOf(GenericTypeNode::class, $generic); + $this->assertInstanceOf(FullyQualifiedNameNode::class, $generic->type); + $this->assertNotInstanceOf(NameNode::class, $generic->genericTypes[0]); + + $closure = self::getContainer()->getByType(TypeStringResolver::class)->resolve('\\Closure(int): void')->toPhpDocNode(); + $this->assertInstanceOf(CallableTypeNode::class, $closure); + $this->assertInstanceOf(FullyQualifiedNameNode::class, $closure->identifier); + + // keywords stay plain identifiers + foreach ([ + new IntegerType(), + new MixedType(true), + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new CallableType(), + ClosureType::createPure(), + TemplateTypeFactory::create(TemplateTypeScope::createWithFunction('foo'), 'T', null, TemplateTypeVariance::createInvariant()), + ] as $keywordType) { + $this->assertNotInstanceOf(NameNode::class, $keywordType->toPhpDocNode(), $keywordType->describe(VerbosityLevel::precise())); + } + } + /** * @return iterable */ diff --git a/turbo-ext/src/CallableType.cpp b/turbo-ext/src/CallableType.cpp index 9cb6be3a4c7..69a84405ff2 100644 --- a/turbo-ext/src/CallableType.cpp +++ b/turbo-ext/src/CallableType.cpp @@ -382,8 +382,6 @@ class CallableType pt_verbosity_case which; if (UNEXPECTED(!pt_type_verbosity_case(level, which))) return zv::Val(); if (which == PT_VERBOSITY_TYPE_ONLY) return zv::Val::string("callable", sizeof("callable") - 1); - zv::Val printer = pt_type_new(PT_CLASS_PHPDOC_PRINTER, 0, NULL); - if (UNEXPECTED(printer.isUndef())) return zv::Val(); zval *a = assertions(); zval *p = a != NULL ? parameters() : NULL; if (UNEXPECTED(p == NULL)) return zv::Val(); @@ -400,7 +398,7 @@ class CallableType if (UNEXPECTED(selfWithoutParameterNames.isUndef())) return zv::Val(); zv::Val node = CallableType(Z_OBJ_P(selfWithoutParameterNames.raw())).toPhpDocNode(); if (UNEXPECTED(node.isUndef())) return zv::Val(); - return pt_type_call(Z_OBJ_P(printer.raw()), PT_LC("print"), 1, node.raw()); + return pt_describe_php_doc_node(node.raw()); } /* [SimpleThrowPoint::createImplicit()] */ @@ -605,8 +603,8 @@ class CallableType /* $this->isPure->yes() — the slot, not the method */ zend_long pureValue = pt_type_trinary_value(pure); if (UNEXPECTED(pureValue < 0)) return zv::Val(); - if (pureValue == PT_TRI_YES) return pt_callable_type_node(PT_LC("pure-callable"), p, tags, a, r); - return pt_callable_type_node(PT_LC("callable"), p, tags, a, r); + if (pureValue == PT_TRI_YES) return pt_callable_type_node(PT_LC("pure-callable"), false, p, tags, a, r); + return pt_callable_type_node(PT_LC("callable"), false, p, tags, a, r); } /* false = pending exception */ diff --git a/turbo-ext/src/ClosureType.cpp b/turbo-ext/src/ClosureType.cpp index 4adaaa71b21..4fe477174e3 100644 --- a/turbo-ext/src/ClosureType.cpp +++ b/turbo-ext/src/ClosureType.cpp @@ -512,8 +512,6 @@ class ClosureType * (unless an assertion refers to them); UNDEF = pending exception */ zv::Val describeCallable() const { - zv::Val printer = pt_type_new(PT_CLASS_PHPDOC_PRINTER, 0, NULL); - if (UNEXPECTED(printer.isUndef())) return zv::Val(); zval *a = assertions(); zval *p = a != NULL ? parameters() : NULL; if (UNEXPECTED(p == NULL)) return zv::Val(); @@ -526,11 +524,7 @@ class ClosureType if (UNEXPECTED(selfWithoutParameterNames.isUndef())) return zv::Val(); zv::Val node = ClosureType(Z_OBJ_P(selfWithoutParameterNames.raw())).toPhpDocNode(); if (UNEXPECTED(node.isUndef())) return zv::Val(); - if (UNEXPECTED(!zv::Ref(printer.raw()).isObject())) { - zend_type_error("phpstan_turbo: the printer must be an object"); - return zv::Val(); - } - return pt_type_call(Z_OBJ_P(printer.raw()), PT_LC("print"), 1, node.raw()); + return pt_describe_php_doc_node(node.raw()); } /* $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod() */ @@ -679,8 +673,8 @@ class ClosureType return create(args); } - /* IdentifierTypeNode('Closure' with its static-/pure- prefixes) for a - * common callable; the CallableTypeNode over the parameters, template + /* FullyQualifiedNameNode('Closure') for a common callable, or an + * IdentifierTypeNode when it has a static-/pure- prefix; the CallableTypeNode over the parameters, template * tags and (conditional) return type otherwise; UNDEF = pending * exception */ zv::Val toPhpDocNode() const @@ -690,14 +684,15 @@ class ClosureType if (common == 1) { zv::Val name = prefixedName(); if (UNEXPECTED(name.isUndef())) return zv::Val(); - return pt_type_new(PT_CLASS_IDENTIFIER_TYPE_NODE, 1, name.raw()); + bool isClassName = zv::Ref(name.raw()).stringEquals("Closure"); + return pt_type_new(isClassName ? PT_CLASS_FULLY_QUALIFIED_NAME_NODE : PT_CLASS_IDENTIFIER_TYPE_NODE, 1, name.raw()); } zval *p = parameters(); zval *tags = p != NULL ? templateTags() : NULL; zval *a = tags != NULL ? assertions() : NULL; zval *r = a != NULL ? returnType() : NULL; if (UNEXPECTED(r == NULL)) return zv::Val(); - return pt_callable_type_node(PT_LC("Closure"), p, tags, a, r); + return pt_callable_type_node(PT_LC("Closure"), true, p, tags, a, r); } /* false = pending exception */ diff --git a/turbo-ext/src/EnumCaseObjectType.cpp b/turbo-ext/src/EnumCaseObjectType.cpp index 3e3f4e70f3a..fa18fa0f6b2 100644 --- a/turbo-ext/src/EnumCaseObjectType.cpp +++ b/turbo-ext/src/EnumCaseObjectType.cpp @@ -340,7 +340,7 @@ class EnumCaseObjectType return pt_intersection_of(std::move(types)); } - /* new ConstTypeNode(new ConstFetchNode($this->getClassName(), $this->getEnumCaseName())); + /* new ConstTypeNode(new ConstFetchNode('\\' . $this->getClassName(), $this->getEnumCaseName())); * UNDEF = pending exception */ zv::Val toPhpDocNode() const { @@ -352,7 +352,8 @@ class EnumCaseObjectType return zv::Val::string(name); }); if (UNEXPECTED(caseName.isUndef())) return zv::Val(); - zv::Args args{className.raw(), caseName.raw()}; + zv::Val qualifiedClassName = zv::Val::adoptString(zend_string_concat2("\\", 1, Z_STRVAL_P(className.raw()), Z_STRLEN_P(className.raw()))); + zv::Args args{qualifiedClassName.raw(), caseName.raw()}; zv::Val constFetch = pt_type_new(PT_CLASS_CONST_FETCH_NODE, 2, args); if (UNEXPECTED(constFetch.isUndef())) return zv::Val(); return pt_type_new(PT_CLASS_CONST_TYPE_NODE, 1, constFetch.raw()); diff --git a/turbo-ext/src/GetTemplateTypeType.cpp b/turbo-ext/src/GetTemplateTypeType.cpp index 1bef313a6f9..d2641d50e75 100644 --- a/turbo-ext/src/GetTemplateTypeType.cpp +++ b/turbo-ext/src/GetTemplateTypeType.cpp @@ -157,7 +157,7 @@ class GetTemplateTypeType } /* new GenericTypeNode(new IdentifierTypeNode('template-type'), [$this->type->toPhpDocNode(), - * new IdentifierTypeNode($this->ancestorClassName), new ConstTypeNode(new + * new FullyQualifiedNameNode($this->ancestorClassName), new ConstTypeNode(new * ConstExprStringNode($this->templateTypeName, ConstExprStringNode::SINGLE_QUOTED))]) */ zv::Val toPhpDocNode() const { @@ -169,7 +169,7 @@ class GetTemplateTypeType if (UNEXPECTED(typeNode.isUndef())) return zv::Val(); zval *ancestor = ancestorClassName(); if (UNEXPECTED(ancestor == NULL)) return zv::Val(); - zv::Val ancestorNode = pt_type_new_identifier_type_node(ZSTR_VAL(Z_STR_P(ancestor)), ZSTR_LEN(Z_STR_P(ancestor))); + zv::Val ancestorNode = pt_type_new(PT_CLASS_FULLY_QUALIFIED_NAME_NODE, 1, ancestor); if (UNEXPECTED(ancestorNode.isUndef())) return zv::Val(); zval *name = templateTypeName(); if (UNEXPECTED(name == NULL)) return zv::Val(); diff --git a/turbo-ext/src/ObjectType.cpp b/turbo-ext/src/ObjectType.cpp index 86149a40410..37c204f98ed 100644 --- a/turbo-ext/src/ObjectType.cpp +++ b/turbo-ext/src/ObjectType.cpp @@ -2849,12 +2849,12 @@ class ObjectType return pt_type_new_error_type(); } - /* new IdentifierTypeNode($this->getClassName()); UNDEF = pending exception */ + /* new FullyQualifiedNameNode($this->getClassName()); UNDEF = pending exception */ zv::Val toPhpDocNode() const { zv::Val name = thisGetClassName(); if (UNEXPECTED(name.isUndef())) return zv::Val(); - return pt_type_new(PT_CLASS_IDENTIFIER_TYPE_NODE, 1, name.raw()); + return pt_type_new(PT_CLASS_FULLY_QUALIFIED_NAME_NODE, 1, name.raw()); } /* null for an empty class name, else $position->isClassNameAllowed() diff --git a/turbo-ext/src/TypeTraits.cpp b/turbo-ext/src/TypeTraits.cpp index 4d703507dc3..58b93068057 100644 --- a/turbo-ext/src/TypeTraits.cpp +++ b/turbo-ext/src/TypeTraits.cpp @@ -2883,16 +2883,19 @@ zv::Val pt_callable_dummy_parameters(zval *parameters, zval *assertions) return zv::Val(std::move(dummies)); } +zv::Val pt_describe_php_doc_node(zval *node) +{ + return pt_type_call_static(PT_CLASS_TYPE_NODE_DESCRIBER, PT_LC("describe"), 1, node); +} + zv::Val pt_callable_print_php_doc_node(zval *type) { - zv::Val printer = pt_type_new(PT_CLASS_PHPDOC_PRINTER, 0, NULL); - if (UNEXPECTED(printer.isUndef())) return zv::Val(); zv::Val node = pt_type_call(Z_OBJ_P(type), PT_LC("tophpdocnode"), 0, NULL); if (UNEXPECTED(node.isUndef())) return zv::Val(); - return pt_type_call(Z_OBJ_P(printer.raw()), PT_LC("print"), 1, node.raw()); + return pt_describe_php_doc_node(node.raw()); } -zv::Val pt_callable_type_node(const char *identifier, size_t identifierLen, zval *parameters, zval *templateTags, zval *assertions, zval *returnType) +zv::Val pt_callable_type_node(const char *identifier, size_t identifierLen, bool identifierIsClassName, zval *parameters, zval *templateTags, zval *assertions, zval *returnType) { if (UNEXPECTED(Z_TYPE_P(parameters) != IS_ARRAY || Z_TYPE_P(templateTags) != IS_ARRAY)) { zend_type_error("phpstan_turbo: the parameters and template tags must be arrays"); @@ -2957,7 +2960,8 @@ zv::Val pt_callable_type_node(const char *identifier, size_t identifierLen, zval returnTypeNode = pt_type_call(Z_OBJ_P(returnType), PT_LC("tophpdocnode"), 0, NULL); if (UNEXPECTED(returnTypeNode.isUndef())) return zv::Val(); } - zv::Val identifierNode = pt_type_new_identifier_type_node(identifier, identifierLen); + zv::Val identifierName = zv::Val::string(identifier, identifierLen); + zv::Val identifierNode = pt_type_new(identifierIsClassName ? PT_CLASS_FULLY_QUALIFIED_NAME_NODE : PT_CLASS_IDENTIFIER_TYPE_NODE, 1, identifierName.raw()); if (UNEXPECTED(identifierNode.isUndef())) return zv::Val(); zv::Args args{identifierNode.raw(), parameterNodes.raw(), returnTypeNode.raw(), templateTagNodes.raw()}; return pt_type_new(PT_CLASS_CALLABLE_TYPE_NODE, 4, args); diff --git a/turbo-ext/src/TypeTraits.h b/turbo-ext/src/TypeTraits.h index 44ab6ce21a4..3da29b73dc1 100644 --- a/turbo-ext/src/TypeTraits.h +++ b/turbo-ext/src/TypeTraits.h @@ -652,12 +652,15 @@ zv::Val pt_callable_parameter_types(zval *parameters); zv::Val pt_callable_dummy_parameters(zval *parameters, zval *assertions); /* (new Printer())->print($type->toPhpDocNode()) — the $printer created * first, as the twins do; UNDEF = pending exception */ +/* TypeNodeDescriber::describe($node); UNDEF = pending exception */ +zv::Val pt_describe_php_doc_node(zval *node); zv::Val pt_callable_print_php_doc_node(zval *type); /* the CallableTypeNode of toPhpDocNode(): new CallableTypeNode(new * IdentifierTypeNode($identifier), , toPhpDocNode()>, *