From 0bb2fe4cc43a6144d3135facec4d036258a5e4dd Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 9 Oct 2023 10:33:20 +0200 Subject: [PATCH 01/26] Tests for comment-like descriptions --- tests/PHPStan/Parser/PhpDocParserTest.php | 93 +++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 97d6cbfe..c52be80b 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -117,6 +117,7 @@ protected function setUp(): void * @dataProvider provideParamOutTagsData * @dataProvider provideDoctrineData * @dataProvider provideDoctrineWithoutDoctrineCheckData + * @dataProvider provideCommentLikeDescriptions */ public function testParse( string $label, @@ -5561,6 +5562,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 [ From c2b8bbfa971e25976e9c64ea30a4e944e2688cb4 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 9 Oct 2023 10:44:39 +0200 Subject: [PATCH 02/26] Different site ID for Fathom --- apigen/theme/blocks/head.latte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apigen/theme/blocks/head.latte b/apigen/theme/blocks/head.latte index e7f54525..6077db0a 100644 --- a/apigen/theme/blocks/head.latte +++ b/apigen/theme/blocks/head.latte @@ -1,4 +1,4 @@ {define head} - + {/define} From 12f01d214f1c73b9c91fdb3b1c415e4c70652083 Mon Sep 17 00:00:00 2001 From: schlndh Date: Fri, 17 Nov 2023 18:09:13 +0100 Subject: [PATCH 03/26] fix HTML description detection for phpstorm stubs --- src/Parser/TypeParser.php | 12 ++++++++++-- tests/PHPStan/Parser/PhpDocParserTest.php | 14 ++++++++++++++ tests/PHPStan/Parser/TypeParserTest.php | 5 +++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 46de7aae..79e70275 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -7,7 +7,9 @@ use PHPStan\PhpDocParser\Lexer\Lexer; use function in_array; use function str_replace; +use function strlen; use function strpos; +use function substr_compare; use function trim; class TypeParser @@ -380,10 +382,16 @@ public function isHtml(TokenIterator $tokens): bool return false; } + $endTag = ''; + $endTagSearchOffset = - strlen($endTag); + while (!$tokens->isCurrentTokenType(Lexer::TOKEN_END)) { if ( - $tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET) - && strpos($tokens->currentTokenValue(), '/' . $htmlTagName . '>') !== false + ( + $tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET) + && strpos($tokens->currentTokenValue(), '/' . $htmlTagName . '>') !== false + ) + || substr_compare($tokens->currentTokenValue(), $endTag, $endTagSearchOffset) === 0 ) { return true; } diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index c52be80b..3b44c3bb 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -1301,6 +1301,20 @@ public function provideReturnTagsData(): Iterator ]), ]; + yield [ + 'OK with HTML description', + '/** @return MongoCollection

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 */', diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 2c66d98d..0cc294f5 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -2166,6 +2166,11 @@ public function provideParseData(): array false )), ], + [ + 'MongoCollection

Returns a collection object representing the new collection.

', + new IdentifierTypeNode('MongoCollection'), + Lexer::TOKEN_OPEN_ANGLE_BRACKET, + ], ]; } From 37d7684e4a5465b0b2abbc8bc9c21974b5a5b4f1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 02:14:17 +0000 Subject: [PATCH 04/26] Update dessant/lock-threads action to v5 --- .github/workflows/lock-closed-issues.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml index 4c7990df..c2b017b9 100644 --- a/.github/workflows/lock-closed-issues.yml +++ b/.github/workflows/lock-closed-issues.yml @@ -8,7 +8,7 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v4 + - uses: dessant/lock-threads@v5 with: github-token: ${{ github.token }} issue-inactive-days: '31' From 58cdc73125fc6075b7796d82ab75e20d0f3a2d41 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 02:14:13 +0000 Subject: [PATCH 05/26] Update metcalfc/changelog-generator action to v4.2.0 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e4a8ac62..2fb750a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Generate changelog id: changelog - uses: metcalfc/changelog-generator@v4.1.0 + uses: metcalfc/changelog-generator@v4.2.0 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} From 6bd0c26f3786cd9b7c359675cb789e35a8e07496 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Sun, 26 Nov 2023 19:27:59 +0100 Subject: [PATCH 06/26] Git export-ignore phpstan-baseline.neon --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index 1daa7443..984945c7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,5 +13,6 @@ tmp export-ignore build-abnfgen.sh export-ignore CODE_OF_CONDUCT.md export-ignore Makefile export-ignore +phpstan-baseline.neon export-ignore phpstan.neon export-ignore phpunit.xml export-ignore From fb1906635bec16e60a680bbb42d480eaa98bc81b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 14 Dec 2023 17:40:58 +0100 Subject: [PATCH 07/26] Fix --- tests/PHPStan/Printer/PhpPrinterIndentationDetectorVisitor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; From c7a1da402b761a30b4b0a902e3777460f11411fb Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 16 Dec 2023 09:56:21 +0100 Subject: [PATCH 08/26] Fix format-preserving printer for GenericTagValueNode without description --- src/Printer/Printer.php | 12 ++++++++++++ tests/PHPStan/Parser/PhpDocParserTest.php | 16 ++++++++++++++++ tests/PHPStan/Printer/PrinterTest.php | 1 + 3 files changed, 29 insertions(+) diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index d7feaf91..274a486f 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -534,6 +534,10 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes, throw new LogicException(); } + if ($itemEndPos < $itemStartPos) { + return null; + } + $result .= $originalTokens->getContentBetween($tokenIndex, $itemStartPos); if (count($delayedAdd) > 0) { @@ -742,6 +746,10 @@ private function printNodeFormatPreserving(Node $node, TokenIterator $originalTo throw new LogicException(); } + if ($endPos < $startPos) { + return $this->print($node); + } + $result = ''; $pos = $startPos; $subNodeNames = array_keys(get_object_vars($node)); @@ -795,6 +803,10 @@ private function printNodeFormatPreserving(Node $node, TokenIterator $originalTo throw new LogicException(); } + if ($subEndPos < $subStartPos) { + return $this->print($node); + } + if ($subNode === null) { return $this->print($node); } diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 3b44c3bb..3b293f8f 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -6765,6 +6765,13 @@ public function dataLinesAndIndexes(): iterable [1, 1, 1, 3], ], ]; + + yield [ + '/** @api */', + [ + [1, 1, 1, 1], + ], + ]; } /** @@ -6839,6 +6846,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 + ], + ]; } diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 746ad027..2307cdb2 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -84,6 +84,7 @@ public function dataPrintFormatPreserving(): iterable }; yield ['/** */', '/** */', $noopVisitor]; + yield ['/** @api */', '/** @api */', $noopVisitor]; yield ['/** */', '/** */', $noopVisitor]; From cb802d05ae9fe2a3b0bca9268ed65431f5905ede Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 16 Dec 2023 10:31:54 +0100 Subject: [PATCH 09/26] Update Slevomat CS for testing --- .github/workflows/test-slevomat-coding-standard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-slevomat-coding-standard.yml b/.github/workflows/test-slevomat-coding-standard.yml index fb266231..1967df97 100644 --- a/.github/workflows/test-slevomat-coding-standard.yml +++ b/.github/workflows/test-slevomat-coding-standard.yml @@ -32,7 +32,7 @@ jobs: with: repository: slevomat/coding-standard path: slevomat-cs - ref: 8.13.2 + ref: ad4eab6f2cf9aa099c8853e4e3a250a9ae5fb4bd - name: "Install PHP" uses: "shivammathur/setup-php@v2" From fedf211ff14ec8381c9bf5714e33a7a552dd1acc Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 16 Dec 2023 10:33:33 +0100 Subject: [PATCH 10/26] Fix --- src/Printer/Printer.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index 274a486f..57e652eb 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -534,10 +534,6 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes, throw new LogicException(); } - if ($itemEndPos < $itemStartPos) { - return null; - } - $result .= $originalTokens->getContentBetween($tokenIndex, $itemStartPos); if (count($delayedAdd) > 0) { @@ -746,10 +742,6 @@ private function printNodeFormatPreserving(Node $node, TokenIterator $originalTo throw new LogicException(); } - if ($endPos < $startPos) { - return $this->print($node); - } - $result = ''; $pos = $startPos; $subNodeNames = array_keys(get_object_vars($node)); From 23d546444c54b7234e081741d1499ac740f4e071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Tue, 19 Dec 2023 16:27:06 +0100 Subject: [PATCH 11/26] Update apiref.yml --- .github/workflows/apiref.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/apiref.yml b/.github/workflows/apiref.yml index 85f91693..a315a699 100644 --- a/.github/workflows/apiref.yml +++ b/.github/workflows/apiref.yml @@ -42,7 +42,7 @@ jobs: run: "cp apigen/favicon.png docs/favicon.png" - name: Upload artifact - uses: actions/upload-pages-artifact@v2 + uses: actions/upload-pages-artifact@v3 with: path: 'docs' @@ -63,4 +63,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v2 + uses: actions/deploy-pages@v4 From f6c9b601d3d9988e5c0ab0628025cff925facfbb Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 19 Dec 2023 16:34:54 +0100 Subject: [PATCH 12/26] Revert "Update apiref.yml" This reverts commit 23d546444c54b7234e081741d1499ac740f4e071. --- .github/workflows/apiref.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/apiref.yml b/.github/workflows/apiref.yml index a315a699..85f91693 100644 --- a/.github/workflows/apiref.yml +++ b/.github/workflows/apiref.yml @@ -42,7 +42,7 @@ jobs: run: "cp apigen/favicon.png docs/favicon.png" - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v2 with: path: 'docs' @@ -63,4 +63,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v2 From 77db537e8d714f03949717b545c12a8c43ab3a85 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 2 Jan 2024 21:41:48 +0100 Subject: [PATCH 13/26] Update apiref.yml --- .github/workflows/apiref.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/apiref.yml b/.github/workflows/apiref.yml index 85f91693..a315a699 100644 --- a/.github/workflows/apiref.yml +++ b/.github/workflows/apiref.yml @@ -42,7 +42,7 @@ jobs: run: "cp apigen/favicon.png docs/favicon.png" - name: Upload artifact - uses: actions/upload-pages-artifact@v2 + uses: actions/upload-pages-artifact@v3 with: path: 'docs' @@ -63,4 +63,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v2 + uses: actions/deploy-pages@v4 From bd84b629c8de41aa2ae82c067c955e06f1b00240 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 4 Jan 2024 18:06:16 +0100 Subject: [PATCH 14/26] Added support for `@phpstan-require-extends` and `@phpstan-require-implements` PHPDoc tags --- src/Ast/PhpDoc/PhpDocNode.php | 25 ++++ src/Ast/PhpDoc/RequireExtendsTagValueNode.php | 32 +++++ .../PhpDoc/RequireImplementsTagValueNode.php | 32 +++++ src/Parser/PhpDocParser.php | 24 ++++ src/Printer/Printer.php | 10 ++ .../Ast/ToString/PhpDocToStringTest.php | 11 ++ tests/PHPStan/Parser/PhpDocParserTest.php | 136 ++++++++++++++++++ 7 files changed, 270 insertions(+) create mode 100644 src/Ast/PhpDoc/RequireExtendsTagValueNode.php create mode 100644 src/Ast/PhpDoc/RequireImplementsTagValueNode.php diff --git a/src/Ast/PhpDoc/PhpDocNode.php b/src/Ast/PhpDoc/PhpDocNode.php index 25f1939c..4c509f41 100644 --- a/src/Ast/PhpDoc/PhpDocNode.php +++ b/src/Ast/PhpDoc/PhpDocNode.php @@ -187,6 +187,31 @@ static function (PhpDocTagValueNode $value): bool { ); } + /** + * @return RequireExtendsTagValueNode[] + */ + public function getRequireExtendsTagValues(string $tagName = '@phpstan-require-extends'): array + { + return array_filter( + array_column($this->getTagsByName($tagName), 'value'), + static function (PhpDocTagValueNode $value): bool { + return $value instanceof RequireExtendsTagValueNode; + } + ); + } + + /** + * @return RequireImplementsTagValueNode[] + */ + public function getRequireImplementsTagValues(string $tagName = '@phpstan-require-implements'): array + { + return array_filter( + array_column($this->getTagsByName($tagName), 'value'), + static function (PhpDocTagValueNode $value): bool { + return $value instanceof RequireImplementsTagValueNode; + } + ); + } /** * @return DeprecatedTagValueNode[] diff --git a/src/Ast/PhpDoc/RequireExtendsTagValueNode.php b/src/Ast/PhpDoc/RequireExtendsTagValueNode.php new file mode 100644 index 00000000..91c26892 --- /dev/null +++ b/src/Ast/PhpDoc/RequireExtendsTagValueNode.php @@ -0,0 +1,32 @@ +type = $type; + $this->description = $description; + } + + + public function __toString(): string + { + return trim("{$this->type} {$this->description}"); + } + +} diff --git a/src/Ast/PhpDoc/RequireImplementsTagValueNode.php b/src/Ast/PhpDoc/RequireImplementsTagValueNode.php new file mode 100644 index 00000000..65c9213f --- /dev/null +++ b/src/Ast/PhpDoc/RequireImplementsTagValueNode.php @@ -0,0 +1,32 @@ +type = $type; + $this->description = $description; + } + + + public function __toString(): string + { + return trim("{$this->type} {$this->description}"); + } + +} diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index 15a2aa5c..e87d92c4 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -408,6 +408,16 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph $tagValue = $this->parseMixinTagValue($tokens); break; + case '@psalm-require-extends': + case '@phpstan-require-extends': + $tagValue = $this->parseRequireExtendsTagValue($tokens); + break; + + case '@psalm-require-implements': + case '@phpstan-require-implements': + $tagValue = $this->parseRequireImplementsTagValue($tokens); + break; + case '@deprecated': $tagValue = $this->parseDeprecatedTagValue($tokens); break; @@ -877,6 +887,20 @@ private function parseMixinTagValue(TokenIterator $tokens): Ast\PhpDoc\MixinTagV return new Ast\PhpDoc\MixinTagValueNode($type, $description); } + private function parseRequireExtendsTagValue(TokenIterator $tokens): Ast\PhpDoc\RequireExtendsTagValueNode + { + $type = $this->typeParser->parse($tokens); + $description = $this->parseOptionalDescription($tokens, true); + return new Ast\PhpDoc\RequireExtendsTagValueNode($type, $description); + } + + private function parseRequireImplementsTagValue(TokenIterator $tokens): Ast\PhpDoc\RequireImplementsTagValueNode + { + $type = $this->typeParser->parse($tokens); + $description = $this->parseOptionalDescription($tokens, true); + return new Ast\PhpDoc\RequireImplementsTagValueNode($type, $description); + } + private function parseDeprecatedTagValue(TokenIterator $tokens): Ast\PhpDoc\DeprecatedTagValueNode { $description = $this->parseOptionalDescription($tokens); diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index 57e652eb..0093e6ca 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -28,6 +28,8 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\SelfOutTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; @@ -283,6 +285,14 @@ private function printTagValue(PhpDocTagValueNode $node): string $type = $this->printType($node->type); return trim("{$type} {$node->description}"); } + if ($node instanceof RequireExtendsTagValueNode) { + $type = $this->printType($node->type); + return trim("{$type} {$node->description}"); + } + if ($node instanceof RequireImplementsTagValueNode) { + $type = $this->printType($node->type); + return trim("{$type} {$node->description}"); + } if ($node instanceof ParamOutTagValueNode) { $type = $this->printType($node->type); return trim("{$type} {$node->parameterName} {$node->description}"); diff --git a/tests/PHPStan/Ast/ToString/PhpDocToStringTest.php b/tests/PHPStan/Ast/ToString/PhpDocToStringTest.php index faac0616..b57b7db6 100644 --- a/tests/PHPStan/Ast/ToString/PhpDocToStringTest.php +++ b/tests/PHPStan/Ast/ToString/PhpDocToStringTest.php @@ -28,6 +28,8 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\SelfOutTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; @@ -213,6 +215,15 @@ public static function provideClassCases(): Generator ['Foo\\Bar Baz', new MixinTagValueNode(new IdentifierTypeNode('Foo\\Bar'), 'Baz')], ]; + yield from [ + ['PHPUnit\\TestCase', new RequireExtendsTagValueNode(new IdentifierTypeNode('PHPUnit\\TestCase'), '')], + ['Foo\\Bar Baz', new RequireExtendsTagValueNode(new IdentifierTypeNode('Foo\\Bar'), 'Baz')], + ]; + yield from [ + ['PHPUnit\\TestCase', new RequireImplementsTagValueNode(new IdentifierTypeNode('PHPUnit\\TestCase'), '')], + ['Foo\\Bar Baz', new RequireImplementsTagValueNode(new IdentifierTypeNode('Foo\\Bar'), 'Baz')], + ]; + yield from [ ['Foo array', new TypeAliasTagValueNode('Foo', $arrayOfStrings)], ['Test from Foo\Bar', new TypeAliasImportTagValueNode('Test', $bar, null)], diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 3b293f8f..67a9d123 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -35,6 +35,8 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\SelfOutTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; @@ -100,6 +102,8 @@ protected function setUp(): void * @dataProvider provideReturnTagsData * @dataProvider provideThrowsTagsData * @dataProvider provideMixinTagsData + * @dataProvider provideRequireExtendsTagsData + * @dataProvider provideRequireImplementsTagsData * @dataProvider provideDeprecatedTagsData * @dataProvider providePropertyTagsData * @dataProvider provideMethodTagsData @@ -1908,6 +1912,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 [ From 6301a43fe52259a692df14085d2db803cb88f089 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 01:12:22 +0000 Subject: [PATCH 15/26] Update github-actions --- .github/workflows/release-toot.yml | 2 +- .github/workflows/send-pr.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-toot.yml b/.github/workflows/release-toot.yml index 6a1c8156..1ba4fd77 100644 --- a/.github/workflows/release-toot.yml +++ b/.github/workflows/release-toot.yml @@ -10,7 +10,7 @@ jobs: toot: runs-on: ubuntu-latest steps: - - uses: cbrgm/mastodon-github-action@v1 + - uses: cbrgm/mastodon-github-action@v2 if: ${{ !github.event.repository.private }} with: # GitHub event payload diff --git a/.github/workflows/send-pr.yml b/.github/workflows/send-pr.yml index 023293c7..37b2a974 100644 --- a/.github/workflows/send-pr.yml +++ b/.github/workflows/send-pr.yml @@ -35,7 +35,7 @@ jobs: - name: "Create Pull Request" id: create-pr - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@v6 with: token: ${{ secrets.PHPSTAN_BOT_TOKEN }} path: ./phpstan-src From e7f0d8f23795321c4f2cd687d5e9c2a4ee46be9c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 21:05:35 +0000 Subject: [PATCH 16/26] Update metcalfc/changelog-generator action to v4.3.1 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2fb750a4..b1a669a9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Generate changelog id: changelog - uses: metcalfc/changelog-generator@v4.2.0 + uses: metcalfc/changelog-generator@v4.3.1 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} From c23674d80bbeaf223f51469b69773283cbdcf9bd Mon Sep 17 00:00:00 2001 From: Brad Miller <28307684+mad-briller@users.noreply.github.com> Date: Fri, 23 Feb 2024 16:03:21 +0000 Subject: [PATCH 17/26] Parse generic callables --- doc/grammars/type.abnf | 20 +++-- src/Ast/Type/CallableTypeNode.php | 13 ++- src/Parser/PhpDocParser.php | 41 +++------- src/Parser/TypeParser.php | 104 ++++++++++++++++++++++-- src/Printer/Printer.php | 8 +- tests/PHPStan/Parser/TypeParserTest.php | 99 ++++++++++++++++++++++ tests/PHPStan/Printer/PrinterTest.php | 29 +++++++ 7 files changed, 267 insertions(+), 47 deletions(-) diff --git a/doc/grammars/type.abnf b/doc/grammars/type.abnf index 0b3247ba..36118d2b 100644 --- a/doc/grammars/type.abnf +++ b/doc/grammars/type.abnf @@ -35,7 +35,13 @@ GenericTypeArgument / TokenWildcard Callable - = TokenParenthesesOpen [CallableParameters] TokenParenthesesClose TokenColon CallableReturnType + = [CallableTemplate] TokenParenthesesOpen [CallableParameters] TokenParenthesesClose TokenColon CallableReturnType + +CallableTemplate + = TokenAngleBracketOpen CallableTemplateArgument *(TokenComma CallableTemplateArgument) TokenAngleBracketClose + +CallableTemplateArgument + = TokenIdentifier [1*ByteHorizontalWs TokenOf Type] CallableParameters = CallableParameter *(TokenComma CallableParameter) @@ -192,6 +198,9 @@ TokenIs TokenNot = %s"not" 1*ByteHorizontalWs +TokenOf + = %s"of" 1*ByteHorizontalWs + TokenContravariant = %s"contravariant" 1*ByteHorizontalWs @@ -211,7 +220,7 @@ TokenIdentifier ByteHorizontalWs = %x09 ; horizontal tab - / %x20 ; space + / " " ByteNumberSign = "+" @@ -238,11 +247,8 @@ ByteIdentifierFirst / %x80-FF ByteIdentifierSecond - = %x30-39 ; 0-9 - / %x41-5A ; A-Z - / "_" - / %x61-7A ; a-z - / %x80-FF + = ByteIdentifierFirst + / %x30-39 ; 0-9 ByteSingleQuote = %x27 ; ' diff --git a/src/Ast/Type/CallableTypeNode.php b/src/Ast/Type/CallableTypeNode.php index e57e5f82..4c913198 100644 --- a/src/Ast/Type/CallableTypeNode.php +++ b/src/Ast/Type/CallableTypeNode.php @@ -3,6 +3,7 @@ namespace PHPStan\PhpDocParser\Ast\Type; use PHPStan\PhpDocParser\Ast\NodeAttributes; +use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use function implode; class CallableTypeNode implements TypeNode @@ -13,6 +14,9 @@ class CallableTypeNode implements TypeNode /** @var IdentifierTypeNode */ public $identifier; + /** @var TemplateTagValueNode[] */ + public $templateTypes; + /** @var CallableTypeParameterNode[] */ public $parameters; @@ -21,12 +25,14 @@ class CallableTypeNode implements TypeNode /** * @param CallableTypeParameterNode[] $parameters + * @param TemplateTagValueNode[] $templateTypes */ - public function __construct(IdentifierTypeNode $identifier, array $parameters, TypeNode $returnType) + public function __construct(IdentifierTypeNode $identifier, array $parameters, TypeNode $returnType, array $templateTypes = []) { $this->identifier = $identifier; $this->parameters = $parameters; $this->returnType = $returnType; + $this->templateTypes = $templateTypes; } @@ -36,8 +42,11 @@ public function __toString(): string if ($returnType instanceof self) { $returnType = "({$returnType})"; } + $template = $this->templateTypes !== [] + ? '<' . implode(', ', $this->templateTypes) . '>' + : ''; $parameters = implode(', ', $this->parameters); - return "{$this->identifier}({$parameters}): {$returnType}"; + return "{$this->identifier}{$template}({$parameters}): {$returnType}"; } } diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index e87d92c4..b6356408 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -449,7 +449,12 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph case '@template-contravariant': case '@phpstan-template-contravariant': case '@psalm-template-contravariant': - $tagValue = $this->parseTemplateTagValue($tokens, true); + $tagValue = $this->typeParser->parseTemplateTagValue( + $tokens, + function ($tokens) { + return $this->parseOptionalDescription($tokens); + } + ); break; case '@extends': @@ -947,7 +952,12 @@ private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTa do { $startLine = $tokens->currentTokenLine(); $startIndex = $tokens->currentTokenIndex(); - $templateTypes[] = $this->enrichWithAttributes($tokens, $this->parseTemplateTagValue($tokens, false), $startLine, $startIndex); + $templateTypes[] = $this->enrichWithAttributes( + $tokens, + $this->typeParser->parseTemplateTagValue($tokens), + $startLine, + $startIndex + ); } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); } @@ -1003,33 +1013,6 @@ private function parseMethodTagValueParameter(TokenIterator $tokens): Ast\PhpDoc ); } - private function parseTemplateTagValue(TokenIterator $tokens, bool $parseDescription): Ast\PhpDoc\TemplateTagValueNode - { - $name = $tokens->currentTokenValue(); - $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); - - if ($tokens->tryConsumeTokenValue('of') || $tokens->tryConsumeTokenValue('as')) { - $bound = $this->typeParser->parse($tokens); - - } else { - $bound = null; - } - - if ($tokens->tryConsumeTokenValue('=')) { - $default = $this->typeParser->parse($tokens); - } else { - $default = null; - } - - if ($parseDescription) { - $description = $this->parseOptionalDescription($tokens); - } else { - $description = ''; - } - - return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description, $default); - } - private function parseExtendsTagValue(string $tagName, TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode { $startLine = $tokens->currentTokenLine(); diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 79e70275..ebc2fbab 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -4,6 +4,7 @@ use LogicException; use PHPStan\PhpDocParser\Ast; +use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use PHPStan\PhpDocParser\Lexer\Lexer; use function in_array; use function str_replace; @@ -164,13 +165,17 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode return $type; } - $type = $this->parseGeneric($tokens, $type); + $origType = $type; + $type = $this->tryParseCallable($tokens, $type, true); + if ($type === $origType) { + $type = $this->parseGeneric($tokens, $type); - if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { - $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); + if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { + $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); + } } } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) { - $type = $this->tryParseCallable($tokens, $type); + $type = $this->tryParseCallable($tokens, $type, false); } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); @@ -464,10 +469,48 @@ public function parseGenericTypeArgument(TokenIterator $tokens): array return [$type, $variance]; } + /** + * @throws ParserException + * @param ?callable(TokenIterator): string $parseDescription + */ + public function parseTemplateTagValue( + TokenIterator $tokens, + ?callable $parseDescription = null + ): TemplateTagValueNode + { + $name = $tokens->currentTokenValue(); + $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); + + if ($tokens->tryConsumeTokenValue('of') || $tokens->tryConsumeTokenValue('as')) { + $bound = $this->parse($tokens); + + } else { + $bound = null; + } + + if ($tokens->tryConsumeTokenValue('=')) { + $default = $this->parse($tokens); + } else { + $default = null; + } + + if ($parseDescription !== null) { + $description = $parseDescription($tokens); + } else { + $description = ''; + } + + return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description, $default); + } + /** @phpstan-impure */ - private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier): Ast\Type\TypeNode + private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier, bool $hasTemplate): Ast\Type\TypeNode { + $templates = $hasTemplate + ? $this->parseCallableTemplates($tokens) + : []; + $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES); $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); @@ -492,7 +535,52 @@ private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNod $startIndex = $tokens->currentTokenIndex(); $returnType = $this->enrichWithAttributes($tokens, $this->parseCallableReturnType($tokens), $startLine, $startIndex); - return new Ast\Type\CallableTypeNode($identifier, $parameters, $returnType); + return new Ast\Type\CallableTypeNode($identifier, $parameters, $returnType, $templates); + } + + + /** + * @return Ast\PhpDoc\TemplateTagValueNode[] + * + * @phpstan-impure + */ + private function parseCallableTemplates(TokenIterator $tokens): array + { + $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); + + $templates = []; + + $isFirst = true; + while ($isFirst || $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + + // trailing comma case + if (!$isFirst && $tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) { + break; + } + $isFirst = false; + + $templates[] = $this->parseCallableTemplateArgument($tokens); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + } + + $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); + + return $templates; + } + + + private function parseCallableTemplateArgument(TokenIterator $tokens): Ast\PhpDoc\TemplateTagValueNode + { + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); + + return $this->enrichWithAttributes( + $tokens, + $this->parseTemplateTagValue($tokens), + $startLine, + $startIndex + ); } @@ -670,11 +758,11 @@ private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNo /** @phpstan-impure */ - private function tryParseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier): Ast\Type\TypeNode + private function tryParseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier, bool $hasTemplate): Ast\Type\TypeNode { try { $tokens->pushSavePoint(); - $type = $this->parseCallable($tokens, $identifier); + $type = $this->parseCallable($tokens, $identifier, $hasTemplate); $tokens->dropSavePoint(); } catch (ParserException $e) { diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index 0093e6ca..d9a060b3 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -99,6 +99,7 @@ final class Printer ArrayShapeNode::class . '->items' => ', ', ObjectShapeNode::class . '->items' => ', ', CallableTypeNode::class . '->parameters' => ', ', + CallableTypeNode::class . '->templateTypes' => ', ', GenericTypeNode::class . '->genericTypes' => ', ', ConstExprArrayNode::class . '->items' => ', ', MethodTagValueNode::class . '->parameters' => ', ', @@ -380,10 +381,15 @@ private function printType(TypeNode $node): string } else { $returnType = $this->printType($node->returnType); } + $template = $node->templateTypes !== [] + ? '<' . implode(', ', array_map(function (TemplateTagValueNode $templateNode): string { + return $this->print($templateNode); + }, $node->templateTypes)) . '>' + : ''; $parameters = implode(', ', array_map(function (CallableTypeParameterNode $parameterNode): string { return $this->print($parameterNode); }, $node->parameters)); - return "{$node->identifier}({$parameters}): {$returnType}"; + return "{$node->identifier}{$template}({$parameters}): {$returnType}"; } if ($node instanceof ConditionalTypeForParameterNode) { return sprintf( diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 0cc294f5..f4d656dd 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; @@ -897,6 +898,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 + ), + ], + [ + 'Closure(T, int): (T|false)', + new CallableTypeNode( + new IdentifierTypeNode('Closure'), + [ + new CallableTypeParameterNode( + new IdentifierTypeNode('T'), + false, + false, + '', + false + ), + new CallableTypeParameterNode( + new IdentifierTypeNode('int'), + false, + false, + '', + false + ), + ], + new UnionTypeNode([ + new IdentifierTypeNode('T'), + new IdentifierTypeNode('false'), + ]), + [ + new TemplateTagValueNode('T', new IdentifierTypeNode('Model'), ''), + ] + ), + ], + [ + '\Closure(Tx, Ty): array{ Ty, Tx }', + new CallableTypeNode( + new IdentifierTypeNode('\Closure'), + [ + new CallableTypeParameterNode( + new IdentifierTypeNode('Tx'), + false, + false, + '', + false + ), + new CallableTypeParameterNode( + new IdentifierTypeNode('Ty'), + false, + false, + '', + false + ), + ], + new ArrayShapeNode([ + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('Ty') + ), + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('Tx') + ), + ]), + [ + new TemplateTagValueNode('Tx', new UnionTypeNode([ + new IdentifierTypeNode('X'), + new IdentifierTypeNode('Z'), + ]), ''), + new TemplateTagValueNode('Ty', new IdentifierTypeNode('Y'), ''), + ] + ), + ], [ '(Foo\\Bar, (int | (string & bar)[])> | Lorem)', new UnionTypeNode([ diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 2307cdb2..34a2e893 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -590,6 +590,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(): T */', + $addCallableTemplateType, + ]; + + yield [ + '/** @var \Closure(U): T */', + '/** @var \Closure(U): T */', + $addCallableTemplateType, + ]; + yield [ '/** * @param callable(): void $cb From 231e3186624c03d7e7c890ec662b81e6b0405227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Tvrd=C3=ADk?= Date: Fri, 23 Feb 2024 17:05:55 +0100 Subject: [PATCH 18/26] Improve static keyword conflict resolution in `@method` --- src/Parser/PhpDocParser.php | 18 +++++++++++------- tests/PHPStan/Parser/PhpDocParserTest.php | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index b6356408..475dd5ba 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -924,10 +924,16 @@ private function parsePropertyTagValue(TokenIterator $tokens): Ast\PhpDoc\Proper private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueNode { - $isStatic = $tokens->tryConsumeTokenValue('static'); - $startLine = $tokens->currentTokenLine(); - $startIndex = $tokens->currentTokenIndex(); - $returnTypeOrMethodName = $this->typeParser->parse($tokens); + $staticKeywordOrReturnTypeOrMethodName = $this->typeParser->parse($tokens); + + if ($staticKeywordOrReturnTypeOrMethodName instanceof Ast\Type\IdentifierTypeNode && $staticKeywordOrReturnTypeOrMethodName->name === 'static') { + $isStatic = true; + $returnTypeOrMethodName = $this->typeParser->parse($tokens); + + } else { + $isStatic = false; + $returnTypeOrMethodName = $staticKeywordOrReturnTypeOrMethodName; + } if ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) { $returnType = $returnTypeOrMethodName; @@ -935,9 +941,7 @@ private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTa $tokens->next(); } elseif ($returnTypeOrMethodName instanceof Ast\Type\IdentifierTypeNode) { - $returnType = $isStatic - ? $this->typeParser->enrichWithAttributes($tokens, new Ast\Type\IdentifierTypeNode('static'), $startLine, $startIndex) - : null; + $returnType = $isStatic ? $staticKeywordOrReturnTypeOrMethodName : null; $methodName = $returnTypeOrMethodName->name; $isStatic = false; diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 67a9d123..b8f2c0a6 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -2626,6 +2626,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', + [], + '' + ) + ), + ]), + ]; } From 8ce0d65bcdd2c7f1c494746f4d1ee50d056f48fc Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 20 Mar 2024 11:05:34 +0100 Subject: [PATCH 19/26] Support for `@param-immediately-invoked-callable` and `@param-later-invoked-callable` --- ...ImmediatelyInvokedCallableTagValueNode.php | 30 ++++++++ .../ParamLaterInvokedCallableTagValueNode.php | 30 ++++++++ src/Ast/PhpDoc/PhpDocNode.php | 28 ++++++++ src/Parser/PhpDocParser.php | 28 ++++++++ src/Printer/Printer.php | 8 +++ tests/PHPStan/Parser/PhpDocParserTest.php | 68 +++++++++++++++++++ tests/PHPStan/Printer/PrinterTest.php | 38 +++++++++++ 7 files changed, 230 insertions(+) create mode 100644 src/Ast/PhpDoc/ParamImmediatelyInvokedCallableTagValueNode.php create mode 100644 src/Ast/PhpDoc/ParamLaterInvokedCallableTagValueNode.php diff --git a/src/Ast/PhpDoc/ParamImmediatelyInvokedCallableTagValueNode.php b/src/Ast/PhpDoc/ParamImmediatelyInvokedCallableTagValueNode.php new file mode 100644 index 00000000..0f480f7a --- /dev/null +++ b/src/Ast/PhpDoc/ParamImmediatelyInvokedCallableTagValueNode.php @@ -0,0 +1,30 @@ +parameterName = $parameterName; + $this->description = $description; + } + + public function __toString(): string + { + return trim("{$this->parameterName} {$this->description}"); + } + +} diff --git a/src/Ast/PhpDoc/ParamLaterInvokedCallableTagValueNode.php b/src/Ast/PhpDoc/ParamLaterInvokedCallableTagValueNode.php new file mode 100644 index 00000000..eab353f9 --- /dev/null +++ b/src/Ast/PhpDoc/ParamLaterInvokedCallableTagValueNode.php @@ -0,0 +1,30 @@ +parameterName = $parameterName; + $this->description = $description; + } + + public function __toString(): string + { + return trim("{$this->parameterName} {$this->description}"); + } + +} diff --git a/src/Ast/PhpDoc/PhpDocNode.php b/src/Ast/PhpDoc/PhpDocNode.php index 4c509f41..b3b68811 100644 --- a/src/Ast/PhpDoc/PhpDocNode.php +++ b/src/Ast/PhpDoc/PhpDocNode.php @@ -90,6 +90,34 @@ static function (PhpDocTagValueNode $value): bool { } + /** + * @return ParamImmediatelyInvokedCallableTagValueNode[] + */ + public function getParamImmediatelyInvokedCallableTagValues(string $tagName = '@param-immediately-invoked-callable'): array + { + return array_filter( + array_column($this->getTagsByName($tagName), 'value'), + static function (PhpDocTagValueNode $value): bool { + return $value instanceof ParamImmediatelyInvokedCallableTagValueNode; + } + ); + } + + + /** + * @return ParamLaterInvokedCallableTagValueNode[] + */ + public function getParamLaterInvokedCallableTagValues(string $tagName = '@param-later-invoked-callable'): array + { + return array_filter( + array_column($this->getTagsByName($tagName), 'value'), + static function (PhpDocTagValueNode $value): bool { + return $value instanceof ParamLaterInvokedCallableTagValueNode; + } + ); + } + + /** * @return TemplateTagValueNode[] */ diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index 475dd5ba..9431fb6f 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -387,6 +387,16 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph $tagValue = $this->parseParamTagValue($tokens); break; + case '@param-immediately-invoked-callable': + case '@phpstan-param-immediately-invoked-callable': + $tagValue = $this->parseParamImmediatelyInvokedCallableTagValue($tokens); + break; + + case '@param-later-invoked-callable': + case '@phpstan-param-later-invoked-callable': + $tagValue = $this->parseParamLaterInvokedCallableTagValue($tokens); + break; + case '@var': case '@phpstan-var': case '@psalm-var': @@ -861,6 +871,24 @@ private function parseParamTagValue(TokenIterator $tokens): Ast\PhpDoc\PhpDocTag } + private function parseParamImmediatelyInvokedCallableTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode + { + $parameterName = $this->parseRequiredVariableName($tokens); + $description = $this->parseOptionalDescription($tokens); + + return new Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode($parameterName, $description); + } + + + private function parseParamLaterInvokedCallableTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode + { + $parameterName = $this->parseRequiredVariableName($tokens); + $description = $this->parseOptionalDescription($tokens); + + return new Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode($parameterName, $description); + } + + private function parseVarTagValue(TokenIterator $tokens): Ast\PhpDoc\VarTagValueNode { $type = $this->typeParser->parse($tokens); diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index d9a060b3..837c9e58 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -20,6 +20,8 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamOutTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocChildNode; @@ -304,6 +306,12 @@ private function printTagValue(PhpDocTagValueNode $node): string $type = $this->printType($node->type); return trim("{$type} {$reference}{$variadic}{$node->parameterName} {$node->description}"); } + if ($node instanceof ParamImmediatelyInvokedCallableTagValueNode) { + return trim("{$node->parameterName} {$node->description}"); + } + if ($node instanceof ParamLaterInvokedCallableTagValueNode) { + return trim("{$node->parameterName} {$node->description}"); + } if ($node instanceof PropertyTagValueNode) { $type = $this->printType($node->type); return trim("{$type} {$node->propertyName} {$node->description}"); diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index b8f2c0a6..8d0ce734 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -29,6 +29,8 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamOutTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; @@ -97,6 +99,8 @@ protected function setUp(): void * @dataProvider provideTagsWithNumbers * @dataProvider provideSpecializedTags * @dataProvider provideParamTagsData + * @dataProvider provideParamImmediatelyInvokedCallableTagsData + * @dataProvider provideParamLaterInvokedCallableTagsData * @dataProvider provideTypelessParamTagsData * @dataProvider provideVarTagsData * @dataProvider provideReturnTagsData @@ -620,6 +624,68 @@ public function provideTypelessParamTagsData(): Iterator ]; } + public function provideParamImmediatelyInvokedCallableTagsData(): Iterator + { + yield [ + 'OK', + '/** @param-immediately-invoked-callable $foo */', + new PhpDocNode([ + new PhpDocTagNode( + '@param-immediately-invoked-callable', + new ParamImmediatelyInvokedCallableTagValueNode( + '$foo', + '' + ) + ), + ]), + ]; + + yield [ + 'OK with description', + '/** @param-immediately-invoked-callable $foo test two three */', + new PhpDocNode([ + new PhpDocTagNode( + '@param-immediately-invoked-callable', + new ParamImmediatelyInvokedCallableTagValueNode( + '$foo', + 'test two three' + ) + ), + ]), + ]; + } + + public function provideParamLaterInvokedCallableTagsData(): Iterator + { + yield [ + 'OK', + '/** @param-later-invoked-callable $foo */', + new PhpDocNode([ + new PhpDocTagNode( + '@param-later-invoked-callable', + new ParamLaterInvokedCallableTagValueNode( + '$foo', + '' + ) + ), + ]), + ]; + + yield [ + 'OK with description', + '/** @param-later-invoked-callable $foo test two three */', + new PhpDocNode([ + new PhpDocTagNode( + '@param-later-invoked-callable', + new ParamLaterInvokedCallableTagValueNode( + '$foo', + 'test two three' + ) + ), + ]), + ]; + } + public function provideVarTagsData(): Iterator { yield [ @@ -7117,6 +7183,8 @@ public function testReturnTypeLinesAndIndexes(string $phpDoc, array $lines): voi * @dataProvider provideSpecializedTags * @dataProvider provideParamTagsData * @dataProvider provideTypelessParamTagsData + * @dataProvider provideParamImmediatelyInvokedCallableTagsData + * @dataProvider provideParamLaterInvokedCallableTagsData * @dataProvider provideVarTagsData * @dataProvider provideReturnTagsData * @dataProvider provideThrowsTagsData diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 34a2e893..ddbe4310 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -17,6 +17,8 @@ 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\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; @@ -1665,6 +1667,42 @@ public function enterNode(Node $node) }, ]; + + yield [ + '/** @param-immediately-invoked-callable $foo test */', + '/** @param-immediately-invoked-callable $bar foo */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ParamImmediatelyInvokedCallableTagValueNode) { + $node->parameterName = '$bar'; + $node->description = 'foo'; + } + + return $node; + } + + }, + ]; + + yield [ + '/** @param-later-invoked-callable $foo test */', + '/** @param-later-invoked-callable $bar foo */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ParamLaterInvokedCallableTagValueNode) { + $node->parameterName = '$bar'; + $node->description = 'foo'; + } + + return $node; + } + + }, + ]; } /** From 86e4d5a4b036f8f0be1464522f4c6b584c452757 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 21 Mar 2024 14:06:20 +0100 Subject: [PATCH 20/26] Support for `@param-closure-this` --- .../PhpDoc/ParamClosureThisTagValueNode.php | 35 +++++++++++++ src/Ast/PhpDoc/PhpDocNode.php | 14 +++++ src/Parser/PhpDocParser.php | 15 ++++++ src/Printer/Printer.php | 4 ++ tests/PHPStan/Parser/PhpDocParserTest.php | 51 +++++++++++++++++++ tests/PHPStan/Printer/PrinterTest.php | 20 ++++++++ 6 files changed, 139 insertions(+) create mode 100644 src/Ast/PhpDoc/ParamClosureThisTagValueNode.php diff --git a/src/Ast/PhpDoc/ParamClosureThisTagValueNode.php b/src/Ast/PhpDoc/ParamClosureThisTagValueNode.php new file mode 100644 index 00000000..0ac2131a --- /dev/null +++ b/src/Ast/PhpDoc/ParamClosureThisTagValueNode.php @@ -0,0 +1,35 @@ +type = $type; + $this->parameterName = $parameterName; + $this->description = $description; + } + + public function __toString(): string + { + return trim("{$this->type} {$this->parameterName} {$this->description}"); + } + +} diff --git a/src/Ast/PhpDoc/PhpDocNode.php b/src/Ast/PhpDoc/PhpDocNode.php index b3b68811..ade55b78 100644 --- a/src/Ast/PhpDoc/PhpDocNode.php +++ b/src/Ast/PhpDoc/PhpDocNode.php @@ -118,6 +118,20 @@ static function (PhpDocTagValueNode $value): bool { } + /** + * @return ParamClosureThisTagValueNode[] + */ + public function getParamClosureThisTagValues(string $tagName = '@param-closure-this'): array + { + return array_filter( + array_column($this->getTagsByName($tagName), 'value'), + static function (PhpDocTagValueNode $value): bool { + return $value instanceof ParamClosureThisTagValueNode; + } + ); + } + + /** * @return TemplateTagValueNode[] */ diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index 9431fb6f..d7678a3d 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -397,6 +397,11 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph $tagValue = $this->parseParamLaterInvokedCallableTagValue($tokens); break; + case '@param-closure-this': + case '@phpstan-param-closure-this': + $tagValue = $this->parseParamClosureThisTagValue($tokens); + break; + case '@var': case '@phpstan-var': case '@psalm-var': @@ -889,6 +894,16 @@ private function parseParamLaterInvokedCallableTagValue(TokenIterator $tokens): } + private function parseParamClosureThisTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamClosureThisTagValueNode + { + $type = $this->typeParser->parse($tokens); + $parameterName = $this->parseRequiredVariableName($tokens); + $description = $this->parseOptionalDescription($tokens); + + return new Ast\PhpDoc\ParamClosureThisTagValueNode($type, $parameterName, $description); + } + + private function parseVarTagValue(TokenIterator $tokens): Ast\PhpDoc\VarTagValueNode { $type = $this->typeParser->parse($tokens); diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index 837c9e58..b1769932 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -20,6 +20,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamClosureThisTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamOutTagValueNode; @@ -312,6 +313,9 @@ private function printTagValue(PhpDocTagValueNode $node): string if ($node instanceof ParamLaterInvokedCallableTagValueNode) { return trim("{$node->parameterName} {$node->description}"); } + if ($node instanceof ParamClosureThisTagValueNode) { + return trim("{$node->type} {$node->parameterName} {$node->description}"); + } if ($node instanceof PropertyTagValueNode) { $type = $this->printType($node->type); return trim("{$type} {$node->propertyName} {$node->description}"); diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 8d0ce734..8085ec64 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -29,6 +29,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamClosureThisTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamOutTagValueNode; @@ -102,6 +103,7 @@ protected function setUp(): void * @dataProvider provideParamImmediatelyInvokedCallableTagsData * @dataProvider provideParamLaterInvokedCallableTagsData * @dataProvider provideTypelessParamTagsData + * @dataProvider provideParamClosureThisTagsData * @dataProvider provideVarTagsData * @dataProvider provideReturnTagsData * @dataProvider provideThrowsTagsData @@ -686,6 +688,54 @@ public function provideParamLaterInvokedCallableTagsData(): Iterator ]; } + public function provideParamClosureThisTagsData(): Iterator + { + yield [ + 'OK', + '/** @param-closure-this Foo $a */', + new PhpDocNode([ + new PhpDocTagNode( + '@param-closure-this', + new ParamClosureThisTagValueNode( + new IdentifierTypeNode('Foo'), + '$a', + '' + ) + ), + ]), + ]; + + yield [ + 'OK with prefix', + '/** @phpstan-param-closure-this Foo $a */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-param-closure-this', + new ParamClosureThisTagValueNode( + new IdentifierTypeNode('Foo'), + '$a', + '' + ) + ), + ]), + ]; + + yield [ + 'OK with description', + '/** @param-closure-this Foo $a test */', + new PhpDocNode([ + new PhpDocTagNode( + '@param-closure-this', + new ParamClosureThisTagValueNode( + new IdentifierTypeNode('Foo'), + '$a', + 'test' + ) + ), + ]), + ]; + } + public function provideVarTagsData(): Iterator { yield [ @@ -7185,6 +7235,7 @@ public function testReturnTypeLinesAndIndexes(string $phpDoc, array $lines): voi * @dataProvider provideTypelessParamTagsData * @dataProvider provideParamImmediatelyInvokedCallableTagsData * @dataProvider provideParamLaterInvokedCallableTagsData + * @dataProvider provideParamClosureThisTagsData * @dataProvider provideVarTagsData * @dataProvider provideReturnTagsData * @dataProvider provideThrowsTagsData diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index ddbe4310..25a268ee 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -17,6 +17,7 @@ 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; @@ -1703,6 +1704,25 @@ public function enterNode(Node $node) }, ]; + + yield [ + '/** @param-closure-this Foo $test haha */', + '/** @param-closure-this Bar $taste hehe */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ParamClosureThisTagValueNode) { + $node->type = new IdentifierTypeNode('Bar'); + $node->parameterName = '$taste'; + $node->description = 'hehe'; + } + + return $node; + } + + }, + ]; } /** From cd06d6b1a1b3c75b0b83f97577869fd85a3cd4fb Mon Sep 17 00:00:00 2001 From: Brad Jorsch Date: Wed, 3 Apr 2024 14:28:40 -0400 Subject: [PATCH 21/26] Add `@phan-` prefixes for recognized doc tags The following prefixes are added: * `@phan-assert` * `@phan-assert-if-false` * `@phan-assert-if-true` * `@phan-extends` * `@phan-inherits` * `@phan-method` * `@phan-mixin` * `@phan-param` * `@phan-property` * `@phan-property-read` * `@phan-property-write` * `@phan-real-return` * `@phan-return` * `@phan-template` * `@phan-type` * `@phan-var` No changes to any of the parsing were made, as the syntaxes seem to match what's already done for the existing unprefixed tags and/or the existing `@psalm-` and `@phpstan-` prefixed tags. --- src/Parser/PhpDocParser.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index d7678a3d..e46e0941 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -384,6 +384,7 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph case '@param': case '@phpstan-param': case '@psalm-param': + case '@phan-param': $tagValue = $this->parseParamTagValue($tokens); break; @@ -405,12 +406,15 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph case '@var': case '@phpstan-var': case '@psalm-var': + case '@phan-var': $tagValue = $this->parseVarTagValue($tokens); break; case '@return': case '@phpstan-return': case '@psalm-return': + case '@phan-return': + case '@phan-real-return': $tagValue = $this->parseReturnTagValue($tokens); break; @@ -420,6 +424,7 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph break; case '@mixin': + case '@phan-mixin': $tagValue = $this->parseMixinTagValue($tokens); break; @@ -446,18 +451,23 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph case '@psalm-property': case '@psalm-property-read': case '@psalm-property-write': + case '@phan-property': + case '@phan-property-read': + case '@phan-property-write': $tagValue = $this->parsePropertyTagValue($tokens); break; case '@method': case '@phpstan-method': case '@psalm-method': + case '@phan-method': $tagValue = $this->parseMethodTagValue($tokens); break; case '@template': case '@phpstan-template': case '@psalm-template': + case '@phan-template': case '@template-covariant': case '@phpstan-template-covariant': case '@psalm-template-covariant': @@ -474,6 +484,8 @@ function ($tokens) { case '@extends': case '@phpstan-extends': + case '@phan-extends': + case '@phan-inherits': case '@template-extends': $tagValue = $this->parseExtendsTagValue('@extends', $tokens); break; @@ -492,6 +504,7 @@ function ($tokens) { case '@phpstan-type': case '@psalm-type': + case '@phan-type': $tagValue = $this->parseTypeAliasTagValue($tokens); break; @@ -506,6 +519,9 @@ function ($tokens) { case '@psalm-assert': case '@psalm-assert-if-true': case '@psalm-assert-if-false': + case '@phan-assert': + case '@phan-assert-if-true': + case '@phan-assert-if-false': $tagValue = $this->parseAssertTagValue($tokens); break; @@ -1091,7 +1107,7 @@ private function parseTypeAliasTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeA $alias = $tokens->currentTokenValue(); $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); - // support psalm-type syntax + // support phan-type/psalm-type syntax $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL); if ($this->preserveTypeAliasesWithInvalidTypes) { From 793d14628dd0dea6b9285aa9fd49878e7f78e3fd Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 4 Apr 2024 09:34:24 +0200 Subject: [PATCH 22/26] Update phpdoc-parser in phpstan-src:1.11.x --- .github/workflows/send-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/send-pr.yml b/.github/workflows/send-pr.yml index 37b2a974..61a65267 100644 --- a/.github/workflows/send-pr.yml +++ b/.github/workflows/send-pr.yml @@ -23,7 +23,7 @@ jobs: repository: phpstan/phpstan-src path: phpstan-src token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - ref: 1.10.x + ref: 1.11.x - name: "Install dependencies" working-directory: ./phpstan-src From 54b191f409690206aed725a1c43fb450e499d4e0 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 5 Apr 2024 10:04:25 +0200 Subject: [PATCH 23/26] Test pure-callable and pure-Closure --- tests/PHPStan/Parser/TypeParserTest.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index f4d656dd..babaf9d1 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -768,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( From 9bb3855466b6c08936643e86a52cfb9cc072e52d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Sat, 20 Apr 2024 08:39:11 +0200 Subject: [PATCH 24/26] Update lock-closed-issues.yml --- .github/workflows/lock-closed-issues.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml index c2b017b9..69545301 100644 --- a/.github/workflows/lock-closed-issues.yml +++ b/.github/workflows/lock-closed-issues.yml @@ -2,7 +2,7 @@ name: 'Lock Issues' on: schedule: - - cron: '0 0 * * *' + - cron: '8 0 * * *' jobs: lock: From 536889f2b340489d328f5ffb7b02bb6b183ddedc Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Mon, 6 May 2024 14:04:23 +0200 Subject: [PATCH 25/26] Support array and offset access on const types --- src/Ast/Type/OffsetAccessTypeNode.php | 1 - src/Parser/TypeParser.php | 26 +++++++++++++++++-------- src/Printer/Printer.php | 2 -- tests/PHPStan/Parser/TypeParserTest.php | 7 +++++++ tests/PHPStan/Printer/PrinterTest.php | 24 +++++++++++++++++++++++ 5 files changed, 49 insertions(+), 11 deletions(-) diff --git a/src/Ast/Type/OffsetAccessTypeNode.php b/src/Ast/Type/OffsetAccessTypeNode.php index 39e83dfe..c27ec0a3 100644 --- a/src/Ast/Type/OffsetAccessTypeNode.php +++ b/src/Ast/Type/OffsetAccessTypeNode.php @@ -25,7 +25,6 @@ public function __toString(): string { if ( $this->type instanceof CallableTypeNode - || $this->type instanceof ConstTypeNode || $this->type instanceof NullableTypeNode ) { return '(' . $this->type . ')[' . $this->offset . ']'; diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index ebc2fbab..5669fe45 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -232,7 +232,17 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode ); } - return $this->enrichWithAttributes($tokens, new Ast\Type\ConstTypeNode($constExpr), $startLine, $startIndex); + $type = $this->enrichWithAttributes( + $tokens, + new Ast\Type\ConstTypeNode($constExpr), + $startLine, + $startIndex + ); + if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { + $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); + } + + return $type; } catch (LogicException $e) { throw new ParserException( $currentTokenValue, @@ -733,14 +743,14 @@ private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNo ); } - $type = new Ast\Type\ConstTypeNode($constExpr); + $type = $this->enrichWithAttributes( + $tokens, + new Ast\Type\ConstTypeNode($constExpr), + $startLine, + $startIndex + ); if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { - $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes( - $tokens, - $type, - $startLine, - $startIndex - )); + $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); } return $type; diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index b1769932..044d07f8 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -141,7 +141,6 @@ final class Printer CallableTypeNode::class, UnionTypeNode::class, IntersectionTypeNode::class, - ConstTypeNode::class, NullableTypeNode::class, ], ]; @@ -512,7 +511,6 @@ private function printOffsetAccessType(TypeNode $type): string $type instanceof CallableTypeNode || $type instanceof UnionTypeNode || $type instanceof IntersectionTypeNode - || $type instanceof ConstTypeNode || $type instanceof NullableTypeNode ) { return $this->wrapInParentheses($type); diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index babaf9d1..d6c66bb8 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -1067,6 +1067,13 @@ public function provideParseData(): array new IdentifierTypeNode('int') ), ], + [ + 'self::TYPES[ int ]', + new OffsetAccessTypeNode( + new ConstTypeNode(new ConstFetchNode('self', 'TYPES')), + new IdentifierTypeNode('int') + ), + ], [ "?\t\xA009", // edge-case with \h new NullableTypeNode( diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 25a268ee..d73481e2 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -1723,6 +1723,23 @@ public function enterNode(Node $node) }, ]; + + yield [ + '/** @return Foo[abc] */', + '/** @return self::FOO[abc] */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ReturnTagValueNode && $node->type instanceof OffsetAccessTypeNode) { + $node->type->type = new ConstTypeNode(new ConstFetchNode('self', 'FOO')); + } + + return $node; + } + + }, + ]; } /** @@ -1862,6 +1879,13 @@ public function dataPrintType(): iterable ]), 'Foo|Bar|(Baz|Lorem)', ]; + yield [ + new OffsetAccessTypeNode( + new ConstTypeNode(new ConstFetchNode('self', 'TYPES')), + new IdentifierTypeNode('int') + ), + 'self::TYPES[int]', + ]; } /** From fcaefacf2d5c417e928405b71b400d4ce10daaf4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 31 May 2024 10:45:45 +0200 Subject: [PATCH 26/26] `TemplateTagValueNode` name cannot be empty string --- src/Ast/PhpDoc/TemplateTagValueNode.php | 5 ++++- src/Parser/TypeParser.php | 4 ++++ tests/PHPStan/Printer/PrinterTest.php | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Ast/PhpDoc/TemplateTagValueNode.php b/src/Ast/PhpDoc/TemplateTagValueNode.php index 1d3c70e4..78b311ee 100644 --- a/src/Ast/PhpDoc/TemplateTagValueNode.php +++ b/src/Ast/PhpDoc/TemplateTagValueNode.php @@ -11,7 +11,7 @@ class TemplateTagValueNode implements PhpDocTagValueNode use NodeAttributes; - /** @var string */ + /** @var non-empty-string */ public $name; /** @var TypeNode|null */ @@ -23,6 +23,9 @@ class TemplateTagValueNode implements PhpDocTagValueNode /** @var string (may be empty) */ public $description; + /** + * @param non-empty-string $name + */ public function __construct(string $name, ?TypeNode $bound, string $description, ?TypeNode $default = null) { $this->name = $name; diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 5669fe45..2e404655 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -510,6 +510,10 @@ public function parseTemplateTagValue( $description = ''; } + if ($name === '') { + throw new LogicException('Template tag name cannot be empty.'); + } + return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description, $default); } diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index d73481e2..07d68af0 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -1792,7 +1792,7 @@ public function enterNode(Node $node) } /** - * @return iterable + * @return iterable */ public function dataPrintType(): iterable { @@ -1905,7 +1905,7 @@ public function testPrintType(TypeNode $node, string $expectedResult): void } /** - * @return iterable + * @return iterable */ public function dataPrintPhpDocNode(): iterable {