diff --git a/src/Parser/TokenIterator.php b/src/Parser/TokenIterator.php index 87ded21..5285d48 100644 --- a/src/Parser/TokenIterator.php +++ b/src/Parser/TokenIterator.php @@ -205,6 +205,15 @@ public function tryConsumeTokenType(int $tokenType): bool } + /** @phpstan-impure */ + public function skipNewLineTokens(): void + { + do { + $foundNewLine = $this->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + } while ($foundNewLine === true); + } + + private function detectNewline(): void { $value = $this->currentTokenValue(); diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 84a3880..1b2080e 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -90,7 +90,7 @@ private function subParse(TokenIterator $tokens): Ast\Type\TypeNode if ($tokens->isCurrentTokenValue('is')) { $type = $this->parseConditional($tokens, $type); } else { - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) { $type = $this->subParseUnion($tokens, $type); @@ -112,9 +112,9 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode $startIndex = $tokens->currentTokenIndex(); if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) { - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $type = $this->subParse($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES); @@ -256,9 +256,9 @@ private function subParseUnion(TokenIterator $tokens, Ast\Type\TypeNode $type): $types = [$type]; while ($tokens->tryConsumeTokenType(Lexer::TOKEN_UNION)) { - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $types[] = $this->parseAtomic($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); } return new Ast\Type\UnionTypeNode($types); @@ -284,9 +284,9 @@ private function subParseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $ $types = [$type]; while ($tokens->tryConsumeTokenType(Lexer::TOKEN_INTERSECTION)) { - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $types[] = $this->parseAtomic($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); } return new Ast\Type\IntersectionTypeNode($types); @@ -306,15 +306,15 @@ private function parseConditional(TokenIterator $tokens, Ast\Type\TypeNode $subj $targetType = $this->parse($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $tokens->consumeTokenType(Lexer::TOKEN_NULLABLE); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $ifType = $this->parse($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $tokens->consumeTokenType(Lexer::TOKEN_COLON); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $elseType = $this->subParse($tokens); @@ -335,15 +335,15 @@ private function parseConditionalForParameter(TokenIterator $tokens, string $par $targetType = $this->parse($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $tokens->consumeTokenType(Lexer::TOKEN_NULLABLE); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $ifType = $this->parse($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $tokens->consumeTokenType(Lexer::TOKEN_COLON); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $elseType = $this->subParse($tokens); @@ -409,8 +409,13 @@ public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $variances = []; $isFirst = true; - while ($isFirst || $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + while ( + $isFirst + || $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA) + || $tokens->tryConsumeTokenType(Lexer::TOKEN_UNION) + || $tokens->tryConsumeTokenType(Lexer::TOKEN_INTERSECTION) + ) { + $tokens->skipNewLineTokens(); // trailing comma case if (!$isFirst && $tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) { @@ -419,7 +424,7 @@ public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $isFirst = false; [$genericTypes[], $variances[]] = $this->parseGenericTypeArgument($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); } $type = new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances); @@ -510,19 +515,19 @@ private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNod : []; $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $parameters = []; if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) { $parameters[] = $this->parseCallableParameter($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) { break; } $parameters[] = $this->parseCallableParameter($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); } } @@ -550,7 +555,7 @@ private function parseCallableTemplates(TokenIterator $tokens): array $isFirst = true; while ($isFirst || $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); // trailing comma case if (!$isFirst && $tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) { @@ -559,7 +564,7 @@ private function parseCallableTemplates(TokenIterator $tokens): array $isFirst = false; $templates[] = $this->parseCallableTemplateArgument($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); } $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); @@ -830,7 +835,7 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, $unsealedType = null; do { - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) { return Ast\Type\ArrayShapeNode::createSealed($items, $kind); @@ -839,14 +844,14 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, if ($tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC)) { $sealed = false; - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { if ($kind === Ast\Type\ArrayShapeNode::KIND_ARRAY) { $unsealedType = $this->parseArrayShapeUnsealedType($tokens); } else { $unsealedType = $this->parseListShapeUnsealedType($tokens); } - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); } $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA); @@ -855,10 +860,10 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, $items[] = $this->parseArrayShapeItem($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET); if ($sealed) { @@ -945,18 +950,18 @@ private function parseArrayShapeUnsealedType(TokenIterator $tokens): Ast\Type\Ar $startIndex = $tokens->currentTokenIndex(); $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $valueType = $this->parse($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $keyType = null; if ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $keyType = $valueType; $valueType = $this->parse($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); } $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); @@ -978,10 +983,10 @@ private function parseListShapeUnsealedType(TokenIterator $tokens): Ast\Type\Arr $startIndex = $tokens->currentTokenIndex(); $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $valueType = $this->parse($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); @@ -1003,7 +1008,7 @@ private function parseObjectShape(TokenIterator $tokens): Ast\Type\ObjectShapeNo $items = []; do { - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) { return new Ast\Type\ObjectShapeNode($items); @@ -1011,10 +1016,10 @@ private function parseObjectShape(TokenIterator $tokens): Ast\Type\ObjectShapeNo $items[] = $this->parseObjectShapeItem($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET); return new Ast\Type\ObjectShapeNode($items); diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 8e98d63..f859e9f 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -62,6 +62,8 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\InvalidTypeNode; use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode; +use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode; +use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode; use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; use PHPStan\PhpDocParser\Lexer\Lexer; @@ -4027,6 +4029,225 @@ public function provideMultiLinePhpDocData(): iterable new PhpDocTextNode(''), ]), ]; + + yield [ + 'Multiline PHPDoc with new line across generic type declaration', + '/**' . PHP_EOL . + ' * @param array,' . PHP_EOL . + ' * }> $a' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTagNode('@param', new ParamTagValueNode( + new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('string'), + ArrayShapeNode::createSealed([ + new ArrayShapeItemNode(new IdentifierTypeNode('foo'), false, new IdentifierTypeNode('int')), + new ArrayShapeItemNode(new IdentifierTypeNode('bar'), true, new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('string'), + ArrayShapeNode::createSealed([ + new ArrayShapeItemNode(new IdentifierTypeNode('foo1'), false, new IdentifierTypeNode('int')), + new ArrayShapeItemNode(new IdentifierTypeNode('bar1'), true, new IdentifierTypeNode('true')), + ]), + ArrayShapeNode::createSealed([ + new ArrayShapeItemNode(new IdentifierTypeNode('foo1'), false, new IdentifierTypeNode('int')), + new ArrayShapeItemNode(new IdentifierTypeNode('foo2'), false, new IdentifierTypeNode('true')), + new ArrayShapeItemNode(new IdentifierTypeNode('bar1'), true, new IdentifierTypeNode('true')), + ]), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + ], + )), + ]), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + ], + ), + false, + '$a', + '', + false, + )), + ]), + ]; + + yield [ + 'Multiline PHPDoc with new line within type declaration', + '/**' . PHP_EOL . + ' * @param array,' . PHP_EOL . + ' * }> $a' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTagNode('@param', new ParamTagValueNode( + new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('string'), + ArrayShapeNode::createSealed([ + new ArrayShapeItemNode(new IdentifierTypeNode('foo'), false, new IdentifierTypeNode('int')), + new ArrayShapeItemNode(new IdentifierTypeNode('bar'), true, new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('string'), + ArrayShapeNode::createSealed([ + new ArrayShapeItemNode(new IdentifierTypeNode('foo1'), false, new IdentifierTypeNode('int')), + new ArrayShapeItemNode(new IdentifierTypeNode('bar1'), true, new IdentifierTypeNode('true')), + ]), + ArrayShapeNode::createSealed([ + new ArrayShapeItemNode(new IdentifierTypeNode('foo1'), false, new IdentifierTypeNode('int')), + new ArrayShapeItemNode(new IdentifierTypeNode('foo2'), false, new IdentifierTypeNode('true')), + new ArrayShapeItemNode(new IdentifierTypeNode('bar1'), true, new IdentifierTypeNode('true')), + ]), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + ], + )), + ]), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + ], + ), + false, + '$a', + '', + false, + )), + ]), + ]; + + yield [ + 'Multiline PHPDoc with new line within type declaration including usage of braces', + '/**' . PHP_EOL . + ' * @phpstan-type FactoriesConfigurationType = array<' . PHP_EOL . + ' * string,' . PHP_EOL . + ' * (class-string|Factory\FactoryInterface)' . PHP_EOL . + ' * |callable(ContainerInterface,?string,array|null):object' . PHP_EOL . + ' * >' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTagNode('@phpstan-type', new TypeAliasTagValueNode( + 'FactoriesConfigurationType', + new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('string'), + new UnionTypeNode([ + new GenericTypeNode( + new IdentifierTypeNode('class-string'), + [new IdentifierTypeNode('Factory\\FactoryInterface')], + [GenericTypeNode::VARIANCE_INVARIANT], + ), + new IdentifierTypeNode('Factory\\FactoryInterface'), + ]), + new CallableTypeNode( + new IdentifierTypeNode('callable'), + [ + new CallableTypeParameterNode(new IdentifierTypeNode('ContainerInterface'), false, false, '', false), + new CallableTypeParameterNode( + new NullableTypeNode( + new IdentifierTypeNode('string'), + ), + false, + false, + '', + false, + ), + new CallableTypeParameterNode( + new UnionTypeNode([ + new GenericTypeNode( + new IdentifierTypeNode('array'), + [new IdentifierTypeNode('mixed')], + [GenericTypeNode::VARIANCE_INVARIANT], + ), + new IdentifierTypeNode('null'), + ]), + false, + false, + '', + false, + ), + ], + new IdentifierTypeNode('object'), + [], + ), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + ], + ), + )), + ]), + ]; + + /** + * @return object{ + * a: int, + * + * b: int, + * } + */ + + yield [ + 'Multiline PHPDoc with new line within object type declaration', + '/**' . PHP_EOL . + ' * @return object{' . PHP_EOL . + ' * a: int,' . PHP_EOL . + ' *' . PHP_EOL . + ' * b: int,' . PHP_EOL . + ' * }' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTagNode( + '@return', + new ReturnTagValueNode( + new ObjectShapeNode( + [ + new ObjectShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int'), + ), + new ObjectShapeItemNode( + new IdentifierTypeNode('b'), + false, + new IdentifierTypeNode('int'), + ), + ], + ), + '', + ), + ), + ]), + ]; } public function provideTemplateTagsData(): Iterator