Skip to content

Commit 2bf0ea5

Browse files
committed
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
1 parent 203cc68 commit 2bf0ea5

5 files changed

Lines changed: 68 additions & 10 deletions

File tree

‎src/PhpDoc/PhpDocNodeResolver.php‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ public function resolveExtendsTags(PhpDocNode $phpDocNode, NameScope $nameScope)
236236

237237
foreach (['@extends', '@phan-extends', '@phan-inherits', '@template-extends', '@phpstan-extends'] as $tagName) {
238238
foreach ($phpDocNode->getExtendsTagValues($tagName) as $tagValue) {
239-
$resolved[$nameScope->resolveStringName($tagValue->type->type->name)] = new ExtendsTag(
239+
$resolved[$nameScope->resolveStringName((string) $tagValue->type->type)] = new ExtendsTag(
240240
$this->typeNodeResolver->resolve($tagValue->type, $nameScope),
241241
);
242242
}
@@ -254,7 +254,7 @@ public function resolveImplementsTags(PhpDocNode $phpDocNode, NameScope $nameSco
254254

255255
foreach (['@implements', '@template-implements', '@phpstan-implements'] as $tagName) {
256256
foreach ($phpDocNode->getImplementsTagValues($tagName) as $tagValue) {
257-
$resolved[$nameScope->resolveStringName($tagValue->type->type->name)] = new ImplementsTag(
257+
$resolved[$nameScope->resolveStringName((string) $tagValue->type->type)] = new ImplementsTag(
258258
$this->typeNodeResolver->resolve($tagValue->type, $nameScope),
259259
);
260260
}
@@ -272,7 +272,7 @@ public function resolveUsesTags(PhpDocNode $phpDocNode, NameScope $nameScope): a
272272

273273
foreach (['@use', '@template-use', '@phpstan-use'] as $tagName) {
274274
foreach ($phpDocNode->getUsesTagValues($tagName) as $tagValue) {
275-
$resolved[$nameScope->resolveStringName($tagValue->type->type->name)] = new UsesTag(
275+
$resolved[$nameScope->resolveStringName((string) $tagValue->type->type)] = new UsesTag(
276276
$this->typeNodeResolver->resolve($tagValue->type, $nameScope),
277277
);
278278
}
@@ -571,7 +571,7 @@ public function resolveTypeAliasImportTags(PhpDocNode $phpDocNode, NameScope $na
571571
foreach (['@psalm-import-type', '@phpstan-import-type'] as $tagName) {
572572
foreach ($phpDocNode->getTypeAliasImportTagValues($tagName) as $typeAliasImportTagValue) {
573573
$importedAlias = $typeAliasImportTagValue->importedAlias;
574-
$importedFrom = $nameScope->resolveStringName($typeAliasImportTagValue->importedFrom->name);
574+
$importedFrom = $nameScope->resolveStringName((string) $typeAliasImportTagValue->importedFrom);
575575
$importedAs = $typeAliasImportTagValue->importedAs;
576576
$resolved[$importedAs ?? $importedAlias] = new TypeAliasImportTag($importedAlias, $importedFrom, $importedAs);
577577
}

‎src/PhpDoc/TypeNodeResolver.php‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
3535
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
3636
use PHPStan\PhpDocParser\Ast\Type\InvalidTypeNode;
37+
use PHPStan\PhpDocParser\Ast\Type\NameNode;
3738
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
3839
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode;
3940
use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode;
@@ -170,7 +171,10 @@ public function resolve(TypeNode $typeNode, NameScope $nameScope): Type
170171
}
171172
}
172173

173-
if ($typeNode instanceof IdentifierTypeNode) {
174+
if ($typeNode instanceof NameNode) {
175+
return $this->resolveNameNode($typeNode, $nameScope);
176+
177+
} elseif ($typeNode instanceof IdentifierTypeNode) {
174178
return $this->resolveIdentifierTypeNode($typeNode, $nameScope);
175179

176180
} elseif ($typeNode instanceof ThisTypeNode) {
@@ -215,6 +219,14 @@ public function resolve(TypeNode $typeNode, NameScope $nameScope): Type
215219
return new ErrorType();
216220
}
217221

222+
/**
223+
* A name node is always a class, even when it is named like a keyword or a template type.
224+
*/
225+
private function resolveNameNode(NameNode $typeNode, NameScope $nameScope): Type
226+
{
227+
return new ObjectType($nameScope->resolveStringName($typeNode->toCodeString()));
228+
}
229+
218230
private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameScope $nameScope): Type
219231
{
220232
switch (strtolower($typeNode->name)) {
@@ -692,7 +704,7 @@ private function resolveArrayTypeNode(ArrayTypeNode $typeNode, NameScope $nameSc
692704

693705
private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $nameScope): Type
694706
{
695-
$mainTypeName = strtolower($typeNode->type->name);
707+
$mainTypeName = $typeNode->type instanceof NameNode ? null : strtolower($typeNode->type->name);
696708
$genericTypes = $this->resolveMultiple($typeNode->genericTypes, $nameScope);
697709
$variances = array_map(
698710
static function (string $variance): TemplateTypeVariance {
@@ -861,7 +873,9 @@ static function (string $variance): TemplateTypeVariance {
861873
return new ErrorType();
862874
}
863875

864-
$mainType = $this->resolveIdentifierTypeNode($typeNode->type, $nameScope);
876+
$mainType = $typeNode->type instanceof NameNode
877+
? $this->resolveNameNode($typeNode->type, $nameScope)
878+
: $this->resolveIdentifierTypeNode($typeNode->type, $nameScope);
865879
$mainTypeObjectClassNames = $mainType->getObjectClassNames();
866880
if (count($mainTypeObjectClassNames) > 1) {
867881
if ($mainType instanceof TemplateType) {

‎src/Type/IntersectionType.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2056,7 +2056,7 @@ public function toPhpDocNode(): TypeNode
20562056

20572057
foreach ($baseTypes as $i => $type) {
20582058
$typeNode = $type->toPhpDocNode();
2059-
if ($typeNode instanceof GenericTypeNode && $typeNode->type->name === 'array') {
2059+
if ($typeNode instanceof GenericTypeNode && $typeNode->type instanceof IdentifierTypeNode && $typeNode->type->name === 'array') {
20602060
$nonEmpty = false;
20612061
$typeName = 'array';
20622062
foreach ($typesToDescribe as $j => $typeToDescribe) {

‎tests/PHPStan/Type/TypeToPhpDocNodeTest.php‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
namespace PHPStan\Type;
44

55
use PHPStan\Analyser\NameScope;
6+
use PHPStan\PhpDoc\TypeNodeResolver;
67
use PHPStan\PhpDoc\TypeStringResolver;
8+
use PHPStan\PhpDocParser\Ast\Type\FullyQualifiedNameNode;
9+
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
10+
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
11+
use PHPStan\PhpDocParser\Ast\Type\RelativeNameNode;
12+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
713
use PHPStan\Testing\PHPStanTestCase;
814
use PHPStan\TrinaryLogic;
915
use PHPStan\Type\Accessory\AccessoryArrayListType;
@@ -650,6 +656,41 @@ public function testLateResolvableArrayShapeToPhpDocNode(string $typeString): vo
650656
$this->assertTrue($type->equals($typeAgain));
651657
}
652658

659+
/**
660+
* @return iterable<array{TypeNode, string}>
661+
*/
662+
public static function dataResolveNameNodes(): iterable
663+
{
664+
// a NameNode is always a class, even when it is named like a keyword or a template type
665+
yield [new RelativeNameNode('Int'), 'App\\Int'];
666+
yield [new RelativeNameNode('T'), 'App\\T'];
667+
yield [new RelativeNameNode('Bar'), 'Vendor\\Bar'];
668+
yield [new FullyQualifiedNameNode('Int'), 'Int'];
669+
yield [new GenericTypeNode(new RelativeNameNode('List'), [new IdentifierTypeNode('int')]), 'App\\List<int>'];
670+
yield [new GenericTypeNode(new FullyQualifiedNameNode('Bar'), [new RelativeNameNode('T')]), 'Bar<App\\T>'];
671+
672+
// plain identifiers keep resolving keywords and template types first
673+
yield [new IdentifierTypeNode('Int'), 'int'];
674+
yield [new IdentifierTypeNode('T'), 'T (function doFoo(), parameter)'];
675+
yield [new GenericTypeNode(new IdentifierTypeNode('List'), [new IdentifierTypeNode('T')]), 'list<T (function doFoo(), parameter)>'];
676+
}
677+
678+
#[DataProvider('dataResolveNameNodes')]
679+
public function testResolveNameNodes(TypeNode $typeNode, string $expected): void
680+
{
681+
$nameScope = new NameScope('App', ['bar' => 'Vendor\\Bar'], null, 'doFoo', new TemplateTypeMap([
682+
'T' => TemplateTypeFactory::create(
683+
TemplateTypeScope::createWithFunction('doFoo'),
684+
'T',
685+
null,
686+
TemplateTypeVariance::createInvariant(),
687+
),
688+
]));
689+
690+
$type = self::getContainer()->getByType(TypeNodeResolver::class)->resolve($typeNode, $nameScope);
691+
$this->assertSame($expected, $type->describe(VerbosityLevel::precise()));
692+
}
693+
653694
public static function getAdditionalConfigFiles(): array
654695
{
655696
return [

‎turbo-ext/src/IntersectionType.cpp‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2433,10 +2433,13 @@ class IntersectionType
24332433
zv::Val identifier = readProperty(typeNode.raw(), PT_LC("type"));
24342434
if (UNEXPECTED(identifier.isUndef())) return zv::Val();
24352435
if (UNEXPECTED(!zv::Ref(identifier.raw()).isObject())) {
2436-
zend_type_error("phpstan_turbo: GenericTypeNode::$type must be an IdentifierTypeNode");
2436+
zend_type_error("phpstan_turbo: GenericTypeNode::$type must be an IdentifierTypeNode or a NameNode");
24372437
return zv::Val();
24382438
}
2439-
zv::Val identifierName = readProperty(identifier.raw(), PT_LC("name"));
2439+
/* a NameNode is a class, never the array keyword */
2440+
bool isIdentifier;
2441+
if (UNEXPECTED(!isInstance(identifier.raw(), PT_CLASS_IDENTIFIER_TYPE_NODE, isIdentifier))) return zv::Val();
2442+
zv::Val identifierName = isIdentifier ? readProperty(identifier.raw(), PT_LC("name")) : zv::Val::null();
24402443
if (UNEXPECTED(identifierName.isUndef())) return zv::Val();
24412444
if (Z_TYPE_P(identifierName.raw()) == IS_STRING && zend_string_equals_literal(Z_STR_P(identifierName.raw()), "array")) {
24422445
bool nonEmpty = false;

0 commit comments

Comments
 (0)