Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/Ast/PhpDoc/TypeAliasTagValueNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function implode;
use function trim;

class TypeAliasTagValueNode implements PhpDocTagValueNode
Expand All @@ -15,23 +16,33 @@ class TypeAliasTagValueNode implements PhpDocTagValueNode

public TypeNode $type;

public function __construct(string $alias, TypeNode $type)
/** @var TemplateTagValueNode[] */
public array $templateTypes;

/**
* @param TemplateTagValueNode[] $templateTypes
*/
public function __construct(string $alias, TypeNode $type, array $templateTypes = [])
{
$this->alias = $alias;
$this->type = $type;
$this->templateTypes = $templateTypes;
}

public function __toString(): string
{
return trim("{$this->alias} {$this->type}");
$templateTypes = $this->templateTypes !== []
? '<' . implode(', ', $this->templateTypes) . '>'
: '';
return trim("{$this->alias}{$templateTypes} {$this->type}");
}

/**
* @param array<string, mixed> $properties
*/
public static function __set_state(array $properties): self
{
$instance = new self($properties['alias'], $properties['type']);
$instance = new self($properties['alias'], $properties['type'], $properties['templateTypes'] ?? []);
if (isset($properties['attributes'])) {
foreach ($properties['attributes'] as $key => $value) {
$instance->setAttribute($key, $value);
Expand Down
37 changes: 36 additions & 1 deletion src/Parser/PhpDocParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,40 @@ private function parseTypeAliasTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeA
$alias = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);

$templateTypes = [];
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
// Skip whitespace and newlines after opening bracket
while ($tokens->isCurrentTokenType(Lexer::TOKEN_HORIZONTAL_WS, Lexer::TOKEN_PHPDOC_EOL)) {
$tokens->next();
}

do {
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
$templateTypes[] = $this->enrichWithAttributes(
$tokens,
$this->typeParser->parseTemplateTagValue($tokens),
$startLine,
$startIndex,
);

// Skip whitespace and newlines after template type
while ($tokens->isCurrentTokenType(Lexer::TOKEN_HORIZONTAL_WS, Lexer::TOKEN_PHPDOC_EOL)) {
$tokens->next();
}

if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
break;
}

// Skip whitespace and newlines after comma
while ($tokens->isCurrentTokenType(Lexer::TOKEN_HORIZONTAL_WS, Lexer::TOKEN_PHPDOC_EOL)) {
$tokens->next();
}
} while (true);
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
}

// support phan-type/psalm-type syntax
$tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL);

Expand All @@ -1087,12 +1121,13 @@ private function parseTypeAliasTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeA
}
}

return new Ast\PhpDoc\TypeAliasTagValueNode($alias, $type);
return new Ast\PhpDoc\TypeAliasTagValueNode($alias, $type, $templateTypes);
} catch (ParserException $e) {
$this->parseOptionalDescription($tokens, false);
return new Ast\PhpDoc\TypeAliasTagValueNode(
$alias,
$this->enrichWithAttributes($tokens, new Ast\Type\InvalidTypeNode($e), $startLine, $startIndex),
$templateTypes,
);
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/Printer/Printer.php
Original file line number Diff line number Diff line change
Expand Up @@ -383,8 +383,11 @@ private function printTagValue(PhpDocTagValueNode $node): string
);
}
if ($node instanceof TypeAliasTagValueNode) {
$templateTypes = $node->templateTypes !== []
? '<' . implode(', ', array_map(fn (TemplateTagValueNode $templateNode): string => $this->print($templateNode), $node->templateTypes)) . '>'
: '';
$type = $this->printType($node->type);
return trim("{$node->alias} {$type}");
return trim("{$node->alias}{$templateTypes} {$type}");
}
if ($node instanceof UsesTagValueNode) {
$type = $this->printType($node->type);
Expand Down
4 changes: 4 additions & 0 deletions tests/PHPStan/Ast/ToString/PhpDocToStringTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,10 @@ public static function provideClassCases(): Generator

yield from [
['Foo array<string>', new TypeAliasTagValueNode('Foo', $arrayOfStrings)],
['Wrapper<T> T', new TypeAliasTagValueNode('Wrapper', new IdentifierTypeNode('T'), [new TemplateTagValueNode('T', null, '')])],
['Pair<TFirst, TSecond> TFirst', new TypeAliasTagValueNode('Pair', new IdentifierTypeNode('TFirst'), [new TemplateTagValueNode('TFirst', null, ''), new TemplateTagValueNode('TSecond', null, '')])],
['Collection<T of object> array<string>', new TypeAliasTagValueNode('Collection', $arrayOfStrings, [new TemplateTagValueNode('T', new IdentifierTypeNode('object'), '')])],
['WithDefault<T = string> T', new TypeAliasTagValueNode('WithDefault', new IdentifierTypeNode('T'), [new TemplateTagValueNode('T', null, '', new IdentifierTypeNode('string'))])],
['Test from Foo\Bar', new TypeAliasImportTagValueNode('Test', $bar, null)],
['Test from Foo\Bar as Foo', new TypeAliasImportTagValueNode('Test', $bar, 'Foo')],
];
Expand Down
124 changes: 124 additions & 0 deletions tests/PHPStan/Parser/PhpDocParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5152,6 +5152,130 @@ public function provideTypeAliasTagsData(): Iterator
),
]),
];

