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..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, @@ -4563,21 +4564,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, + '' ) ), ]), @@ -5526,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 [ 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]; + } + +}