diff --git a/src/Ast/PhpDoc/TypeAliasImportTagValueNode.php b/src/Ast/PhpDoc/TypeAliasImportTagValueNode.php index 147513d3..d1c3c9f7 100644 --- a/src/Ast/PhpDoc/TypeAliasImportTagValueNode.php +++ b/src/Ast/PhpDoc/TypeAliasImportTagValueNode.php @@ -4,6 +4,8 @@ use PHPStan\PhpDocParser\Ast\NodeAttributes; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\NameNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use function trim; class TypeAliasImportTagValueNode implements PhpDocTagValueNode @@ -13,11 +15,15 @@ class TypeAliasImportTagValueNode implements PhpDocTagValueNode public string $importedAlias; - public IdentifierTypeNode $importedFrom; + /** @var IdentifierTypeNode|NameNode */ + public TypeNode $importedFrom; public ?string $importedAs = null; - public function __construct(string $importedAlias, IdentifierTypeNode $importedFrom, ?string $importedAs) + /** + * @param IdentifierTypeNode|NameNode $importedFrom + */ + public function __construct(string $importedAlias, TypeNode $importedFrom, ?string $importedAs) { $this->importedAlias = $importedAlias; $this->importedFrom = $importedFrom; diff --git a/src/Ast/Type/CallableTypeNode.php b/src/Ast/Type/CallableTypeNode.php index ea34a55a..0aa232bb 100644 --- a/src/Ast/Type/CallableTypeNode.php +++ b/src/Ast/Type/CallableTypeNode.php @@ -11,7 +11,8 @@ class CallableTypeNode implements TypeNode use NodeAttributes; - public IdentifierTypeNode $identifier; + /** @var IdentifierTypeNode|NameNode */ + public TypeNode $identifier; /** @var TemplateTagValueNode[] */ public array $templateTypes; @@ -22,10 +23,11 @@ class CallableTypeNode implements TypeNode public TypeNode $returnType; /** + * @param IdentifierTypeNode|NameNode $identifier * @param CallableTypeParameterNode[] $parameters * @param TemplateTagValueNode[] $templateTypes */ - public function __construct(IdentifierTypeNode $identifier, array $parameters, TypeNode $returnType, array $templateTypes) + public function __construct(TypeNode $identifier, array $parameters, TypeNode $returnType, array $templateTypes) { $this->identifier = $identifier; $this->parameters = $parameters; diff --git a/src/Ast/Type/FullyQualifiedNameNode.php b/src/Ast/Type/FullyQualifiedNameNode.php new file mode 100644 index 00000000..c8780163 --- /dev/null +++ b/src/Ast/Type/FullyQualifiedNameNode.php @@ -0,0 +1,34 @@ +name = ltrim($name, '\\'); + } + + public function toCodeString(): string + { + return '\\' . $this->name; + } + + public function isFullyQualified(): bool + { + return true; + } + +} diff --git a/src/Ast/Type/GenericTypeNode.php b/src/Ast/Type/GenericTypeNode.php index 0069aa4d..dd6e75ae 100644 --- a/src/Ast/Type/GenericTypeNode.php +++ b/src/Ast/Type/GenericTypeNode.php @@ -16,7 +16,8 @@ class GenericTypeNode implements TypeNode use NodeAttributes; - public IdentifierTypeNode $type; + /** @var IdentifierTypeNode|NameNode */ + public TypeNode $type; /** @var TypeNode[] */ public array $genericTypes; @@ -25,10 +26,11 @@ class GenericTypeNode implements TypeNode public array $variances; /** + * @param IdentifierTypeNode|NameNode $type * @param TypeNode[] $genericTypes * @param (self::VARIANCE_*)[] $variances */ - public function __construct(IdentifierTypeNode $type, array $genericTypes, array $variances = []) + public function __construct(TypeNode $type, array $genericTypes, array $variances = []) { $this->type = $type; $this->genericTypes = $genericTypes; diff --git a/src/Ast/Type/NameNode.php b/src/Ast/Type/NameNode.php new file mode 100644 index 00000000..ca5f285c --- /dev/null +++ b/src/Ast/Type/NameNode.php @@ -0,0 +1,37 @@ + + */ + public function getParts(): array; + + public function getFirst(): string; + + public function getLast(): string; + + public function isFullyQualified(): bool; + +} diff --git a/src/Ast/Type/NameNodeTrait.php b/src/Ast/Type/NameNodeTrait.php new file mode 100644 index 00000000..636fa44e --- /dev/null +++ b/src/Ast/Type/NameNodeTrait.php @@ -0,0 +1,61 @@ +name; + } + + public function getParts(): array + { + return explode('\\', $this->name); + } + + public function getFirst(): string + { + return $this->getParts()[0]; + } + + public function getLast(): string + { + $parts = $this->getParts(); + + return $parts[count($parts) - 1]; + } + + public function __toString(): string + { + return $this->toCodeString(); + } + + /** + * @param array $properties + */ + public static function __set_state(array $properties): self + { + $instance = new self($properties['name']); + if (isset($properties['attributes'])) { + foreach ($properties['attributes'] as $key => $value) { + $instance->setAttribute($key, $value); + } + } + return $instance; + } + +} diff --git a/src/Ast/Type/RelativeNameNode.php b/src/Ast/Type/RelativeNameNode.php new file mode 100644 index 00000000..e776877b --- /dev/null +++ b/src/Ast/Type/RelativeNameNode.php @@ -0,0 +1,37 @@ +name = $name; + } + + public function toCodeString(): string + { + return $this->name; + } + + public function isFullyQualified(): bool + { + return false; + } + +} diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index a85a769c..83e4a9a0 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -58,6 +58,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\ObjectShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode; @@ -457,7 +458,7 @@ private function printType(TypeNode $node): string ? '<' . implode(', ', array_map(fn (TemplateTagValueNode $templateNode): string => $this->print($templateNode), $node->templateTypes)) . '>' : ''; $parameters = implode(', ', array_map(fn (CallableTypeParameterNode $parameterNode): string => $this->print($parameterNode), $node->parameters)); - return "{$node->identifier}{$template}({$parameters}): {$returnType}"; + return $this->printType($node->identifier) . "{$template}({$parameters}): {$returnType}"; } if ($node instanceof ConditionalTypeForParameterNode) { return sprintf( @@ -496,11 +497,14 @@ private function printType(TypeNode $node): string } } - return $node->type . '<' . implode(', ', $genericTypes) . '>'; + return $this->printType($node->type) . '<' . implode(', ', $genericTypes) . '>'; } if ($node instanceof IdentifierTypeNode) { return $node->name; } + if ($node instanceof NameNode) { + return $node->toCodeString(); + } if ($node instanceof IntersectionTypeNode || $node instanceof UnionTypeNode) { $items = []; foreach ($node->types as $type) { diff --git a/tests/PHPStan/Ast/Type/NameNodeTest.php b/tests/PHPStan/Ast/Type/NameNodeTest.php new file mode 100644 index 00000000..361232da --- /dev/null +++ b/tests/PHPStan/Ast/Type/NameNodeTest.php @@ -0,0 +1,58 @@ +assertSame('Foo\Bar\Baz', $node->name); + $this->assertSame('Foo\Bar\Baz', $node->toString()); + $this->assertSame('\Foo\Bar\Baz', $node->toCodeString()); + $this->assertSame('\Foo\Bar\Baz', (string) $node); + $this->assertSame(['Foo', 'Bar', 'Baz'], $node->getParts()); + $this->assertSame('Foo', $node->getFirst()); + $this->assertSame('Baz', $node->getLast()); + $this->assertTrue($node->isFullyQualified()); + + $this->assertSame('Foo\Bar\Baz', (new FullyQualifiedNameNode('\Foo\Bar\Baz'))->name); + } + + public function testRelative(): void + { + $node = new RelativeNameNode('Bar\Baz'); + + $this->assertSame('Bar\Baz', $node->name); + $this->assertSame('Bar\Baz', $node->toString()); + $this->assertSame('Bar\Baz', $node->toCodeString()); + $this->assertSame('Bar\Baz', (string) $node); + $this->assertSame(['Bar', 'Baz'], $node->getParts()); + $this->assertSame('Bar', $node->getFirst()); + $this->assertSame('Baz', $node->getLast()); + $this->assertFalse($node->isFullyQualified()); + } + + public function testRelativeRejectsFullyQualifiedName(): void + { + $this->expectException(InvalidArgumentException::class); + new RelativeNameNode('\Foo'); + } + + public function testSetState(): void + { + $node = new FullyQualifiedNameNode('Foo\Bar'); + $node->setAttribute('a', 1); + $this->assertEquals($node, eval('return ' . var_export($node, true) . ';')); + + $relative = new RelativeNameNode('Bar'); + $this->assertEquals($relative, eval('return ' . var_export($relative, true) . ';')); + } + +} diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 4288e1bc..98682fcb 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -41,6 +41,7 @@ use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode; 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; @@ -48,6 +49,7 @@ use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode; use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode; +use PHPStan\PhpDocParser\Ast\Type\RelativeNameNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; use PHPStan\PhpDocParser\Lexer\Lexer; @@ -3234,6 +3236,86 @@ public function testPrintType(TypeNode $node, string $expectedResult): void ); } + /** + * @return iterable + */ + public function dataPrintNameNodes(): iterable + { + yield [new FullyQualifiedNameNode('Foo\\Bar'), '\\Foo\\Bar']; + yield [new RelativeNameNode('Foo\\Bar'), 'Foo\\Bar']; + yield [ + new GenericTypeNode(new FullyQualifiedNameNode('Foo'), [new RelativeNameNode('Bar'), new IdentifierTypeNode('int')]), + '\\Foo', + ]; + yield [ + new CallableTypeNode(new FullyQualifiedNameNode('Closure'), [new CallableTypeParameterNode(new FullyQualifiedNameNode('Foo'), false, false, '$a', false)], new RelativeNameNode('Bar'), []), + '\\Closure(\\Foo $a): Bar', + ]; + yield [ + new UnionTypeNode([new FullyQualifiedNameNode('Foo'), new IdentifierTypeNode('null')]), + '\\Foo|null', + '(\\Foo | null)', + ]; + } + + /** + * @dataProvider dataPrintNameNodes + */ + public function testPrintNameNodes(TypeNode $node, string $expectedResult, ?string $expectedToString = null): void + { + $this->assertSame($expectedResult, (new Printer())->print($node)); + $this->assertSame($expectedToString ?? $expectedResult, (string) $node); + } + + public function testPrintTypeAliasImportWithNameNode(): void + { + $node = new TypeAliasImportTagValueNode('Foo', new FullyQualifiedNameNode('Some\\Bar'), 'Baz'); + $this->assertSame('Foo from \\Some\\Bar as Baz', (new Printer())->print($node)); + $this->assertSame('Foo from \\Some\\Bar as Baz', (string) $node); + } + + public function testPrintFormatPreservingNameNodes(): void + { + $visitor = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof IdentifierTypeNode && $node->name === 'Foo') { + return new FullyQualifiedNameNode('Some\\Foo'); + } + if ($node instanceof IdentifierTypeNode && $node->name === 'Some\\Bar') { + return new RelativeNameNode('Bar'); + } + + return null; + } + + }; + + $phpDoc = <<<'PHPDOC' +/** + * @param Foo $a description + * @return array + */ +PHPDOC; + $expected = <<<'PHPDOC' +/** + * @param \Some\Foo $a description + * @return array<\Some\Foo|null> + */ +PHPDOC; + + $config = new ParserConfig(['lines' => true, 'indexes' => true, 'comments' => true]); + $tokens = new TokenIterator((new Lexer($config))->tokenize($phpDoc)); + $phpDocNode = $this->phpDocParser->parse($tokens); + $newNodes = (new NodeTraverser([new NodeVisitor\CloningVisitor()]))->traverse([$phpDocNode]); + + /** @var PhpDocNode $newNode */ + [$newNode] = (new NodeTraverser([$visitor]))->traverse($newNodes); + + $this->assertSame($expected, (new Printer())->printFormatPreserving($newNode, $phpDocNode, $tokens)); + } + /** * @return iterable */