Returns a collection object representing the new collection.
*/', + new PhpDocNode([ + new PhpDocTagNode( + '@return', + new ReturnTagValueNode( + new IdentifierTypeNode('MongoCollection'), + 'Returns a collection object representing the new collection.
' + ) + ), + ]), + ]; + yield [ 'invalid without type and description', '/** @return */', @@ -1893,6 +2028,138 @@ public function provideMixinTagsData(): Iterator ]; } + public function provideRequireExtendsTagsData(): Iterator + { + yield [ + 'OK without description', + '/** @phpstan-require-extends Foo */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-require-extends', + new RequireExtendsTagValueNode( + new IdentifierTypeNode('Foo'), + '' + ) + ), + ]), + ]; + + yield [ + 'OK with description', + '/** @phpstan-require-extends Foo optional description */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-require-extends', + new RequireExtendsTagValueNode( + new IdentifierTypeNode('Foo'), + 'optional description' + ) + ), + ]), + ]; + + yield [ + 'OK with psalm-prefix description', + '/** @psalm-require-extends Foo optional description */', + new PhpDocNode([ + new PhpDocTagNode( + '@psalm-require-extends', + new RequireExtendsTagValueNode( + new IdentifierTypeNode('Foo'), + 'optional description' + ) + ), + ]), + ]; + + yield [ + 'invalid without type and description', + '/** @phpstan-require-extends */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-require-extends', + new InvalidTagValueNode( + '', + new ParserException( + '*/', + Lexer::TOKEN_CLOSE_PHPDOC, + 29, + Lexer::TOKEN_IDENTIFIER, + null, + 1 + ) + ) + ), + ]), + ]; + } + + public function provideRequireImplementsTagsData(): Iterator + { + yield [ + 'OK without description', + '/** @phpstan-require-implements Foo */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-require-implements', + new RequireImplementsTagValueNode( + new IdentifierTypeNode('Foo'), + '' + ) + ), + ]), + ]; + + yield [ + 'OK with description', + '/** @phpstan-require-implements Foo optional description */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-require-implements', + new RequireImplementsTagValueNode( + new IdentifierTypeNode('Foo'), + 'optional description' + ) + ), + ]), + ]; + + yield [ + 'OK with psalm-prefix description', + '/** @psalm-require-implements Foo optional description */', + new PhpDocNode([ + new PhpDocTagNode( + '@psalm-require-implements', + new RequireImplementsTagValueNode( + new IdentifierTypeNode('Foo'), + 'optional description' + ) + ), + ]), + ]; + + yield [ + 'invalid without type and description', + '/** @phpstan-require-implements */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-require-implements', + new InvalidTagValueNode( + '', + new ParserException( + '*/', + Lexer::TOKEN_CLOSE_PHPDOC, + 32, + Lexer::TOKEN_IDENTIFIER, + null, + 1 + ) + ) + ), + ]), + ]; + } + public function provideDeprecatedTagsData(): Iterator { yield [ @@ -2475,6 +2742,26 @@ public function provideMethodTagsData(): Iterator ), ]), ]; + + yield [ + 'OK non-static with return type that starts with static type', + '/** @method static|null foo() */', + new PhpDocNode([ + new PhpDocTagNode( + '@method', + new MethodTagValueNode( + false, + new UnionTypeNode([ + new IdentifierTypeNode('static'), + new IdentifierTypeNode('null'), + ]), + 'foo', + [], + '' + ) + ), + ]), + ]; } @@ -5561,6 +5848,98 @@ public function provideSelfOutTagsData(): Iterator ]; } + public function provideCommentLikeDescriptions(): Iterator + { + yield [ + 'Comment after @param', + '/** @param int $a // this is a description */', + new PhpDocNode([ + new PhpDocTagNode('@param', new ParamTagValueNode( + new IdentifierTypeNode('int'), + false, + '$a', + '// this is a description' + )), + ]), + ]; + + yield [ + 'Comment on a separate line', + '/**' . PHP_EOL . + ' * @param int $a' . PHP_EOL . + ' * // this is a comment' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTagNode('@param', new ParamTagValueNode( + new IdentifierTypeNode('int'), + false, + '$a', + '' + )), + new PhpDocTextNode('// this is a comment'), + ]), + ]; + yield [ + 'Comment on a separate line 2', + '/**' . PHP_EOL . + ' * @param int $a' . PHP_EOL . + ' *' . PHP_EOL . + ' * // this is a comment' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTagNode('@param', new ParamTagValueNode( + new IdentifierTypeNode('int'), + false, + '$a', + '' + )), + new PhpDocTextNode(''), + new PhpDocTextNode('// this is a comment'), + ]), + ]; + yield [ + 'Comment after Doctrine tag 1', + '/** @ORM\Doctrine // this is a description */', + new PhpDocNode([ + new PhpDocTagNode('@ORM\Doctrine', new GenericTagValueNode('// this is a description')), + ]), + ]; + yield [ + 'Comment after Doctrine tag 2', + '/** @\ORM\Doctrine // this is a description */', + new PhpDocNode([ + new PhpDocTagNode('@\ORM\Doctrine', new DoctrineTagValueNode( + new DoctrineAnnotation('@\ORM\Doctrine', []), + '// this is a description' + )), + ]), + ]; + yield [ + 'Comment after Doctrine tag 3', + '/** @\ORM\Doctrine() // this is a description */', + new PhpDocNode([ + new PhpDocTagNode('@\ORM\Doctrine', new DoctrineTagValueNode( + new DoctrineAnnotation('@\ORM\Doctrine', []), + '// this is a description' + )), + ]), + ]; + yield [ + 'Comment after Doctrine tag 4', + '/** @\ORM\Doctrine() @\ORM\Entity() // this is a description */', + new PhpDocNode([ + new PhpDocTagNode('@\ORM\Doctrine', new DoctrineTagValueNode( + new DoctrineAnnotation('@\ORM\Doctrine', []), + '' + )), + new PhpDocTagNode('@\ORM\Entity', new DoctrineTagValueNode( + new DoctrineAnnotation('@\ORM\Entity', []), + '// this is a description' + )), + ]), + ]; + } + public function provideParamOutTagsData(): Iterator { yield [ @@ -6658,6 +7037,13 @@ public function dataLinesAndIndexes(): iterable [1, 1, 1, 3], ], ]; + + yield [ + '/** @api */', + [ + [1, 1, 1, 1], + ], + ]; } /** @@ -6732,6 +7118,15 @@ public function dataDeepNodesLinesAndIndexes(): iterable [2, 4, 4, 6], // DoctrineArray ], ]; + + yield [ + '/** @api */', + [ + [1, 1, 0, 3], + [1, 1, 1, 1], + [1, 1, 3, 1], // GenericTagValueNode is empty so start index is higher than end index + ], + ]; } @@ -6838,6 +7233,9 @@ public function testReturnTypeLinesAndIndexes(string $phpDoc, array $lines): voi * @dataProvider provideSpecializedTags * @dataProvider provideParamTagsData * @dataProvider provideTypelessParamTagsData + * @dataProvider provideParamImmediatelyInvokedCallableTagsData + * @dataProvider provideParamLaterInvokedCallableTagsData + * @dataProvider provideParamClosureThisTagsData * @dataProvider provideVarTagsData * @dataProvider provideReturnTagsData * @dataProvider provideThrowsTagsData diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 2c66d98d..d6c66bb8 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -10,6 +10,7 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\QuoteAwareConstExprStringNode; use PHPStan\PhpDocParser\Ast\Node; use PHPStan\PhpDocParser\Ast\NodeTraverser; +use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; @@ -767,6 +768,22 @@ public function provideParseData(): array new IdentifierTypeNode('Foo') ), ], + [ + 'pure-callable(): Foo', + new CallableTypeNode( + new IdentifierTypeNode('pure-callable'), + [], + new IdentifierTypeNode('Foo') + ), + ], + [ + 'pure-Closure(): Foo', + new CallableTypeNode( + new IdentifierTypeNode('pure-Closure'), + [], + new IdentifierTypeNode('Foo') + ), + ], [ 'callable(): ?Foo', new CallableTypeNode( @@ -897,6 +914,104 @@ public function provideParseData(): array new IdentifierTypeNode('Foo') ), ], + [ + 'callable(B): C', + new CallableTypeNode( + new IdentifierTypeNode('callable'), + [ + new CallableTypeParameterNode( + new IdentifierTypeNode('B'), + false, + false, + '', + false + ), + ], + new IdentifierTypeNode('C'), + [ + new TemplateTagValueNode('A', null, ''), + ] + ), + ], + [ + 'callable<>(): void', + new ParserException( + '>', + Lexer::TOKEN_END, + 9, + Lexer::TOKEN_IDENTIFIER + ), + ], + [ + 'ClosureReturns a collection object representing the new collection.
', + new IdentifierTypeNode('MongoCollection'), + Lexer::TOKEN_OPEN_ANGLE_BRACKET, + ], ]; } diff --git a/tests/PHPStan/Printer/PhpPrinterIndentationDetectorVisitor.php b/tests/PHPStan/Printer/PhpPrinterIndentationDetectorVisitor.php index 9e2b9248..58a807eb 100644 --- a/tests/PHPStan/Printer/PhpPrinterIndentationDetectorVisitor.php +++ b/tests/PHPStan/Printer/PhpPrinterIndentationDetectorVisitor.php @@ -4,8 +4,8 @@ use PhpParser\Internal\TokenStream; use PhpParser\Node; +use PhpParser\NodeTraverser; use PhpParser\NodeVisitorAbstract; -use PHPStan\PhpDocParser\Ast\NodeTraverser; use function count; use function preg_match; use function preg_match_all; diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 746ad027..07d68af0 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -17,6 +17,9 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineArray; use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineArrayItem; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamClosureThisTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; @@ -84,6 +87,7 @@ public function dataPrintFormatPreserving(): iterable }; yield ['/** */', '/** */', $noopVisitor]; + yield ['/** @api */', '/** @api */', $noopVisitor]; yield ['/** */', '/** */', $noopVisitor]; @@ -589,6 +593,35 @@ public function enterNode(Node $node) }; + $addCallableTemplateType = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof CallableTypeNode) { + $node->templateTypes[] = new TemplateTagValueNode( + 'T', + new IdentifierTypeNode('int'), + '' + ); + } + + return $node; + } + + }; + + yield [ + '/** @var Closure(): T */', + '/** @var Closure