From f23cd900352f3b6b53027ca170bb98088562de29 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Sun, 17 Sep 2023 10:23:01 -0700 Subject: [PATCH 1/2] support comments in array shape use older syntax linting + stanning revert missing file remove typehint lint stan stan comments as attributes revert some stuff revert some stuff missing file styles fix make public, change const name restore tests undo multiline ignore single lines comments preceded by : add comments to arrayshapeitems, not arrayshape whoops fix various things support comment after comma add test fix order remove unused remove unused restore start on format-preserving printer comment support remove addComments support multiline comments in add flow remove comments to the right stuff support changing of comments support editing of comments delayed add fix tests remove comments handle multiple comments clean up lint lint add nowdoc stanning linting, stanning add visibility use nowdoc in a few more places set up test for adding comment to object lint remove debug thing gather comments in TokenIterator move comment handling to TokenIterator use nowdoc add consumeAll flag add more tests for array shape comments add test file for object Allow asserting the type of `$this` Update actions/checkout action to v4 Make the CI job pass after upgrading PHPStan to v1.10.34 simplify/unify parseGeneric method fix/unify callable template parsing with EOL Allow conditional type in callable return type fix template Revert "fix template" This reverts commit 655d9687919c28892f48afc90c031c53059f6cd9. restore baseline add tests for comments on callable introduce tryConsumeTokenTypeAll single-line comments only simplify getReformattedText lint linting lint --- .github/workflows/apiref.yml | 2 +- .github/workflows/backward-compatibility.yml | 2 +- .github/workflows/build.yml | 10 +- .github/workflows/create-tag.yml | 2 +- .github/workflows/merge-maintained-branch.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/send-pr.yml | 2 +- .../test-slevomat-coding-standard.yml | 4 +- phpstan-baseline.neon | 5 - src/Ast/Attribute.php | 2 + src/Ast/Comment.php | 31 + src/Lexer/Lexer.php | 4 + src/Parser/PhpDocParser.php | 4 +- src/Parser/TokenIterator.php | 45 +- src/Parser/TypeParser.php | 84 +- src/Printer/Printer.php | 84 +- tests/PHPStan/Parser/PhpDocParserTest.php | 57 +- tests/PHPStan/Parser/TypeParserTest.php | 105 +- ...intArrayShapeWithSingleLineCommentTest.php | 241 ++++ ...PrintCallableWithSingleLineCommentTest.php | 143 +++ .../PrintObjectWithSingleLineCommentTest.php | 229 ++++ tests/PHPStan/Printer/PrinterTest.php | 1101 ++++++++++------- tests/PHPStan/Printer/PrinterTestBase.php | 120 ++ 23 files changed, 1730 insertions(+), 551 deletions(-) create mode 100644 src/Ast/Comment.php create mode 100644 tests/PHPStan/Printer/PrintArrayShapeWithSingleLineCommentTest.php create mode 100644 tests/PHPStan/Printer/PrintCallableWithSingleLineCommentTest.php create mode 100644 tests/PHPStan/Printer/PrintObjectWithSingleLineCommentTest.php create mode 100644 tests/PHPStan/Printer/PrinterTestBase.php diff --git a/.github/workflows/apiref.yml b/.github/workflows/apiref.yml index d3d55f98..85f91693 100644 --- a/.github/workflows/apiref.yml +++ b/.github/workflows/apiref.yml @@ -18,7 +18,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/.github/workflows/backward-compatibility.yml b/.github/workflows/backward-compatibility.yml index eb78a350..213da72c 100644 --- a/.github/workflows/backward-compatibility.yml +++ b/.github/workflows/backward-compatibility.yml @@ -17,7 +17,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 61dd4466..d356f34e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -53,10 +53,10 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Checkout build-cs" - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: "phpstan/build-cs" path: "build-cs" @@ -104,7 +104,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -144,7 +144,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml index 8452d986..a8535014 100644 --- a/.github/workflows/create-tag.yml +++ b/.github/workflows/create-tag.yml @@ -21,7 +21,7 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.PHPSTAN_BOT_TOKEN }} diff --git a/.github/workflows/merge-maintained-branch.yml b/.github/workflows/merge-maintained-branch.yml index 3aa2b0b3..18d17974 100644 --- a/.github/workflows/merge-maintained-branch.yml +++ b/.github/workflows/merge-maintained-branch.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Merge branch" uses: everlytic/branch-merge@1.1.5 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 92b72547..e4a8ac62 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Generate changelog id: changelog diff --git a/.github/workflows/send-pr.yml b/.github/workflows/send-pr.yml index bc305f97..023293c7 100644 --- a/.github/workflows/send-pr.yml +++ b/.github/workflows/send-pr.yml @@ -18,7 +18,7 @@ jobs: php-version: "8.1" - name: "Checkout phpstan-src" - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: phpstan/phpstan-src path: phpstan-src diff --git a/.github/workflows/test-slevomat-coding-standard.yml b/.github/workflows/test-slevomat-coding-standard.yml index fd03d240..fb266231 100644 --- a/.github/workflows/test-slevomat-coding-standard.yml +++ b/.github/workflows/test-slevomat-coding-standard.yml @@ -25,10 +25,10 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Checkout Slevomat Coding Standard" - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: slevomat/coding-standard path: slevomat-cs diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 04100fcd..4596cc77 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -10,11 +10,6 @@ parameters: count: 1 path: src/Ast/NodeTraverser.php - - - message: "#^Strict comparison using \\=\\=\\= between 2 and 2 will always evaluate to true\\.$#" - count: 2 - path: src/Ast/NodeTraverser.php - - message: "#^Variable property access on PHPStan\\\\PhpDocParser\\\\Ast\\\\Node\\.$#" count: 1 diff --git a/src/Ast/Attribute.php b/src/Ast/Attribute.php index cd3a0a29..1f770ded 100644 --- a/src/Ast/Attribute.php +++ b/src/Ast/Attribute.php @@ -13,4 +13,6 @@ final class Attribute public const ORIGINAL_NODE = 'originalNode'; + public const COMMENTS = 'comments'; + } diff --git a/src/Ast/Comment.php b/src/Ast/Comment.php new file mode 100644 index 00000000..574f8732 --- /dev/null +++ b/src/Ast/Comment.php @@ -0,0 +1,31 @@ +text = $text; + $this->startLine = $startLine; + $this->startIndex = $startIndex; + } + + public function getReformattedText(): ?string + { + return trim($this->text); + } + +} diff --git a/src/Lexer/Lexer.php b/src/Lexer/Lexer.php index 32539faf..3f06ef53 100644 --- a/src/Lexer/Lexer.php +++ b/src/Lexer/Lexer.php @@ -50,6 +50,8 @@ class Lexer public const TOKEN_NEGATED = 35; public const TOKEN_ARROW = 36; + public const TOKEN_COMMENT = 37; + public const TOKEN_LABELS = [ self::TOKEN_REFERENCE => '\'&\'', self::TOKEN_UNION => '\'|\'', @@ -65,6 +67,7 @@ class Lexer self::TOKEN_OPEN_CURLY_BRACKET => '\'{\'', self::TOKEN_CLOSE_CURLY_BRACKET => '\'}\'', self::TOKEN_COMMA => '\',\'', + self::TOKEN_COMMENT => '\'//\'', self::TOKEN_COLON => '\':\'', self::TOKEN_VARIADIC => '\'...\'', self::TOKEN_DOUBLE_COLON => '\'::\'', @@ -160,6 +163,7 @@ private function generateRegexp(): string self::TOKEN_CLOSE_CURLY_BRACKET => '\\}', self::TOKEN_COMMA => ',', + self::TOKEN_COMMENT => '((? '\\.\\.\\.', self::TOKEN_DOUBLE_COLON => '::', self::TOKEN_DOUBLE_ARROW => '=>', diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index c21358cf..15a2aa5c 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -1127,15 +1127,13 @@ private function parseAssertParameter(TokenIterator $tokens): array { if ($tokens->isCurrentTokenType(Lexer::TOKEN_THIS_VARIABLE)) { $parameter = '$this'; - $requirePropertyOrMethod = true; $tokens->next(); } else { $parameter = $tokens->currentTokenValue(); - $requirePropertyOrMethod = false; $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE); } - if ($requirePropertyOrMethod || $tokens->isCurrentTokenType(Lexer::TOKEN_ARROW)) { + if ($tokens->isCurrentTokenType(Lexer::TOKEN_ARROW)) { $tokens->consumeTokenType(Lexer::TOKEN_ARROW); $propertyOrMethod = $tokens->currentTokenValue(); diff --git a/src/Parser/TokenIterator.php b/src/Parser/TokenIterator.php index 9be7593d..054d7662 100644 --- a/src/Parser/TokenIterator.php +++ b/src/Parser/TokenIterator.php @@ -3,6 +3,7 @@ namespace PHPStan\PhpDocParser\Parser; use LogicException; +use PHPStan\PhpDocParser\Ast\Comment; use PHPStan\PhpDocParser\Lexer\Lexer; use function array_pop; use function assert; @@ -17,6 +18,9 @@ class TokenIterator /** @var list */ private $tokens; + /** @var array */ + private $comments = []; + /** @var int */ private $index; @@ -24,7 +28,9 @@ class TokenIterator private $savePoints = []; /** @var list */ - private $skippedTokenTypes = [Lexer::TOKEN_HORIZONTAL_WS]; + private $skippedTokenTypes = [ + Lexer::TOKEN_HORIZONTAL_WS, + Lexer::TOKEN_COMMENT]; /** @var string|null */ private $newline = null; @@ -154,8 +160,7 @@ public function consumeTokenType(int $tokenType): void } } - $this->index++; - $this->skipIrrelevantTokens(); + $this->next(); } @@ -168,8 +173,7 @@ public function consumeTokenValue(int $tokenType, string $tokenValue): void $this->throwError($tokenType, $tokenValue); } - $this->index++; - $this->skipIrrelevantTokens(); + $this->next(); } @@ -180,12 +184,30 @@ public function tryConsumeTokenValue(string $tokenValue): bool return false; } - $this->index++; - $this->skipIrrelevantTokens(); + $this->next(); return true; } + /** + * @return Comment[] + */ + public function flushComments(): array + { + $res = $this->comments; + $this->comments = []; + return $res; + } + + /** @phpstan-impure */ + public function tryConsumeTokenTypeAll(int $tokenType): bool + { + $found = false; + while ($this->tryConsumeTokenType($tokenType)) { + $found = true; + } + return $found; + } /** @phpstan-impure */ public function tryConsumeTokenType(int $tokenType): bool @@ -200,8 +222,7 @@ public function tryConsumeTokenType(int $tokenType): bool } } - $this->index++; - $this->skipIrrelevantTokens(); + $this->next(); return true; } @@ -256,6 +277,11 @@ private function skipIrrelevantTokens(): void if (!isset($this->tokens[$this->index + 1])) { break; } + + if ($this->currentTokenType() === Lexer::TOKEN_COMMENT) { + $this->comments[] = new Comment($this->currentTokenValue(), $this->currentTokenLine(), $this->currentTokenIndex()); + } + $this->index++; } } @@ -299,7 +325,6 @@ public function rollback(): void $this->index = $index; } - /** * @throws ParserException */ diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index d0b1fdea..e9491e2a 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -22,11 +22,18 @@ class TypeParser /** @var bool */ private $useLinesAttributes; + /** @var bool */ + private $useCommentsAttributes; + /** @var bool */ private $useIndexAttributes; /** - * @param array{lines?: bool, indexes?: bool} $usedAttributes + * @param array{ + * lines?: bool, + * indexes?: bool, + * comments?: bool + * } $usedAttributes */ public function __construct( ?ConstExprParser $constExprParser = null, @@ -38,6 +45,7 @@ public function __construct( $this->quoteAwareConstExprString = $quoteAwareConstExprString; $this->useLinesAttributes = $usedAttributes['lines'] ?? false; $this->useIndexAttributes = $usedAttributes['indexes'] ?? false; + $this->useCommentsAttributes = $usedAttributes['comments'] ?? false; } /** @phpstan-impure */ @@ -66,6 +74,7 @@ public function parse(TokenIterator $tokens): Ast\Type\TypeNode * @internal * @template T of Ast\Node * @param T $type + * * @return T */ public function enrichWithAttributes(TokenIterator $tokens, Ast\Node $type, int $startLine, int $startIndex): Ast\Node @@ -75,6 +84,10 @@ public function enrichWithAttributes(TokenIterator $tokens, Ast\Node $type, int $type->setAttribute(Ast\Attribute::END_LINE, $tokens->currentTokenLine()); } + if ($this->useCommentsAttributes) { + $type->setAttribute(Ast\Attribute::COMMENTS, $tokens->flushComments()); + } + if ($this->useIndexAttributes) { $type->setAttribute(Ast\Attribute::START_INDEX, $startIndex); $type->setAttribute(Ast\Attribute::END_INDEX, $tokens->endIndexOfLastRelevantToken()); @@ -310,7 +323,7 @@ private function parseConditional(TokenIterator $tokens, Ast\Type\TypeNode $subj $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); $tokens->consumeTokenType(Lexer::TOKEN_NULLABLE); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->tryConsumeTokenTypeAll(Lexer::TOKEN_PHPDOC_EOL); $ifType = $this->parse($tokens); @@ -398,42 +411,34 @@ public function isHtml(TokenIterator $tokens): bool public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $baseType): Ast\Type\GenericTypeNode { $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->tryConsumeTokenTypeAll(Lexer::TOKEN_PHPDOC_EOL); + $startLine = $baseType->getAttribute(Ast\Attribute::START_LINE); + $startIndex = $baseType->getAttribute(Ast\Attribute::START_INDEX); $genericTypes = []; $variances = []; - [$genericTypes[], $variances[]] = $this->parseGenericTypeArgument($tokens); - - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); - - while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { + $isFirst = true; + while ($isFirst || $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); - if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) { - // trailing comma case - $type = new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances); - $startLine = $baseType->getAttribute(Ast\Attribute::START_LINE); - $startIndex = $baseType->getAttribute(Ast\Attribute::START_INDEX); - if ($startLine !== null && $startIndex !== null) { - $type = $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); - } - return $type; + // trailing comma case + if (!$isFirst && $tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) { + break; } + $isFirst = false; + [$genericTypes[], $variances[]] = $this->parseGenericTypeArgument($tokens); $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); } - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); - $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); - $type = new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances); - $startLine = $baseType->getAttribute(Ast\Attribute::START_LINE); - $startIndex = $baseType->getAttribute(Ast\Attribute::START_INDEX); if ($startLine !== null && $startIndex !== null) { $type = $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); } + $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); + return $type; } @@ -477,7 +482,7 @@ private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNod $parameters[] = $this->parseCallableParameter($tokens); $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->tryConsumeTokenTypeAll(Lexer::TOKEN_PHPDOC_EOL); if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) { break; } @@ -533,7 +538,7 @@ private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNo return $this->parseNullable($tokens); } elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) { - $type = $this->parse($tokens); + $type = $this->subParse($tokens); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES); if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); @@ -748,6 +753,8 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, $items = []; $sealed = true; + $done = false; + do { $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); @@ -761,10 +768,19 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, break; } - $items[] = $this->parseArrayShapeItem($tokens); - + $item = $this->parseArrayShapeItem($tokens); + $items[] = $item; $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); - } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)); + if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { + $done = true; + } + if ($tokens->currentTokenType() !== Lexer::TOKEN_COMMENT) { + continue; + } + + $tokens->next(); + + } while (!$done); $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET); @@ -778,12 +794,17 @@ private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShape { $startLine = $tokens->currentTokenLine(); $startIndex = $tokens->currentTokenIndex(); + + // parse any comments above the item + $tokens->tryConsumeTokenTypeAll(Lexer::TOKEN_PHPDOC_EOL); + try { $tokens->pushSavePoint(); $key = $this->parseArrayShapeKey($tokens); $optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE); $tokens->consumeTokenType(Lexer::TOKEN_COLON); $value = $this->parse($tokens); + $tokens->dropSavePoint(); return $this->enrichWithAttributes( @@ -881,12 +902,19 @@ private function parseObjectShapeItem(TokenIterator $tokens): Ast\Type\ObjectSha $startLine = $tokens->currentTokenLine(); $startIndex = $tokens->currentTokenIndex(); + $tokens->tryConsumeTokenTypeAll(Lexer::TOKEN_PHPDOC_EOL); + $key = $this->parseObjectShapeKey($tokens); $optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE); $tokens->consumeTokenType(Lexer::TOKEN_COLON); $value = $this->parse($tokens); - return $this->enrichWithAttributes($tokens, new Ast\Type\ObjectShapeItemNode($key, $optional, $value), $startLine, $startIndex); + return $this->enrichWithAttributes( + $tokens, + new Ast\Type\ObjectShapeItemNode($key, $optional, $value), + $startLine, + $startIndex + ); } /** diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index d7feaf91..c278b4ff 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -4,6 +4,7 @@ use LogicException; use PHPStan\PhpDocParser\Ast\Attribute; +use PHPStan\PhpDocParser\Ast\Comment; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNode; use PHPStan\PhpDocParser\Ast\Node; @@ -59,6 +60,7 @@ use PHPStan\PhpDocParser\Parser\TokenIterator; use function array_keys; use function array_map; +use function assert; use function count; use function get_class; use function get_object_vars; @@ -67,6 +69,7 @@ use function is_array; use function preg_match_all; use function sprintf; +use function str_replace; use function strlen; use function strpos; use function trim; @@ -521,19 +524,25 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes, foreach ($diff as $i => $diffElem) { $diffType = $diffElem->type; - $newNode = $diffElem->new; - $originalNode = $diffElem->old; + $arrItem = $diffElem->new; + $origArrayItem = $diffElem->old; if ($diffType === DiffElem::TYPE_KEEP || $diffType === DiffElem::TYPE_REPLACE) { $beforeFirstKeepOrReplace = false; - if (!$newNode instanceof Node || !$originalNode instanceof Node) { + if (!$arrItem instanceof Node || !$origArrayItem instanceof Node) { return null; } - $itemStartPos = $originalNode->getAttribute(Attribute::START_INDEX); - $itemEndPos = $originalNode->getAttribute(Attribute::END_INDEX); + $itemStartPos = $origArrayItem->getAttribute(Attribute::START_INDEX); + $itemEndPos = $origArrayItem->getAttribute(Attribute::END_INDEX); if ($itemStartPos < 0 || $itemEndPos < 0 || $itemStartPos < $tokenIndex) { throw new LogicException(); } + $comments = $arrItem->getAttribute(Attribute::COMMENTS) ?? []; + $origComments = $origArrayItem->getAttribute(Attribute::COMMENTS) ?? []; + + $commentStartPos = count($origComments) > 0 ? $origComments[0]->startIndex : $itemStartPos; + assert($commentStartPos >= 0); + $result .= $originalTokens->getContentBetween($tokenIndex, $itemStartPos); if (count($delayedAdd) > 0) { @@ -543,6 +552,15 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes, if ($parenthesesNeeded) { $result .= '('; } + + if ($insertNewline) { + $delayedAddComments = $delayedAddNode->getAttribute(Attribute::COMMENTS) ?? []; + if (count($delayedAddComments) > 0) { + $result .= $this->pComments($delayedAddComments, $beforeAsteriskIndent, $afterAsteriskIndent); + $result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent); + } + } + $result .= $this->printNodeFormatPreserving($delayedAddNode, $originalTokens); if ($parenthesesNeeded) { $result .= ')'; @@ -559,14 +577,21 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes, } $parenthesesNeeded = isset($this->parenthesesListMap[$mapKey]) - && in_array(get_class($newNode), $this->parenthesesListMap[$mapKey], true) - && !in_array(get_class($originalNode), $this->parenthesesListMap[$mapKey], true); + && in_array(get_class($arrItem), $this->parenthesesListMap[$mapKey], true) + && !in_array(get_class($origArrayItem), $this->parenthesesListMap[$mapKey], true); $addParentheses = $parenthesesNeeded && !$originalTokens->hasParentheses($itemStartPos, $itemEndPos); if ($addParentheses) { $result .= '('; } - $result .= $this->printNodeFormatPreserving($newNode, $originalTokens); + if ($comments !== $origComments) { + if (count($comments) > 0) { + $result .= $this->pComments($comments, $beforeAsteriskIndent, $afterAsteriskIndent); + $result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent); + } + } + + $result .= $this->printNodeFormatPreserving($arrItem, $originalTokens); if ($addParentheses) { $result .= ')'; } @@ -576,35 +601,41 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes, if ($insertStr === null) { return null; } - if (!$newNode instanceof Node) { + if (!$arrItem instanceof Node) { return null; } - if ($insertStr === ', ' && $isMultiline) { + if ($insertStr === ', ' && $isMultiline || count($arrItem->getAttribute(Attribute::COMMENTS) ?? []) > 0) { $insertStr = ','; $insertNewline = true; } if ($beforeFirstKeepOrReplace) { // Will be inserted at the next "replace" or "keep" element - $delayedAdd[] = $newNode; + $delayedAdd[] = $arrItem; continue; } $itemEndPos = $tokenIndex - 1; if ($insertNewline) { - $result .= $insertStr . sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent); + $comments = $arrItem->getAttribute(Attribute::COMMENTS) ?? []; + $result .= $insertStr; + if (count($comments) > 0) { + $result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent); + $result .= $this->pComments($comments, $beforeAsteriskIndent, $afterAsteriskIndent); + } + $result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent); } else { $result .= $insertStr; } $parenthesesNeeded = isset($this->parenthesesListMap[$mapKey]) - && in_array(get_class($newNode), $this->parenthesesListMap[$mapKey], true); + && in_array(get_class($arrItem), $this->parenthesesListMap[$mapKey], true); if ($parenthesesNeeded) { $result .= '('; } - $result .= $this->printNodeFormatPreserving($newNode, $originalTokens); + $result .= $this->printNodeFormatPreserving($arrItem, $originalTokens); if ($parenthesesNeeded) { $result .= ')'; } @@ -612,12 +643,12 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes, $tokenIndex = $itemEndPos + 1; } elseif ($diffType === DiffElem::TYPE_REMOVE) { - if (!$originalNode instanceof Node) { + if (!$origArrayItem instanceof Node) { return null; } - $itemStartPos = $originalNode->getAttribute(Attribute::START_INDEX); - $itemEndPos = $originalNode->getAttribute(Attribute::END_INDEX); + $itemStartPos = $origArrayItem->getAttribute(Attribute::START_INDEX); + $itemEndPos = $origArrayItem->getAttribute(Attribute::END_INDEX); if ($itemStartPos < 0 || $itemEndPos < 0) { throw new LogicException(); } @@ -675,6 +706,20 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes, return $result; } + /** + * @param array $comments + */ + protected function pComments(array $comments, string $beforeAsteriskIndent, string $afterAsteriskIndent): string + { + $formattedComments = []; + + foreach ($comments as $comment) { + $formattedComments[] = str_replace("\n", "\n" . $beforeAsteriskIndent . '*' . $afterAsteriskIndent, $comment->getReformattedText() ?? ''); + } + + return implode("\n$beforeAsteriskIndent*$afterAsteriskIndent", $formattedComments); + } + /** * @param Node[] $nodes * @return array{bool, string, string} @@ -704,7 +749,7 @@ private function isMultiline(int $initialIndex, array $nodes, TokenIterator $ori $c = preg_match_all('~\n(?[\\x09\\x20]*)\*(?\\x20*)~', $allText, $matches, PREG_SET_ORDER); if ($c === 0) { - return [$isMultiline, '', '']; + return [$isMultiline, ' ', ' ']; } $before = ''; @@ -720,6 +765,9 @@ private function isMultiline(int $initialIndex, array $nodes, TokenIterator $ori $after = $match['after']; } + $before = strlen($before) === 0 ? ' ' : $before; + $after = strlen($after) === 0 ? ' ' : $after; + return [$isMultiline, $before, $after]; } diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index b448dc24..97d6cbfe 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -4563,21 +4563,56 @@ public function provideAssertTagsData(): Iterator ]; yield [ - 'invalid $this', + 'OK $this', '/** @phpstan-assert Type $this */', new PhpDocNode([ new PhpDocTagNode( '@phpstan-assert', - new InvalidTagValueNode( - 'Type $this', - new ParserException( - '*/', - Lexer::TOKEN_CLOSE_PHPDOC, - 31, - Lexer::TOKEN_ARROW, - null, - 1 - ) + new AssertTagValueNode( + new IdentifierTypeNode('Type'), + '$this', + false, + '' + ) + ), + ]), + ]; + + yield [ + 'OK $this with description', + '/** @phpstan-assert Type $this assert Type to $this */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-assert', + new AssertTagValueNode( + new IdentifierTypeNode('Type'), + '$this', + false, + 'assert Type to $this' + ) + ), + ]), + ]; + + yield [ + 'OK $this with generic type', + '/** @phpstan-assert GenericType $this */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-assert', + new AssertTagValueNode( + new GenericTypeNode( + new IdentifierTypeNode('GenericType'), + [ + new IdentifierTypeNode('T'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + ] + ), + '$this', + false, + '' ) ), ]), diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 85ae0db8..2a05996a 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -86,14 +86,12 @@ private function assertPrintedNodeViaToString(TypeNode $typeNode): void $this->assertPrintedNode($typeNode, (string) $typeNode); } - private function assertPrintedNodeViaPrinter(TypeNode $typeNode): void { $printer = new Printer(); $this->assertPrintedNode($typeNode, $printer->print($typeNode)); } - private function assertPrintedNode(TypeNode $typeNode, string $typeNodeString): void { $typeNodeTokens = new TokenIterator($this->lexer->tokenize($typeNodeString)); @@ -103,7 +101,6 @@ private function assertPrintedNode(TypeNode $typeNode, string $typeNodeString): $this->assertEquals($typeNode, $parsedAgainTypeNode); } - /** * @dataProvider provideParseData * @param TypeNode|Exception $expectedResult @@ -115,7 +112,7 @@ public function testVerifyAttributes(string $input, $expectedResult): void $this->expectExceptionMessage($expectedResult->getMessage()); } - $usedAttributes = ['lines' => true, 'indexes' => true]; + $usedAttributes = ['lines' => true, 'indexes' => true, 'comments' => true]; $typeParser = new TypeParser(new ConstExprParser(true, true, $usedAttributes), true, $usedAttributes); $tokens = new TokenIterator($this->lexer->tokenize($input)); @@ -138,6 +135,61 @@ public function testVerifyAttributes(string $input, $expectedResult): void public function provideParseData(): array { return [ + [ + 'array{ + // a is for apple + a: int, + }', + new ArrayShapeNode([ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + ]), + ], + [ + 'array{ + // a is for apple + // a is also for awesome + a: int, + }', + new ArrayShapeNode([ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + ]), + ], + [ + 'string', + new IdentifierTypeNode('string'), + ], + [ + ' string ', + new IdentifierTypeNode('string'), + ], + [ + ' ( string ) ', + new IdentifierTypeNode('string'), + ], + [ + '( ( string ) )', + new IdentifierTypeNode('string'), + ], + [ + '\\Foo\Bar\\Baz', + new IdentifierTypeNode('\\Foo\Bar\\Baz'), + ], + [ + ' \\Foo\Bar\\Baz ', + new IdentifierTypeNode('\\Foo\Bar\\Baz'), + ], + [ + ' ( \\Foo\Bar\\Baz ) ', + new IdentifierTypeNode('\\Foo\Bar\\Baz'), + ], [ 'string', new IdentifierTypeNode('string'), @@ -356,7 +408,11 @@ public function provideParseData(): array ), ], [ - 'array', + 'array< + // index with an int + int, + Foo\\Bar + >', new GenericTypeNode( new IdentifierTypeNode('array'), [ @@ -1385,7 +1441,13 @@ public function provideParseData(): array ), ], [ - '(Foo is Bar ? never : int)', + '( + Foo is Bar + ? + // never, I say + never + : + int)', new ConditionalTypeNode( new IdentifierTypeNode('Foo'), new IdentifierTypeNode('Bar'), @@ -1866,6 +1928,7 @@ public function provideParseData(): array ], [ 'object{ + // a is for apple a: int, }', new ObjectShapeNode([ @@ -1876,24 +1939,6 @@ public function provideParseData(): array ), ]), ], - [ - 'object{ - a: int, - b: string, - }', - new ObjectShapeNode([ - new ObjectShapeItemNode( - new IdentifierTypeNode('a'), - false, - new IdentifierTypeNode('int') - ), - new ObjectShapeItemNode( - new IdentifierTypeNode('b'), - false, - new IdentifierTypeNode('string') - ), - ]), - ], [ 'object{ a: int @@ -2154,6 +2199,18 @@ public function provideParseData(): array ]), ]), ], + [ + 'Closure(Container):($serviceId is class-string ? TService : mixed)', + new CallableTypeNode(new IdentifierTypeNode('Closure'), [ + new CallableTypeParameterNode(new IdentifierTypeNode('Container'), false, false, '', false), + ], new ConditionalTypeForParameterNode( + '$serviceId', + new GenericTypeNode(new IdentifierTypeNode('class-string'), [new IdentifierTypeNode('TService')], ['invariant']), + new IdentifierTypeNode('TService'), + new IdentifierTypeNode('mixed'), + false + )), + ], ]; } diff --git a/tests/PHPStan/Printer/PrintArrayShapeWithSingleLineCommentTest.php b/tests/PHPStan/Printer/PrintArrayShapeWithSingleLineCommentTest.php new file mode 100644 index 00000000..e7b483e1 --- /dev/null +++ b/tests/PHPStan/Printer/PrintArrayShapeWithSingleLineCommentTest.php @@ -0,0 +1,241 @@ + + */ + public function dataPrintArrayFormatPreservingAddFront(): iterable + { + yield [ + self::nowdoc(' + /** + * @param array{} $foo + */'), + self::nowdoc(' + /** + * @param array{float} $foo + */'), + ]; + + yield [ + self::nowdoc(' + /** + * @param array{string} $foo + */'), + self::nowdoc(' + /** + * @param array{// A fractional number + * float, + * string} $foo + */'), + ]; + + yield [ + self::nowdoc(' + /** + * @param array{ + * string,int} $foo + */'), + self::nowdoc(' + /** + * @param array{ + * // A fractional number + * float, + * string,int} $foo + */'), + ]; + + yield [ + self::nowdoc(' + /** + * @param array{ + * string, + * int + * } $foo + */'), + self::nowdoc(' + /** + * @param array{ + * // A fractional number + * float, + * string, + * int + * } $foo + */'), + ]; + } + + /** + * @dataProvider dataPrintArrayFormatPreservingAddFront + */ + public function testPrintFormatPreservingSingleLineAddFront(string $phpDoc, string $expectedResult): void + { + $visitor = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ArrayShapeNode) { + array_unshift($node->items, PrinterTestBase::withComment( + new ArrayShapeItemNode(null, false, new IdentifierTypeNode('float')), + '// A fractional number' + )); + } + + return $node; + } + + }; + + $lexer = new Lexer(true); + $tokens = new TokenIterator($lexer->tokenize($phpDoc)); + $phpDocNode = $this->phpDocParser->parse($tokens); + $cloningTraverser = new NodeTraverser([new NodeVisitor\CloningVisitor()]); + $newNodes = $cloningTraverser->traverse([$phpDocNode]); + + $changingTraverser = new NodeTraverser([$visitor]); + + /** @var PhpDocNode $newNode */ + [$newNode] = $changingTraverser->traverse($newNodes); + + $printer = new Printer(); + $actualResult = $printer->printFormatPreserving($newNode, $phpDocNode, $tokens); + $this->assertSame($expectedResult, $actualResult); + + $this->assertEquals( + $this->unsetAttributes($newNode), + $this->unsetAttributes($this->phpDocParser->parse(new TokenIterator($lexer->tokenize($actualResult)))) + ); + } + + + /** + * @return iterable + */ + public function dataPrintArrayFormatPreservingAddMiddle(): iterable + { + yield [ + self::nowdoc(' + /** + * @param array{} $foo + */'), + self::nowdoc(' + /** + * @param array{float} $foo + */'), + ]; + + yield [ + self::nowdoc(' + /** + * @param array{string} $foo + */'), + self::nowdoc(' + /** + * @param array{string, + * // A fractional number + * float} $foo + */'), + ]; + + yield [ + self::nowdoc(' + /** + * @param array{ + * string,int} $foo + */'), + self::nowdoc(' + /** + * @param array{ + * string, + * // A fractional number + * float,int} $foo + */'), + ]; + + yield [ + self::nowdoc(' + /** + * @param array{ + * string, + * int + * } $foo + */'), + self::nowdoc(' + /** + * @param array{ + * string, + * // A fractional number + * float, + * int + * } $foo + */'), + ]; + } + + /** + * @dataProvider dataPrintArrayFormatPreservingAddMiddle + */ + public function testPrintFormatPreservingSingleLineAddMiddle(string $phpDoc, string $expectedResult): void + { + $visitor = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + $newItem = PrinterTestBase::withComment( + new ArrayShapeItemNode(null, false, new IdentifierTypeNode('float')), + '// A fractional number' + ); + + if ($node instanceof ArrayShapeNode) { + if (count($node->items) === 0) { + $node->items[] = $newItem; + } else { + array_splice($node->items, 1, 0, [$newItem]); + } + } + + return $node; + } + + }; + + $lexer = new Lexer(true); + $tokens = new TokenIterator($lexer->tokenize($phpDoc)); + $phpDocNode = $this->phpDocParser->parse($tokens); + $cloningTraverser = new NodeTraverser([new NodeVisitor\CloningVisitor()]); + $newNodes = $cloningTraverser->traverse([$phpDocNode]); + + $changingTraverser = new NodeTraverser([$visitor]); + + /** @var PhpDocNode $newNode */ + [$newNode] = $changingTraverser->traverse($newNodes); + + $printer = new Printer(); + $actualResult = $printer->printFormatPreserving($newNode, $phpDocNode, $tokens); + $this->assertSame($expectedResult, $actualResult); + + $this->assertEquals( + $this->unsetAttributes($newNode), + $this->unsetAttributes($this->phpDocParser->parse(new TokenIterator($lexer->tokenize($actualResult)))) + ); + } + +} diff --git a/tests/PHPStan/Printer/PrintCallableWithSingleLineCommentTest.php b/tests/PHPStan/Printer/PrintCallableWithSingleLineCommentTest.php new file mode 100644 index 00000000..d253d85e --- /dev/null +++ b/tests/PHPStan/Printer/PrintCallableWithSingleLineCommentTest.php @@ -0,0 +1,143 @@ + + */ + public function dataAddCommentToParamsFront(): iterable + { + yield [ + self::nowdoc(' + /** + * @param callable(Bar $bar): int $a + */'), + self::nowdoc(' + /** + * @param callable(// never pet a burning dog + * Foo $foo, + * Bar $bar): int $a + */'), + ]; + } + + /** + * @dataProvider dataAddCommentToParamsFront + */ + public function testAddCommentToParamsFront(string $phpDoc, string $expectedResult): void + { + $visitor = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof CallableTypeNode) { + array_unshift($node->parameters, PrinterTestBase::withComment( + new CallableTypeParameterNode(new IdentifierTypeNode('Foo'), false, false, '$foo', false), + '// never pet a burning dog' + )); + } + + return $node; + } + + }; + + $lexer = new Lexer(true); + $tokens = new TokenIterator($lexer->tokenize($phpDoc)); + $phpDocNode = $this->phpDocParser->parse($tokens); + $cloningTraverser = new NodeTraverser([new NodeVisitor\CloningVisitor()]); + $newNodes = $cloningTraverser->traverse([$phpDocNode]); + + $changingTraverser = new NodeTraverser([$visitor]); + + /** @var PhpDocNode $newNode */ + [$newNode] = $changingTraverser->traverse($newNodes); + + $printer = new Printer(); + $actualResult = $printer->printFormatPreserving($newNode, $phpDocNode, $tokens); + $this->assertSame($expectedResult, $actualResult); + + $this->assertEquals( + $this->unsetAttributes($newNode), + $this->unsetAttributes($this->phpDocParser->parse(new TokenIterator($lexer->tokenize($actualResult)))) + ); + } + + + /** + * @return iterable + */ + public function dataPrintArrayFormatPreservingAddMiddle(): iterable + { + yield [ + self::nowdoc(' + /** + * @param callable(Foo $foo): int $a + */'), + self::nowdoc(' + /** + * @param callable(Foo $foo, + * // never pet a burning dog + * Bar $bar): int $a + */'), + ]; + } + + /** + * @dataProvider dataPrintArrayFormatPreservingAddMiddle + */ + public function testPrintFormatPreservingSingleLineAddMiddle(string $phpDoc, string $expectedResult): void + { + $visitor = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof CallableTypeNode) { + $node->parameters[] = PrinterTestBase::withComment( + new CallableTypeParameterNode(new IdentifierTypeNode('Bar'), false, false, '$bar', false), + '// never pet a burning dog' + ); + } + + return $node; + } + + }; + + $lexer = new Lexer(true); + $tokens = new TokenIterator($lexer->tokenize($phpDoc)); + $phpDocNode = $this->phpDocParser->parse($tokens); + $cloningTraverser = new NodeTraverser([new NodeVisitor\CloningVisitor()]); + $newNodes = $cloningTraverser->traverse([$phpDocNode]); + + $changingTraverser = new NodeTraverser([$visitor]); + + /** @var PhpDocNode $newNode */ + [$newNode] = $changingTraverser->traverse($newNodes); + + $printer = new Printer(); + $actualResult = $printer->printFormatPreserving($newNode, $phpDocNode, $tokens); + $this->assertSame($expectedResult, $actualResult); + + $this->assertEquals( + $this->unsetAttributes($newNode), + $this->unsetAttributes($this->phpDocParser->parse(new TokenIterator($lexer->tokenize($actualResult)))) + ); + } + +} diff --git a/tests/PHPStan/Printer/PrintObjectWithSingleLineCommentTest.php b/tests/PHPStan/Printer/PrintObjectWithSingleLineCommentTest.php new file mode 100644 index 00000000..7e54afa4 --- /dev/null +++ b/tests/PHPStan/Printer/PrintObjectWithSingleLineCommentTest.php @@ -0,0 +1,229 @@ + + */ + public function dataPrintArrayFormatPreservingAddFront(): iterable + { + yield [ + self::nowdoc(' + /** + * @param object{bar: string} $foo + */'), + self::nowdoc(' + /** + * @param object{// A fractional number + * foo: float, + * bar: string} $foo + */'), + ]; + + yield [ + self::nowdoc(' + /** + * @param object{ + * bar:string,naz:int} $foo + */'), + self::nowdoc(' + /** + * @param object{ + * // A fractional number + * foo: float, + * bar:string,naz:int} $foo + */'), + ]; + + yield [ + self::nowdoc(' + /** + * @param object{ + * bar:string, + * naz:int + * } $foo + */'), + self::nowdoc(' + /** + * @param object{ + * // A fractional number + * foo: float, + * bar:string, + * naz:int + * } $foo + */'), + ]; + } + + /** + * @dataProvider dataPrintArrayFormatPreservingAddFront + */ + public function testPrintFormatPreservingSingleLineAddFront(string $phpDoc, string $expectedResult): void + { + $visitor = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ObjectShapeNode) { + array_unshift($node->items, PrinterTestBase::withComment( + new ObjectShapeItemNode(new IdentifierTypeNode('foo'), false, new IdentifierTypeNode('float')), + '// A fractional number' + )); + } + + return $node; + } + + }; + + $lexer = new Lexer(true); + $tokens = new TokenIterator($lexer->tokenize($phpDoc)); + $phpDocNode = $this->phpDocParser->parse($tokens); + $cloningTraverser = new NodeTraverser([new NodeVisitor\CloningVisitor()]); + $newNodes = $cloningTraverser->traverse([$phpDocNode]); + + $changingTraverser = new NodeTraverser([$visitor]); + + /** @var PhpDocNode $newNode */ + [$newNode] = $changingTraverser->traverse($newNodes); + + $printer = new Printer(); + $actualResult = $printer->printFormatPreserving($newNode, $phpDocNode, $tokens); + $this->assertSame($expectedResult, $actualResult); + + $this->assertEquals( + $this->unsetAttributes($newNode), + $this->unsetAttributes($this->phpDocParser->parse(new TokenIterator($lexer->tokenize($actualResult)))) + ); + } + + + /** + * @return iterable + */ + public function dataPrintObjectFormatPreservingAddMiddle(): iterable + { + yield [ + self::nowdoc(' + /** + * @param object{} $foo + */'), + self::nowdoc(' + /** + * @param object{bar: float} $foo + */'), + ]; + + yield [ + self::nowdoc(' + /** + * @param object{foo:string} $foo + */'), + self::nowdoc(' + /** + * @param object{foo:string, + * // A fractional number + * bar: float} $foo + */'), + ]; + + yield [ + self::nowdoc(' + /** + * @param object{ + * foo:string,naz:int} $foo + */'), + self::nowdoc(' + /** + * @param object{ + * foo:string, + * // A fractional number + * bar: float,naz:int} $foo + */'), + ]; + + yield [ + self::nowdoc(' + /** + * @param object{ + * foo:string, + * naz:int + * } $foo + */'), + self::nowdoc(' + /** + * @param object{ + * foo:string, + * // A fractional number + * bar: float, + * naz:int + * } $foo + */'), + ]; + } + + /** + * @dataProvider dataPrintObjectFormatPreservingAddMiddle + */ + public function testPrintFormatPreservingSingleLineAddMiddle(string $phpDoc, string $expectedResult): void + { + $visitor = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ObjectShapeNode) { + $newItem = PrinterTestBase::withComment( + new ObjectShapeItemNode(new IdentifierTypeNode('bar'), false, new IdentifierTypeNode('float')), + '// A fractional number' + ); + if (count($node->items) === 0) { + $node->items[] = $newItem; + } else { + array_splice($node->items, 1, 0, [$newItem]); + } + } + + return $node; + } + + }; + + $lexer = new Lexer(true); + $tokens = new TokenIterator($lexer->tokenize($phpDoc)); + $phpDocNode = $this->phpDocParser->parse($tokens); + $cloningTraverser = new NodeTraverser([new NodeVisitor\CloningVisitor()]); + $newNodes = $cloningTraverser->traverse([$phpDocNode]); + + $changingTraverser = new NodeTraverser([$visitor]); + + /** @var PhpDocNode $newNode */ + [$newNode] = $changingTraverser->traverse($newNodes); + + $printer = new Printer(); + $actualResult = $printer->printFormatPreserving($newNode, $phpDocNode, $tokens); + $this->assertSame($expectedResult, $actualResult); + + $this->assertEquals( + $this->unsetAttributes($newNode), + $this->unsetAttributes($this->phpDocParser->parse(new TokenIterator($lexer->tokenize($actualResult)))) + ); + } + +} diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 746ad027..1c0dd6a9 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -4,6 +4,7 @@ use PHPStan\PhpDocParser\Ast\AbstractNodeVisitor; use PHPStan\PhpDocParser\Ast\Attribute; +use PHPStan\PhpDocParser\Ast\Comment; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayItemNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; @@ -39,11 +40,7 @@ use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; use PHPStan\PhpDocParser\Lexer\Lexer; -use PHPStan\PhpDocParser\Parser\ConstExprParser; -use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; -use PHPStan\PhpDocParser\Parser\TypeParser; -use PHPUnit\Framework\TestCase; use function array_pop; use function array_splice; use function array_unshift; @@ -51,30 +48,9 @@ use function count; use const PHP_EOL; -class PrinterTest extends TestCase +class PrinterTest extends PrinterTestBase { - /** @var TypeParser */ - private $typeParser; - - /** @var PhpDocParser */ - private $phpDocParser; - - protected function setUp(): void - { - $usedAttributes = ['lines' => true, 'indexes' => true]; - $constExprParser = new ConstExprParser(true, true, $usedAttributes); - $this->typeParser = new TypeParser($constExprParser, true, $usedAttributes); - $this->phpDocParser = new PhpDocParser( - $this->typeParser, - $constExprParser, - true, - true, - $usedAttributes, - true - ); - } - /** * @return iterable */ @@ -93,12 +69,14 @@ public function dataPrintFormatPreserving(): iterable $noopVisitor, ]; yield [ - '/** - * @param Foo $foo - */', - '/** - * @param Foo $foo - */', + self::nowdoc(' + /** + * @param Foo $foo + */'), + self::nowdoc(' + /** + * @param Foo $foo + */'), $noopVisitor, ]; @@ -140,33 +118,39 @@ public function enterNode(Node $node) ]; yield [ - '/** - * @param Foo $foo - */', - '/** - */', + self::nowdoc(' + /** + * @param Foo $foo + */'), + self::nowdoc(' + /** + */'), $removeFirst, ]; yield [ - '/** - * @param Foo $foo - * @param Bar $bar - */', - '/** - * @param Bar $bar - */', + self::nowdoc(' + /** + * @param Foo $foo + * @param Bar $bar + */'), + self::nowdoc(' + /** + * @param Bar $bar + */'), $removeFirst, ]; yield [ - '/** - * @param Foo $foo - * @param Bar $bar - */', - '/** - * @param Bar $bar - */', + self::nowdoc(' + /** + * @param Foo $foo + * @param Bar $bar + */'), + self::nowdoc(' + /** + * @param Bar $bar + */'), $removeFirst, ]; @@ -186,13 +170,15 @@ public function enterNode(Node $node) }; yield [ - '/** - * @param Foo $foo - * @param Bar $bar - */', - '/** - * @param Foo $foo - */', + self::nowdoc(' + /** + * @param Foo $foo + * @param Bar $bar + */'), + self::nowdoc(' + /** + * @param Foo $foo + */'), $removeLast, ]; @@ -213,39 +199,45 @@ public function enterNode(Node $node) }; yield [ - '/** - * @param Foo $foo - * @param Bar $bar - */', - '/** - * @param Foo $foo - */', + self::nowdoc(' + /** + * @param Foo $foo + * @param Bar $bar + */'), + self::nowdoc(' + /** + * @param Foo $foo + */'), $removeSecond, ]; yield [ - '/** - * @param Foo $foo - * @param Bar $bar - * @param Baz $baz - */', - '/** - * @param Foo $foo - * @param Baz $baz - */', + self::nowdoc(' + /** + * @param Foo $foo + * @param Bar $bar + * @param Baz $baz + */'), + self::nowdoc(' + /** + * @param Foo $foo + * @param Baz $baz + */'), $removeSecond, ]; yield [ - '/** - * @param Foo $foo - * @param Bar $bar - * @param Baz $baz - */', - '/** - * @param Foo $foo - * @param Baz $baz - */', + self::nowdoc(' + /** + * @param Foo $foo + * @param Bar $bar + * @param Baz $baz + */'), + self::nowdoc(' + /** + * @param Foo $foo + * @param Baz $baz + */'), $removeSecond, ]; @@ -277,58 +269,66 @@ public function enterNode(Node $node) ]; yield [ - '/** -* @return Foo -* @param Foo $foo -* @param Bar $bar -*/', - '/** -* @return Bar -* @param Foo $foo -* @param Bar $bar -*/', + self::nowdoc(' + /** + * @return Foo + * @param Foo $foo + * @param Bar $bar + */'), + self::nowdoc(' + /** + * @return Bar + * @param Foo $foo + * @param Bar $bar + */'), $changeReturnType, ]; yield [ - '/** -* @param Foo $foo -* @return Foo -* @param Bar $bar -*/', - '/** -* @param Foo $foo -* @return Bar -* @param Bar $bar -*/', + self::nowdoc(' + /** + * @param Foo $foo + * @return Foo + * @param Bar $bar + */'), + self::nowdoc(' + /** + * @param Foo $foo + * @return Bar + * @param Bar $bar + */'), $changeReturnType, ]; yield [ - '/** -* @return Foo -* @param Foo $foo -* @param Bar $bar -*/', - '/** -* @return Bar -* @param Foo $foo -* @param Bar $bar -*/', + self::nowdoc(' + /** + * @return Foo + * @param Foo $foo + * @param Bar $bar + */'), + self::nowdoc(' + /** + * @return Bar + * @param Foo $foo + * @param Bar $bar + */'), $changeReturnType, ]; yield [ - '/** -* @param Foo $foo Foo description -* @return Foo Foo return description -* @param Bar $bar Bar description -*/', - '/** -* @param Foo $foo Foo description -* @return Bar Foo return description -* @param Bar $bar Bar description -*/', + self::nowdoc(' + /** + * @param Foo $foo Foo description + * @return Foo Foo return description + * @param Bar $bar Bar description + */'), + self::nowdoc(' + /** + * @param Foo $foo Foo description + * @return Bar Foo return description + * @param Bar $bar Bar description + */'), $changeReturnType, ]; @@ -353,22 +353,26 @@ public function enterNode(Node $node) ]; yield [ - '/** - * @param Foo $foo - */', - '/** - * @param Baz $a - */', + self::nowdoc(' + /** + * @param Foo $foo + */'), + self::nowdoc(' + /** + * @param Baz $a + */'), $replaceFirst, ]; yield [ - '/** - * @param Foo $foo - */', - '/** - * @param Baz $a - */', + self::nowdoc(' + /** + * @param Foo $foo + */'), + self::nowdoc(' + /** + * @param Baz $a + */'), $replaceFirst, ]; @@ -388,24 +392,28 @@ public function enterNode(Node $node) }; yield [ - '/** - * @param Foo $foo - */', - '/** - * @param Baz $a - * @param Foo $foo - */', + self::nowdoc(' + /** + * @param Foo $foo + */'), + self::nowdoc(' + /** + * @param Baz $a + * @param Foo $foo + */'), $insertFirst, ]; yield [ - '/** - * @param Foo $foo - */', - '/** - * @param Baz $a - * @param Foo $foo - */', + self::nowdoc(' + /** + * @param Foo $foo + */'), + self::nowdoc(' + /** + * @param Baz $a + * @param Foo $foo + */'), $insertFirst, ]; @@ -427,52 +435,60 @@ public function enterNode(Node $node) }; yield [ - '/** - * @param Foo $foo - */', - '/** - * @param Foo $foo - * @param Baz $a - */', + self::nowdoc(' + /** + * @param Foo $foo + */'), + self::nowdoc(' + /** + * @param Foo $foo + * @param Baz $a + */'), $insertSecond, ]; yield [ - '/** - * @param Foo $foo - * @param Bar $bar - */', - '/** - * @param Foo $foo - * @param Baz $a - * @param Bar $bar - */', + self::nowdoc(' + /** + * @param Foo $foo + * @param Bar $bar + */'), + self::nowdoc(' + /** + * @param Foo $foo + * @param Baz $a + * @param Bar $bar + */'), $insertSecond, ]; yield [ - '/** - * @param Foo $foo - * @param Bar $bar - */', - '/** - * @param Foo $foo - * @param Baz $a - * @param Bar $bar - */', + self::nowdoc(' + /** + * @param Foo $foo + * @param Bar $bar + */'), + self::nowdoc(' + /** + * @param Foo $foo + * @param Baz $a + * @param Bar $bar + */'), $insertSecond, ]; yield [ - '/** - * @param Foo $foo - * @param Bar $bar - */', - '/** - * @param Foo $foo - * @param Baz $a - * @param Bar $bar - */', + self::nowdoc(' + /** + * @param Foo $foo + * @param Bar $bar + */'), + self::nowdoc(' + /** + * @param Foo $foo + * @param Baz $a + * @param Bar $bar + */'), $insertSecond, ]; @@ -491,24 +507,28 @@ public function enterNode(Node $node) }; yield [ - '/** - * @param Foo $foo - */', - '/** - * @param Baz $a - */', + self::nowdoc(' + /** + * @param Foo $foo + */'), + self::nowdoc(' + /** + * @param Baz $a + */'), $replaceLast, ]; yield [ - '/** - * @param Foo $foo - * @param Bar $bar - */', - '/** - * @param Foo $foo - * @param Baz $a - */', + self::nowdoc(' + /** + * @param Foo $foo + * @param Bar $bar + */'), + self::nowdoc(' + /** + * @param Foo $foo + * @param Baz $a + */'), $replaceLast, ]; @@ -526,24 +546,28 @@ public function enterNode(Node $node) }; yield [ - '/** - * @param Bar|Baz $foo - */', - '/** - * @param Foo|Bar|Baz $foo - */', + self::nowdoc(' + /** + * @param Bar|Baz $foo + */'), + self::nowdoc(' + /** + * @param Foo|Bar|Baz $foo + */'), $insertFirstTypeInUnionType, ]; yield [ - '/** - * @param Bar|Baz $foo - * @param Foo $bar - */', - '/** - * @param Foo|Bar|Baz $foo - * @param Foo $bar - */', + self::nowdoc(' + /** + * @param Bar|Baz $foo + * @param Foo $bar + */'), + self::nowdoc(' + /** + * @param Foo|Bar|Baz $foo + * @param Foo $bar + */'), $insertFirstTypeInUnionType, ]; @@ -564,12 +588,14 @@ public function enterNode(Node $node) }; yield [ - '/** - * @param Foo|Bar $bar - */', - '/** - * @param Lorem|Ipsum $bar - */', + self::nowdoc(' + /** + * @param Foo|Bar $bar + */'), + self::nowdoc(' + /** + * @param Lorem|Ipsum $bar + */'), $replaceTypesInUnionType, ]; @@ -590,12 +616,14 @@ public function enterNode(Node $node) }; yield [ - '/** - * @param callable(): void $cb - */', - '/** - * @param callable(Foo $foo, Bar $bar): void $cb - */', + self::nowdoc(' + /** + * @param callable(): void $cb + */'), + self::nowdoc(' + /** + * @param callable(Foo $foo, Bar $bar): void $cb + */'), $replaceParametersInCallableType, ]; @@ -613,12 +641,14 @@ public function enterNode(Node $node) }; yield [ - '/** - * @param callable(Foo $foo, Bar $bar): void $cb - */', - '/** - * @param callable(): void $cb - */', + self::nowdoc(' + /** + * @param callable(Foo $foo, Bar $bar): void $cb + */'), + self::nowdoc(' + /** + * @param callable(): void $cb + */'), $removeParametersInCallableType, ]; @@ -636,18 +666,20 @@ public function enterNode(Node $node) }; yield [ - '/** - * @param callable(Foo $foo, Bar $bar): void $cb - * @param callable(): void $cb2 - */', - '/** - * @param Closure(Foo $foo, Bar $bar): void $cb - * @param Closure(): void $cb2 - */', + self::nowdoc(' + /** + * @param callable(Foo $foo, Bar $bar): void $cb + * @param callable(): void $cb2 + */'), + self::nowdoc(' + /** + * @param Closure(Foo $foo, Bar $bar): void $cb + * @param Closure(): void $cb2 + */'), $changeCallableTypeIdentifier, ]; - $addItemsToArrayShape = new class extends AbstractNodeVisitor { + $addKeylessItemsToArrayShape = new class extends AbstractNodeVisitor { public function enterNode(Node $node) { @@ -663,132 +695,277 @@ public function enterNode(Node $node) }; - yield [ - '/** - * @return array{float} - */', - '/** - * @return array{float, int, string} - */', - $addItemsToArrayShape, - ]; + $addItemsWithCommentsToMultilineArrayShape = new class extends AbstractNodeVisitor { - yield [ - '/** - * @return array{float, Foo} - */', - '/** - * @return array{float, int, Foo, string} - */', - $addItemsToArrayShape, - ]; + public function enterNode(Node $node) + { + if ($node instanceof ArrayShapeNode) { + $commentedNode = new ArrayShapeItemNode(new IdentifierTypeNode('b'), false, new IdentifierTypeNode('int')); + $commentedNode->setAttribute(Attribute::COMMENTS, [new Comment('// bar')]); + array_splice($node->items, 1, 0, [ + $commentedNode, + ]); + $commentedNode = new ArrayShapeItemNode(new IdentifierTypeNode('d'), false, new IdentifierTypeNode('string')); + $commentedNode->setAttribute(Attribute::COMMENTS, [new Comment( + PrinterTest::nowdoc(' + // first comment') + )]); + $node->items[] = $commentedNode; + + $commentedNode = new ArrayShapeItemNode(new IdentifierTypeNode('e'), false, new IdentifierTypeNode('string')); + $commentedNode->setAttribute(Attribute::COMMENTS, [new Comment( + PrinterTest::nowdoc(' + // second comment') + )]); + $node->items[] = $commentedNode; + + $commentedNode = new ArrayShapeItemNode(new IdentifierTypeNode('f'), false, new IdentifierTypeNode('string')); + $commentedNode->setAttribute(Attribute::COMMENTS, [ + new Comment('// third comment'), + new Comment('// fourth comment'), + ]); + $node->items[] = $commentedNode; + } - yield [ - '/** - * @return array{ - * float, - * Foo, - * } - */', - '/** - * @return array{ - * float, - * int, - * Foo, - * string, - * } - */', - $addItemsToArrayShape, - ]; + return $node; + } - yield [ - '/** - * @return array{ - * float, - * Foo - * } - */', - '/** - * @return array{ - * float, - * int, - * Foo, - * string - * } - */', - $addItemsToArrayShape, - ]; + }; yield [ - '/** - * @return array{ - * float, - * Foo - * } - */', - '/** - * @return array{ - * float, - * int, - * Foo, - * string - * } - */', - $addItemsToArrayShape, - ]; + self::nowdoc(' + /** + * @return array{ + * // foo + * a: int, + * c: string + * } + */'), + self::nowdoc(' + /** + * @return array{ + * // foo + * a: int, + * // bar + * b: int, + * c: string, + * // first comment + * d: string, + * // second comment + * e: string, + * // third comment + * // fourth comment + * f: string + * } + */'), + $addItemsWithCommentsToMultilineArrayShape, + ]; + + $prependItemsWithCommentsToMultilineArrayShape = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ArrayShapeNode) { + $commentedNode = new ArrayShapeItemNode(new IdentifierTypeNode('a'), false, new IdentifierTypeNode('int')); + $commentedNode->setAttribute(Attribute::COMMENTS, [new Comment('// first item')]); + array_splice($node->items, 0, 0, [ + $commentedNode, + ]); + } + + return $node; + } + + }; yield [ - '/** - * @return array{ - * float, - * Foo, - * } - */', - '/** - * @return array{ - * float, - * int, - * Foo, - * string, - * } - */', - $addItemsToArrayShape, + self::nowdoc(' + /** + * @return array{ + * b: int, + * } + */'), + self::nowdoc(' + /** + * @return array{ + * // first item + * a: int, + * b: int, + * } + */'), + $prependItemsWithCommentsToMultilineArrayShape, ]; + $changeCommentOnArrayShapeItem = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ArrayShapeItemNode) { + $node->setAttribute(Attribute::COMMENTS, [new Comment('// puppies')]); + } + + return $node; + } + + }; + yield [ - '/** - * @return array{ - * float, - * Foo - * } - */', - '/** - * @return array{ - * float, - * int, - * Foo, - * string - * } - */', - $addItemsToArrayShape, + self::nowdoc(' + /** + * @return array{ + * a: int, + * } + */'), + self::nowdoc(' + /** + * @return array{ + * // puppies + * a: int, + * } + */'), + $changeCommentOnArrayShapeItem, ]; yield [ '/** - * @return array{ - * float, - * Foo - * } - */', + * @return array{float} + */', '/** - * @return array{ - * float, - * int, - * Foo, - * string - * } - */', - $addItemsToArrayShape, + * @return array{float, int, string} + */', + $addKeylessItemsToArrayShape, + ]; + + yield [ + self::nowdoc(' + /** + * @return array{float, Foo} + */'), + self::nowdoc(' + /** + * @return array{float, int, Foo, string} + */'), + $addKeylessItemsToArrayShape, + ]; + + yield [ + self::nowdoc(' + /** + * @return array{ + * float, + * Foo, + * } + */'), + self::nowdoc(' + /** + * @return array{ + * float, + * int, + * Foo, + * string, + * } + */'), + $addKeylessItemsToArrayShape, + ]; + + yield [ + self::nowdoc(' + /** + * @return array{ + * float, + * Foo + * } + */'), + self::nowdoc(' + /** + * @return array{ + * float, + * int, + * Foo, + * string + * } + */'), + $addKeylessItemsToArrayShape, + ]; + + yield [ + self::nowdoc(' + /** + * @return array{ + * float, + * Foo + * } + */'), + self::nowdoc(' + /** + * @return array{ + * float, + * int, + * Foo, + * string + * } + */'), + $addKeylessItemsToArrayShape, + ]; + + yield [ + self::nowdoc(' + /** + * @return array{ + * float, + * Foo, + * } + */'), + self::nowdoc(' + /** + * @return array{ + * float, + * int, + * Foo, + * string, + * } + */'), + $addKeylessItemsToArrayShape, + ]; + + yield [ + self::nowdoc(' + /** + * @return array{ + * float, + * Foo + * } + */'), + self::nowdoc(' + /** + * @return array{ + * float, + * int, + * Foo, + * string + * } + */'), + $addKeylessItemsToArrayShape, + ]; + + yield [ + self::nowdoc(' + /** + * @return array{ + * float, + * Foo + * } + */'), + self::nowdoc(' + /** + * @return array{ + * float, + * int, + * Foo, + * string + * } + */'), + $addKeylessItemsToArrayShape, ]; $addItemsToObjectShape = new class extends AbstractNodeVisitor { @@ -805,22 +982,73 @@ public function enterNode(Node $node) }; yield [ - '/** - * @return object{} - */', - '/** - * @return object{foo: int} - */', + self::nowdoc(' + /** + * @return object{} + */'), + self::nowdoc(' + /** + * @return object{foo: int} + */'), $addItemsToObjectShape, ]; yield [ - '/** - * @return object{bar: string} - */', - '/** - * @return object{bar: string, foo: int} - */', + self::nowdoc(' + /** + * @return object{bar: string} + */'), + self::nowdoc(' + /** + * @return object{bar: string, foo: int} + */'), + $addItemsToObjectShape, + ]; + + $addItemsWithCommentsToObjectShape = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ObjectShapeNode) { + $item = new ObjectShapeItemNode(new IdentifierTypeNode('foo'), false, new IdentifierTypeNode('int')); + $item->setAttribute(Attribute::COMMENTS, [new Comment('// favorite foo')]); + $node->items[] = $item; + } + + return $node; + } + + }; + + yield [ + self::nowdoc(' + /** + * @return object{ + * // your favorite bar + * bar: string + * } + */'), + self::nowdoc(' + /** + * @return object{ + * // your favorite bar + * bar: string, + * // favorite foo + * foo: int + * } + */'), + $addItemsWithCommentsToObjectShape, + ]; + + yield [ + self::nowdoc(' + /** + * @return object{bar: string} + */'), + self::nowdoc(' + /** + * @return object{bar: string, foo: int} + */'), $addItemsToObjectShape, ]; @@ -1008,36 +1236,42 @@ public function enterNode(Node $node) ]; yield [ - '/** - * @param int $a - */', - '/** - * @param int $bz - */', + self::nowdoc(' + /** + * @param int $a + */'), + self::nowdoc(' + /** + * @param int $bz + */'), $changeParameterName, ]; yield [ - '/** - * @param int $a - * @return string - */', - '/** - * @param int $bz - * @return string - */', + self::nowdoc(' + /** + * @param int $a + * @return string + */'), + self::nowdoc(' + /** + * @param int $bz + * @return string + */'), $changeParameterName, ]; yield [ - '/** - * @param int $a haha description - * @return string - */', - '/** - * @param int $bz haha description - * @return string - */', + self::nowdoc(' + /** + * @param int $a haha description + * @return string + */'), + self::nowdoc(' + /** + * @param int $bz haha description + * @return string + */'), $changeParameterName, ]; @@ -1073,12 +1307,14 @@ public function enterNode(Node $node) ]; yield [ - '/** - * @param int $a haha - */', - '/** - * @param int $a hehe - */', + self::nowdoc(' + /** + * @param int $a haha + */'), + self::nowdoc(' + /** + * @param int $a hehe + */'), $changeParameterDescription, ]; @@ -1096,12 +1332,14 @@ public function enterNode(Node $node) }; yield [ - '/** - * @param Foo[awesome] $a haha - */', - '/** - * @param Foo[baz] $a haha - */', + self::nowdoc(' + /** + * @param Foo[awesome] $a haha + */'), + self::nowdoc(' + /** + * @param Foo[baz] $a haha + */'), $changeOffsetAccess, ]; @@ -1119,12 +1357,14 @@ public function enterNode(Node $node) }; yield [ - '/** - * @phpstan-import-type TypeAlias from AnotherClass as DifferentAlias - */', - '/** - * @phpstan-import-type TypeAlias from AnotherClass as Ciao - */', + self::nowdoc(' + /** + * @phpstan-import-type TypeAlias from AnotherClass as DifferentAlias + */'), + self::nowdoc(' + /** + * @phpstan-import-type TypeAlias from AnotherClass as Ciao + */'), $changeTypeAliasImportAs, ]; @@ -1142,12 +1382,14 @@ public function enterNode(Node $node) }; yield [ - '/** - * @phpstan-import-type TypeAlias from AnotherClass as DifferentAlias - */', - '/** - * @phpstan-import-type TypeAlias from AnotherClass - */', + self::nowdoc(' + /** + * @phpstan-import-type TypeAlias from AnotherClass as DifferentAlias + */'), + self::nowdoc(' + /** + * @phpstan-import-type TypeAlias from AnotherClass + */'), $removeImportAs, ]; @@ -1559,15 +1801,19 @@ public function enterNode(Node $node) ]; yield [ - '/** @Foo( - * 1, - * 2, - * ) */', - '/** @Foo( - * 1, - * 2, - * 3, - * ) */', + self::nowdoc(' + /** @Foo( + * 1, + * 2, + * ) + */'), + self::nowdoc(' + /** @Foo( + * 1, + * 2, + * 3, + * ) + */'), new class extends AbstractNodeVisitor { public function enterNode(Node $node) @@ -1663,29 +1909,6 @@ public function testPrintFormatPreserving(string $phpDoc, string $expectedResult ); } - private function unsetAttributes(Node $node): Node - { - $visitor = new class extends AbstractNodeVisitor { - - public function enterNode(Node $node) - { - $node->setAttribute(Attribute::START_LINE, null); - $node->setAttribute(Attribute::END_LINE, null); - $node->setAttribute(Attribute::START_INDEX, null); - $node->setAttribute(Attribute::END_INDEX, null); - $node->setAttribute(Attribute::ORIGINAL_NODE, null); - - return $node; - } - - }; - - $traverser = new NodeTraverser([$visitor]); - - /** @var PhpDocNode */ - return $traverser->traverse([$node])[0]; - } - /** * @return iterable */ diff --git a/tests/PHPStan/Printer/PrinterTestBase.php b/tests/PHPStan/Printer/PrinterTestBase.php new file mode 100644 index 00000000..834c6c23 --- /dev/null +++ b/tests/PHPStan/Printer/PrinterTestBase.php @@ -0,0 +1,120 @@ +setAttribute(Attribute::COMMENTS, [new Comment($comment)]); + return $node; + } + + public static function nowdoc(string $str): string + { + $lines = preg_split('/\\n/', $str); + + if ($lines === false) { + return ''; + } + + if (count($lines) < 2) { + return ''; + } + + // Toss out the first line + $lines = array_slice($lines, 1, count($lines) - 1); + + // normalize any tabs to spaces + $lines = array_map(static function ($line) { + return preg_replace_callback('/(\t+)/m', static function ($matches) { + $fixed = str_repeat(' ', strlen($matches[1])); + return $fixed; + }, $line); + }, $lines); + + // take the ws from the first line and subtract them from all lines + $matches = []; + preg_match('/(^[ \t]+)/', $lines[0] ?? '', $matches); + + $numLines = count($lines); + for ($i = 0; $i < $numLines; ++$i) { + $lines[$i] = str_replace($matches[0], '', $lines[$i] ?? ''); + } + + return implode("\n", $lines); + } + + protected function setUp(): void + { + $usedAttributes = ['lines' => true, 'indexes' => true, 'comments' => true]; + $constExprParser = new ConstExprParser(true, true, $usedAttributes); + $this->typeParser = new TypeParser($constExprParser, true, $usedAttributes); + $this->phpDocParser = new PhpDocParser( + $this->typeParser, + $constExprParser, + true, + true, + $usedAttributes, + true + ); + } + + protected function unsetAttributes(Node $node): Node + { + $visitor = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + $node->setAttribute(Attribute::START_LINE, null); + $node->setAttribute(Attribute::END_LINE, null); + $node->setAttribute(Attribute::START_INDEX, null); + $node->setAttribute(Attribute::END_INDEX, null); + $node->setAttribute(Attribute::ORIGINAL_NODE, null); + $node->setAttribute(Attribute::COMMENTS, null); + + return $node; + } + + }; + + $traverser = new NodeTraverser([$visitor]); + + /** @var PhpDocNode */ + return $traverser->traverse([$node])[0]; + } + +} From 4dd3d9a23ed95c863120ac49eba5d97c2f4684f9 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 9 Oct 2023 10:33:20 +0200 Subject: [PATCH 2/2] 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 [