yield [
'OK with one template type',
'/** @phpstan-type Wrapper<T> T */',
new PhpDocNode([
new PhpDocTagNode(
'@phpstan-type',
new TypeAliasTagValueNode(
'Wrapper',
new IdentifierTypeNode('T'),
[
new TemplateTagValueNode('T', null, ''),
],
),
),
]),
];

yield [
'OK with two template types',
'/** @phpstan-type Pair<TFirst, TSecond> TFirst */',
new PhpDocNode([
new PhpDocTagNode(
'@phpstan-type',
new TypeAliasTagValueNode(
'Pair',
new IdentifierTypeNode('TFirst'),
[
new TemplateTagValueNode('TFirst', null, ''),
new TemplateTagValueNode('TSecond', null, ''),
],
),
),
]),
];

yield [
'OK with bounded template type',
'/** @phpstan-type Collection<T of object> list<T> */',
new PhpDocNode([
new PhpDocTagNode(
'@phpstan-type',
new TypeAliasTagValueNode(
'Collection',
new GenericTypeNode(
new IdentifierTypeNode('list'),
[new IdentifierTypeNode('T')],
[GenericTypeNode::VARIANCE_INVARIANT],
),
[
new TemplateTagValueNode('T', new IdentifierTypeNode('object'), ''),
],
),
),
]),
];

yield [
'OK with default template type',
'/** @phpstan-type WithDefault<T = string> T */',
new PhpDocNode([
new PhpDocTagNode(
'@phpstan-type',
new TypeAliasTagValueNode(
'WithDefault',
new IdentifierTypeNode('T'),
[
new TemplateTagValueNode('T', null, '', new IdentifierTypeNode('string')),
],
),
),
]),
];

yield [
'OK multiline with two template types',
'/**
* @phpstan-type Widget<
* TFoo,
* TBar
* > array{foo: TFoo, bar: TBar}
*/',
new PhpDocNode([
new PhpDocTagNode(
'@phpstan-type',
new TypeAliasTagValueNode(
'Widget',
ArrayShapeNode::createSealed([
new ArrayShapeItemNode(new IdentifierTypeNode('foo'), false, new IdentifierTypeNode('TFoo')),
new ArrayShapeItemNode(new IdentifierTypeNode('bar'), false, new IdentifierTypeNode('TBar')),
]),
[
new TemplateTagValueNode('TFoo', null, ''),
new TemplateTagValueNode('TBar', null, ''),
],
),
),
]),
];

yield [
'OK multiline with bounded template',
'/**
* @phpstan-type Collection<
* T of object
* > list<T>
*/',
new PhpDocNode([
new PhpDocTagNode(
'@phpstan-type',
new TypeAliasTagValueNode(
'Collection',
new GenericTypeNode(
new IdentifierTypeNode('list'),
[new IdentifierTypeNode('T')],
[GenericTypeNode::VARIANCE_INVARIANT],
),
[
new TemplateTagValueNode('T', new IdentifierTypeNode('object'), ''),
],
),
),
]),
];
}

public function provideTypeAliasImportTagsData(): Iterator
Expand Down
Loading