From 6301a43fe52259a692df14085d2db803cb88f089 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 01:12:22 +0000 Subject: [PATCH 01/27] Update github-actions --- .github/workflows/release-toot.yml | 2 +- .github/workflows/send-pr.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-toot.yml b/.github/workflows/release-toot.yml index 6a1c8156..1ba4fd77 100644 --- a/.github/workflows/release-toot.yml +++ b/.github/workflows/release-toot.yml @@ -10,7 +10,7 @@ jobs: toot: runs-on: ubuntu-latest steps: - - uses: cbrgm/mastodon-github-action@v1 + - uses: cbrgm/mastodon-github-action@v2 if: ${{ !github.event.repository.private }} with: # GitHub event payload diff --git a/.github/workflows/send-pr.yml b/.github/workflows/send-pr.yml index 023293c7..37b2a974 100644 --- a/.github/workflows/send-pr.yml +++ b/.github/workflows/send-pr.yml @@ -35,7 +35,7 @@ jobs: - name: "Create Pull Request" id: create-pr - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@v6 with: token: ${{ secrets.PHPSTAN_BOT_TOKEN }} path: ./phpstan-src From e7f0d8f23795321c4f2cd687d5e9c2a4ee46be9c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 21:05:35 +0000 Subject: [PATCH 02/27] Update metcalfc/changelog-generator action to v4.3.1 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2fb750a4..b1a669a9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Generate changelog id: changelog - uses: metcalfc/changelog-generator@v4.2.0 + uses: metcalfc/changelog-generator@v4.3.1 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} From c23674d80bbeaf223f51469b69773283cbdcf9bd Mon Sep 17 00:00:00 2001 From: Brad Miller <28307684+mad-briller@users.noreply.github.com> Date: Fri, 23 Feb 2024 16:03:21 +0000 Subject: [PATCH 03/27] Parse generic callables --- doc/grammars/type.abnf | 20 +++-- src/Ast/Type/CallableTypeNode.php | 13 ++- src/Parser/PhpDocParser.php | 41 +++------- src/Parser/TypeParser.php | 104 ++++++++++++++++++++++-- src/Printer/Printer.php | 8 +- tests/PHPStan/Parser/TypeParserTest.php | 99 ++++++++++++++++++++++ tests/PHPStan/Printer/PrinterTest.php | 29 +++++++ 7 files changed, 267 insertions(+), 47 deletions(-) diff --git a/doc/grammars/type.abnf b/doc/grammars/type.abnf index 0b3247ba..36118d2b 100644 --- a/doc/grammars/type.abnf +++ b/doc/grammars/type.abnf @@ -35,7 +35,13 @@ GenericTypeArgument / TokenWildcard Callable - = TokenParenthesesOpen [CallableParameters] TokenParenthesesClose TokenColon CallableReturnType + = [CallableTemplate] TokenParenthesesOpen [CallableParameters] TokenParenthesesClose TokenColon CallableReturnType + +CallableTemplate + = TokenAngleBracketOpen CallableTemplateArgument *(TokenComma CallableTemplateArgument) TokenAngleBracketClose + +CallableTemplateArgument + = TokenIdentifier [1*ByteHorizontalWs TokenOf Type] CallableParameters = CallableParameter *(TokenComma CallableParameter) @@ -192,6 +198,9 @@ TokenIs TokenNot = %s"not" 1*ByteHorizontalWs +TokenOf + = %s"of" 1*ByteHorizontalWs + TokenContravariant = %s"contravariant" 1*ByteHorizontalWs @@ -211,7 +220,7 @@ TokenIdentifier ByteHorizontalWs = %x09 ; horizontal tab - / %x20 ; space + / " " ByteNumberSign = "+" @@ -238,11 +247,8 @@ ByteIdentifierFirst / %x80-FF ByteIdentifierSecond - = %x30-39 ; 0-9 - / %x41-5A ; A-Z - / "_" - / %x61-7A ; a-z - / %x80-FF + = ByteIdentifierFirst + / %x30-39 ; 0-9 ByteSingleQuote = %x27 ; ' diff --git a/src/Ast/Type/CallableTypeNode.php b/src/Ast/Type/CallableTypeNode.php index e57e5f82..4c913198 100644 --- a/src/Ast/Type/CallableTypeNode.php +++ b/src/Ast/Type/CallableTypeNode.php @@ -3,6 +3,7 @@ namespace PHPStan\PhpDocParser\Ast\Type; use PHPStan\PhpDocParser\Ast\NodeAttributes; +use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use function implode; class CallableTypeNode implements TypeNode @@ -13,6 +14,9 @@ class CallableTypeNode implements TypeNode /** @var IdentifierTypeNode */ public $identifier; + /** @var TemplateTagValueNode[] */ + public $templateTypes; + /** @var CallableTypeParameterNode[] */ public $parameters; @@ -21,12 +25,14 @@ class CallableTypeNode implements TypeNode /** * @param CallableTypeParameterNode[] $parameters + * @param TemplateTagValueNode[] $templateTypes */ - public function __construct(IdentifierTypeNode $identifier, array $parameters, TypeNode $returnType) + public function __construct(IdentifierTypeNode $identifier, array $parameters, TypeNode $returnType, array $templateTypes = []) { $this->identifier = $identifier; $this->parameters = $parameters; $this->returnType = $returnType; + $this->templateTypes = $templateTypes; } @@ -36,8 +42,11 @@ public function __toString(): string if ($returnType instanceof self) { $returnType = "({$returnType})"; } + $template = $this->templateTypes !== [] + ? '<' . implode(', ', $this->templateTypes) . '>' + : ''; $parameters = implode(', ', $this->parameters); - return "{$this->identifier}({$parameters}): {$returnType}"; + return "{$this->identifier}{$template}({$parameters}): {$returnType}"; } } diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index e87d92c4..b6356408 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -449,7 +449,12 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph case '@template-contravariant': case '@phpstan-template-contravariant': case '@psalm-template-contravariant': - $tagValue = $this->parseTemplateTagValue($tokens, true); + $tagValue = $this->typeParser->parseTemplateTagValue( + $tokens, + function ($tokens) { + return $this->parseOptionalDescription($tokens); + } + ); break; case '@extends': @@ -947,7 +952,12 @@ private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTa do { $startLine = $tokens->currentTokenLine(); $startIndex = $tokens->currentTokenIndex(); - $templateTypes[] = $this->enrichWithAttributes($tokens, $this->parseTemplateTagValue($tokens, false), $startLine, $startIndex); + $templateTypes[] = $this->enrichWithAttributes( + $tokens, + $this->typeParser->parseTemplateTagValue($tokens), + $startLine, + $startIndex + ); } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); } @@ -1003,33 +1013,6 @@ private function parseMethodTagValueParameter(TokenIterator $tokens): Ast\PhpDoc ); } - private function parseTemplateTagValue(TokenIterator $tokens, bool $parseDescription): Ast\PhpDoc\TemplateTagValueNode - { - $name = $tokens->currentTokenValue(); - $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); - - if ($tokens->tryConsumeTokenValue('of') || $tokens->tryConsumeTokenValue('as')) { - $bound = $this->typeParser->parse($tokens); - - } else { - $bound = null; - } - - if ($tokens->tryConsumeTokenValue('=')) { - $default = $this->typeParser->parse($tokens); - } else { - $default = null; - } - - if ($parseDescription) { - $description = $this->parseOptionalDescription($tokens); - } else { - $description = ''; - } - - return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description, $default); - } - private function parseExtendsTagValue(string $tagName, TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode { $startLine = $tokens->currentTokenLine(); diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 79e70275..ebc2fbab 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -4,6 +4,7 @@ use LogicException; use PHPStan\PhpDocParser\Ast; +use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use PHPStan\PhpDocParser\Lexer\Lexer; use function in_array; use function str_replace; @@ -164,13 +165,17 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode return $type; } - $type = $this->parseGeneric($tokens, $type); + $origType = $type; + $type = $this->tryParseCallable($tokens, $type, true); + if ($type === $origType) { + $type = $this->parseGeneric($tokens, $type); - if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { - $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); + if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { + $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); + } } } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) { - $type = $this->tryParseCallable($tokens, $type); + $type = $this->tryParseCallable($tokens, $type, false); } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); @@ -464,10 +469,48 @@ public function parseGenericTypeArgument(TokenIterator $tokens): array return [$type, $variance]; } + /** + * @throws ParserException + * @param ?callable(TokenIterator): string $parseDescription + */ + public function parseTemplateTagValue( + TokenIterator $tokens, + ?callable $parseDescription = null + ): TemplateTagValueNode + { + $name = $tokens->currentTokenValue(); + $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); + + if ($tokens->tryConsumeTokenValue('of') || $tokens->tryConsumeTokenValue('as')) { + $bound = $this->parse($tokens); + + } else { + $bound = null; + } + + if ($tokens->tryConsumeTokenValue('=')) { + $default = $this->parse($tokens); + } else { + $default = null; + } + + if ($parseDescription !== null) { + $description = $parseDescription($tokens); + } else { + $description = ''; + } + + return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description, $default); + } + /** @phpstan-impure */ - private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier): Ast\Type\TypeNode + private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier, bool $hasTemplate): Ast\Type\TypeNode { + $templates = $hasTemplate + ? $this->parseCallableTemplates($tokens) + : []; + $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES); $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); @@ -492,7 +535,52 @@ private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNod $startIndex = $tokens->currentTokenIndex(); $returnType = $this->enrichWithAttributes($tokens, $this->parseCallableReturnType($tokens), $startLine, $startIndex); - return new Ast\Type\CallableTypeNode($identifier, $parameters, $returnType); + return new Ast\Type\CallableTypeNode($identifier, $parameters, $returnType, $templates); + } + + + /** + * @return Ast\PhpDoc\TemplateTagValueNode[] + * + * @phpstan-impure + */ + private function parseCallableTemplates(TokenIterator $tokens): array + { + $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); + + $templates = []; + + $isFirst = true; + while ($isFirst || $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + + // trailing comma case + if (!$isFirst && $tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) { + break; + } + $isFirst = false; + + $templates[] = $this->parseCallableTemplateArgument($tokens); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + } + + $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); + + return $templates; + } + + + private function parseCallableTemplateArgument(TokenIterator $tokens): Ast\PhpDoc\TemplateTagValueNode + { + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); + + return $this->enrichWithAttributes( + $tokens, + $this->parseTemplateTagValue($tokens), + $startLine, + $startIndex + ); } @@ -670,11 +758,11 @@ private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNo /** @phpstan-impure */ - private function tryParseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier): Ast\Type\TypeNode + private function tryParseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier, bool $hasTemplate): Ast\Type\TypeNode { try { $tokens->pushSavePoint(); - $type = $this->parseCallable($tokens, $identifier); + $type = $this->parseCallable($tokens, $identifier, $hasTemplate); $tokens->dropSavePoint(); } catch (ParserException $e) { diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index 0093e6ca..d9a060b3 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -99,6 +99,7 @@ final class Printer ArrayShapeNode::class . '->items' => ', ', ObjectShapeNode::class . '->items' => ', ', CallableTypeNode::class . '->parameters' => ', ', + CallableTypeNode::class . '->templateTypes' => ', ', GenericTypeNode::class . '->genericTypes' => ', ', ConstExprArrayNode::class . '->items' => ', ', MethodTagValueNode::class . '->parameters' => ', ', @@ -380,10 +381,15 @@ private function printType(TypeNode $node): string } else { $returnType = $this->printType($node->returnType); } + $template = $node->templateTypes !== [] + ? '<' . implode(', ', array_map(function (TemplateTagValueNode $templateNode): string { + return $this->print($templateNode); + }, $node->templateTypes)) . '>' + : ''; $parameters = implode(', ', array_map(function (CallableTypeParameterNode $parameterNode): string { return $this->print($parameterNode); }, $node->parameters)); - return "{$node->identifier}({$parameters}): {$returnType}"; + return "{$node->identifier}{$template}({$parameters}): {$returnType}"; } if ($node instanceof ConditionalTypeForParameterNode) { return sprintf( diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 0cc294f5..f4d656dd 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -10,6 +10,7 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\QuoteAwareConstExprStringNode; use PHPStan\PhpDocParser\Ast\Node; use PHPStan\PhpDocParser\Ast\NodeTraverser; +use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; @@ -897,6 +898,104 @@ public function provideParseData(): array new IdentifierTypeNode('Foo') ), ], + [ + 'callable(B): C', + new CallableTypeNode( + new IdentifierTypeNode('callable'), + [ + new CallableTypeParameterNode( + new IdentifierTypeNode('B'), + false, + false, + '', + false + ), + ], + new IdentifierTypeNode('C'), + [ + new TemplateTagValueNode('A', null, ''), + ] + ), + ], + [ + 'callable<>(): void', + new ParserException( + '>', + Lexer::TOKEN_END, + 9, + Lexer::TOKEN_IDENTIFIER + ), + ], + [ + 'Closure(T, int): (T|false)', + new CallableTypeNode( + new IdentifierTypeNode('Closure'), + [ + new CallableTypeParameterNode( + new IdentifierTypeNode('T'), + false, + false, + '', + false + ), + new CallableTypeParameterNode( + new IdentifierTypeNode('int'), + false, + false, + '', + false + ), + ], + new UnionTypeNode([ + new IdentifierTypeNode('T'), + new IdentifierTypeNode('false'), + ]), + [ + new TemplateTagValueNode('T', new IdentifierTypeNode('Model'), ''), + ] + ), + ], + [ + '\Closure(Tx, Ty): array{ Ty, Tx }', + new CallableTypeNode( + new IdentifierTypeNode('\Closure'), + [ + new CallableTypeParameterNode( + new IdentifierTypeNode('Tx'), + false, + false, + '', + false + ), + new CallableTypeParameterNode( + new IdentifierTypeNode('Ty'), + false, + false, + '', + false + ), + ], + new ArrayShapeNode([ + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('Ty') + ), + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('Tx') + ), + ]), + [ + new TemplateTagValueNode('Tx', new UnionTypeNode([ + new IdentifierTypeNode('X'), + new IdentifierTypeNode('Z'), + ]), ''), + new TemplateTagValueNode('Ty', new IdentifierTypeNode('Y'), ''), + ] + ), + ], [ '(Foo\\Bar, (int | (string & bar)[])> | Lorem)', new UnionTypeNode([ diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 2307cdb2..34a2e893 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -590,6 +590,35 @@ public function enterNode(Node $node) }; + $addCallableTemplateType = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof CallableTypeNode) { + $node->templateTypes[] = new TemplateTagValueNode( + 'T', + new IdentifierTypeNode('int'), + '' + ); + } + + return $node; + } + + }; + + yield [ + '/** @var Closure(): T */', + '/** @var Closure(): T */', + $addCallableTemplateType, + ]; + + yield [ + '/** @var \Closure(U): T */', + '/** @var \Closure(U): T */', + $addCallableTemplateType, + ]; + yield [ '/** * @param callable(): void $cb From 231e3186624c03d7e7c890ec662b81e6b0405227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Tvrd=C3=ADk?= Date: Fri, 23 Feb 2024 17:05:55 +0100 Subject: [PATCH 04/27] Improve static keyword conflict resolution in `@method` --- src/Parser/PhpDocParser.php | 18 +++++++++++------- tests/PHPStan/Parser/PhpDocParserTest.php | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index b6356408..475dd5ba 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -924,10 +924,16 @@ private function parsePropertyTagValue(TokenIterator $tokens): Ast\PhpDoc\Proper private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueNode { - $isStatic = $tokens->tryConsumeTokenValue('static'); - $startLine = $tokens->currentTokenLine(); - $startIndex = $tokens->currentTokenIndex(); - $returnTypeOrMethodName = $this->typeParser->parse($tokens); + $staticKeywordOrReturnTypeOrMethodName = $this->typeParser->parse($tokens); + + if ($staticKeywordOrReturnTypeOrMethodName instanceof Ast\Type\IdentifierTypeNode && $staticKeywordOrReturnTypeOrMethodName->name === 'static') { + $isStatic = true; + $returnTypeOrMethodName = $this->typeParser->parse($tokens); + + } else { + $isStatic = false; + $returnTypeOrMethodName = $staticKeywordOrReturnTypeOrMethodName; + } if ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) { $returnType = $returnTypeOrMethodName; @@ -935,9 +941,7 @@ private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTa $tokens->next(); } elseif ($returnTypeOrMethodName instanceof Ast\Type\IdentifierTypeNode) { - $returnType = $isStatic - ? $this->typeParser->enrichWithAttributes($tokens, new Ast\Type\IdentifierTypeNode('static'), $startLine, $startIndex) - : null; + $returnType = $isStatic ? $staticKeywordOrReturnTypeOrMethodName : null; $methodName = $returnTypeOrMethodName->name; $isStatic = false; diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 67a9d123..b8f2c0a6 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -2626,6 +2626,26 @@ public function provideMethodTagsData(): Iterator ), ]), ]; + + yield [ + 'OK non-static with return type that starts with static type', + '/** @method static|null foo() */', + new PhpDocNode([ + new PhpDocTagNode( + '@method', + new MethodTagValueNode( + false, + new UnionTypeNode([ + new IdentifierTypeNode('static'), + new IdentifierTypeNode('null'), + ]), + 'foo', + [], + '' + ) + ), + ]), + ]; } From 8ce0d65bcdd2c7f1c494746f4d1ee50d056f48fc Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 20 Mar 2024 11:05:34 +0100 Subject: [PATCH 05/27] Support for `@param-immediately-invoked-callable` and `@param-later-invoked-callable` --- ...ImmediatelyInvokedCallableTagValueNode.php | 30 ++++++++ .../ParamLaterInvokedCallableTagValueNode.php | 30 ++++++++ src/Ast/PhpDoc/PhpDocNode.php | 28 ++++++++ src/Parser/PhpDocParser.php | 28 ++++++++ src/Printer/Printer.php | 8 +++ tests/PHPStan/Parser/PhpDocParserTest.php | 68 +++++++++++++++++++ tests/PHPStan/Printer/PrinterTest.php | 38 +++++++++++ 7 files changed, 230 insertions(+) create mode 100644 src/Ast/PhpDoc/ParamImmediatelyInvokedCallableTagValueNode.php create mode 100644 src/Ast/PhpDoc/ParamLaterInvokedCallableTagValueNode.php diff --git a/src/Ast/PhpDoc/ParamImmediatelyInvokedCallableTagValueNode.php b/src/Ast/PhpDoc/ParamImmediatelyInvokedCallableTagValueNode.php new file mode 100644 index 00000000..0f480f7a --- /dev/null +++ b/src/Ast/PhpDoc/ParamImmediatelyInvokedCallableTagValueNode.php @@ -0,0 +1,30 @@ +parameterName = $parameterName; + $this->description = $description; + } + + public function __toString(): string + { + return trim("{$this->parameterName} {$this->description}"); + } + +} diff --git a/src/Ast/PhpDoc/ParamLaterInvokedCallableTagValueNode.php b/src/Ast/PhpDoc/ParamLaterInvokedCallableTagValueNode.php new file mode 100644 index 00000000..eab353f9 --- /dev/null +++ b/src/Ast/PhpDoc/ParamLaterInvokedCallableTagValueNode.php @@ -0,0 +1,30 @@ +parameterName = $parameterName; + $this->description = $description; + } + + public function __toString(): string + { + return trim("{$this->parameterName} {$this->description}"); + } + +} diff --git a/src/Ast/PhpDoc/PhpDocNode.php b/src/Ast/PhpDoc/PhpDocNode.php index 4c509f41..b3b68811 100644 --- a/src/Ast/PhpDoc/PhpDocNode.php +++ b/src/Ast/PhpDoc/PhpDocNode.php @@ -90,6 +90,34 @@ static function (PhpDocTagValueNode $value): bool { } + /** + * @return ParamImmediatelyInvokedCallableTagValueNode[] + */ + public function getParamImmediatelyInvokedCallableTagValues(string $tagName = '@param-immediately-invoked-callable'): array + { + return array_filter( + array_column($this->getTagsByName($tagName), 'value'), + static function (PhpDocTagValueNode $value): bool { + return $value instanceof ParamImmediatelyInvokedCallableTagValueNode; + } + ); + } + + + /** + * @return ParamLaterInvokedCallableTagValueNode[] + */ + public function getParamLaterInvokedCallableTagValues(string $tagName = '@param-later-invoked-callable'): array + { + return array_filter( + array_column($this->getTagsByName($tagName), 'value'), + static function (PhpDocTagValueNode $value): bool { + return $value instanceof ParamLaterInvokedCallableTagValueNode; + } + ); + } + + /** * @return TemplateTagValueNode[] */ diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index 475dd5ba..9431fb6f 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -387,6 +387,16 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph $tagValue = $this->parseParamTagValue($tokens); break; + case '@param-immediately-invoked-callable': + case '@phpstan-param-immediately-invoked-callable': + $tagValue = $this->parseParamImmediatelyInvokedCallableTagValue($tokens); + break; + + case '@param-later-invoked-callable': + case '@phpstan-param-later-invoked-callable': + $tagValue = $this->parseParamLaterInvokedCallableTagValue($tokens); + break; + case '@var': case '@phpstan-var': case '@psalm-var': @@ -861,6 +871,24 @@ private function parseParamTagValue(TokenIterator $tokens): Ast\PhpDoc\PhpDocTag } + private function parseParamImmediatelyInvokedCallableTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode + { + $parameterName = $this->parseRequiredVariableName($tokens); + $description = $this->parseOptionalDescription($tokens); + + return new Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode($parameterName, $description); + } + + + private function parseParamLaterInvokedCallableTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode + { + $parameterName = $this->parseRequiredVariableName($tokens); + $description = $this->parseOptionalDescription($tokens); + + return new Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode($parameterName, $description); + } + + private function parseVarTagValue(TokenIterator $tokens): Ast\PhpDoc\VarTagValueNode { $type = $this->typeParser->parse($tokens); diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index d9a060b3..837c9e58 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -20,6 +20,8 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamOutTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocChildNode; @@ -304,6 +306,12 @@ private function printTagValue(PhpDocTagValueNode $node): string $type = $this->printType($node->type); return trim("{$type} {$reference}{$variadic}{$node->parameterName} {$node->description}"); } + if ($node instanceof ParamImmediatelyInvokedCallableTagValueNode) { + return trim("{$node->parameterName} {$node->description}"); + } + if ($node instanceof ParamLaterInvokedCallableTagValueNode) { + return trim("{$node->parameterName} {$node->description}"); + } if ($node instanceof PropertyTagValueNode) { $type = $this->printType($node->type); return trim("{$type} {$node->propertyName} {$node->description}"); diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index b8f2c0a6..8d0ce734 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -29,6 +29,8 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamOutTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; @@ -97,6 +99,8 @@ protected function setUp(): void * @dataProvider provideTagsWithNumbers * @dataProvider provideSpecializedTags * @dataProvider provideParamTagsData + * @dataProvider provideParamImmediatelyInvokedCallableTagsData + * @dataProvider provideParamLaterInvokedCallableTagsData * @dataProvider provideTypelessParamTagsData * @dataProvider provideVarTagsData * @dataProvider provideReturnTagsData @@ -620,6 +624,68 @@ public function provideTypelessParamTagsData(): Iterator ]; } + public function provideParamImmediatelyInvokedCallableTagsData(): Iterator + { + yield [ + 'OK', + '/** @param-immediately-invoked-callable $foo */', + new PhpDocNode([ + new PhpDocTagNode( + '@param-immediately-invoked-callable', + new ParamImmediatelyInvokedCallableTagValueNode( + '$foo', + '' + ) + ), + ]), + ]; + + yield [ + 'OK with description', + '/** @param-immediately-invoked-callable $foo test two three */', + new PhpDocNode([ + new PhpDocTagNode( + '@param-immediately-invoked-callable', + new ParamImmediatelyInvokedCallableTagValueNode( + '$foo', + 'test two three' + ) + ), + ]), + ]; + } + + public function provideParamLaterInvokedCallableTagsData(): Iterator + { + yield [ + 'OK', + '/** @param-later-invoked-callable $foo */', + new PhpDocNode([ + new PhpDocTagNode( + '@param-later-invoked-callable', + new ParamLaterInvokedCallableTagValueNode( + '$foo', + '' + ) + ), + ]), + ]; + + yield [ + 'OK with description', + '/** @param-later-invoked-callable $foo test two three */', + new PhpDocNode([ + new PhpDocTagNode( + '@param-later-invoked-callable', + new ParamLaterInvokedCallableTagValueNode( + '$foo', + 'test two three' + ) + ), + ]), + ]; + } + public function provideVarTagsData(): Iterator { yield [ @@ -7117,6 +7183,8 @@ public function testReturnTypeLinesAndIndexes(string $phpDoc, array $lines): voi * @dataProvider provideSpecializedTags * @dataProvider provideParamTagsData * @dataProvider provideTypelessParamTagsData + * @dataProvider provideParamImmediatelyInvokedCallableTagsData + * @dataProvider provideParamLaterInvokedCallableTagsData * @dataProvider provideVarTagsData * @dataProvider provideReturnTagsData * @dataProvider provideThrowsTagsData diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 34a2e893..ddbe4310 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -17,6 +17,8 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineArray; use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineArrayItem; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; @@ -1665,6 +1667,42 @@ public function enterNode(Node $node) }, ]; + + yield [ + '/** @param-immediately-invoked-callable $foo test */', + '/** @param-immediately-invoked-callable $bar foo */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ParamImmediatelyInvokedCallableTagValueNode) { + $node->parameterName = '$bar'; + $node->description = 'foo'; + } + + return $node; + } + + }, + ]; + + yield [ + '/** @param-later-invoked-callable $foo test */', + '/** @param-later-invoked-callable $bar foo */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ParamLaterInvokedCallableTagValueNode) { + $node->parameterName = '$bar'; + $node->description = 'foo'; + } + + return $node; + } + + }, + ]; } /** From 86e4d5a4b036f8f0be1464522f4c6b584c452757 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 21 Mar 2024 14:06:20 +0100 Subject: [PATCH 06/27] Support for `@param-closure-this` --- .../PhpDoc/ParamClosureThisTagValueNode.php | 35 +++++++++++++ src/Ast/PhpDoc/PhpDocNode.php | 14 +++++ src/Parser/PhpDocParser.php | 15 ++++++ src/Printer/Printer.php | 4 ++ tests/PHPStan/Parser/PhpDocParserTest.php | 51 +++++++++++++++++++ tests/PHPStan/Printer/PrinterTest.php | 20 ++++++++ 6 files changed, 139 insertions(+) create mode 100644 src/Ast/PhpDoc/ParamClosureThisTagValueNode.php diff --git a/src/Ast/PhpDoc/ParamClosureThisTagValueNode.php b/src/Ast/PhpDoc/ParamClosureThisTagValueNode.php new file mode 100644 index 00000000..0ac2131a --- /dev/null +++ b/src/Ast/PhpDoc/ParamClosureThisTagValueNode.php @@ -0,0 +1,35 @@ +type = $type; + $this->parameterName = $parameterName; + $this->description = $description; + } + + public function __toString(): string + { + return trim("{$this->type} {$this->parameterName} {$this->description}"); + } + +} diff --git a/src/Ast/PhpDoc/PhpDocNode.php b/src/Ast/PhpDoc/PhpDocNode.php index b3b68811..ade55b78 100644 --- a/src/Ast/PhpDoc/PhpDocNode.php +++ b/src/Ast/PhpDoc/PhpDocNode.php @@ -118,6 +118,20 @@ static function (PhpDocTagValueNode $value): bool { } + /** + * @return ParamClosureThisTagValueNode[] + */ + public function getParamClosureThisTagValues(string $tagName = '@param-closure-this'): array + { + return array_filter( + array_column($this->getTagsByName($tagName), 'value'), + static function (PhpDocTagValueNode $value): bool { + return $value instanceof ParamClosureThisTagValueNode; + } + ); + } + + /** * @return TemplateTagValueNode[] */ diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index 9431fb6f..d7678a3d 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -397,6 +397,11 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph $tagValue = $this->parseParamLaterInvokedCallableTagValue($tokens); break; + case '@param-closure-this': + case '@phpstan-param-closure-this': + $tagValue = $this->parseParamClosureThisTagValue($tokens); + break; + case '@var': case '@phpstan-var': case '@psalm-var': @@ -889,6 +894,16 @@ private function parseParamLaterInvokedCallableTagValue(TokenIterator $tokens): } + private function parseParamClosureThisTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamClosureThisTagValueNode + { + $type = $this->typeParser->parse($tokens); + $parameterName = $this->parseRequiredVariableName($tokens); + $description = $this->parseOptionalDescription($tokens); + + return new Ast\PhpDoc\ParamClosureThisTagValueNode($type, $parameterName, $description); + } + + private function parseVarTagValue(TokenIterator $tokens): Ast\PhpDoc\VarTagValueNode { $type = $this->typeParser->parse($tokens); diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index 837c9e58..b1769932 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -20,6 +20,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamClosureThisTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamOutTagValueNode; @@ -312,6 +313,9 @@ private function printTagValue(PhpDocTagValueNode $node): string if ($node instanceof ParamLaterInvokedCallableTagValueNode) { return trim("{$node->parameterName} {$node->description}"); } + if ($node instanceof ParamClosureThisTagValueNode) { + return trim("{$node->type} {$node->parameterName} {$node->description}"); + } if ($node instanceof PropertyTagValueNode) { $type = $this->printType($node->type); return trim("{$type} {$node->propertyName} {$node->description}"); diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 8d0ce734..8085ec64 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -29,6 +29,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamClosureThisTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamOutTagValueNode; @@ -102,6 +103,7 @@ protected function setUp(): void * @dataProvider provideParamImmediatelyInvokedCallableTagsData * @dataProvider provideParamLaterInvokedCallableTagsData * @dataProvider provideTypelessParamTagsData + * @dataProvider provideParamClosureThisTagsData * @dataProvider provideVarTagsData * @dataProvider provideReturnTagsData * @dataProvider provideThrowsTagsData @@ -686,6 +688,54 @@ public function provideParamLaterInvokedCallableTagsData(): Iterator ]; } + public function provideParamClosureThisTagsData(): Iterator + { + yield [ + 'OK', + '/** @param-closure-this Foo $a */', + new PhpDocNode([ + new PhpDocTagNode( + '@param-closure-this', + new ParamClosureThisTagValueNode( + new IdentifierTypeNode('Foo'), + '$a', + '' + ) + ), + ]), + ]; + + yield [ + 'OK with prefix', + '/** @phpstan-param-closure-this Foo $a */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-param-closure-this', + new ParamClosureThisTagValueNode( + new IdentifierTypeNode('Foo'), + '$a', + '' + ) + ), + ]), + ]; + + yield [ + 'OK with description', + '/** @param-closure-this Foo $a test */', + new PhpDocNode([ + new PhpDocTagNode( + '@param-closure-this', + new ParamClosureThisTagValueNode( + new IdentifierTypeNode('Foo'), + '$a', + 'test' + ) + ), + ]), + ]; + } + public function provideVarTagsData(): Iterator { yield [ @@ -7185,6 +7235,7 @@ public function testReturnTypeLinesAndIndexes(string $phpDoc, array $lines): voi * @dataProvider provideTypelessParamTagsData * @dataProvider provideParamImmediatelyInvokedCallableTagsData * @dataProvider provideParamLaterInvokedCallableTagsData + * @dataProvider provideParamClosureThisTagsData * @dataProvider provideVarTagsData * @dataProvider provideReturnTagsData * @dataProvider provideThrowsTagsData diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index ddbe4310..25a268ee 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -17,6 +17,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineArray; use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineArrayItem; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamClosureThisTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; @@ -1703,6 +1704,25 @@ public function enterNode(Node $node) }, ]; + + yield [ + '/** @param-closure-this Foo $test haha */', + '/** @param-closure-this Bar $taste hehe */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ParamClosureThisTagValueNode) { + $node->type = new IdentifierTypeNode('Bar'); + $node->parameterName = '$taste'; + $node->description = 'hehe'; + } + + return $node; + } + + }, + ]; } /** From cd06d6b1a1b3c75b0b83f97577869fd85a3cd4fb Mon Sep 17 00:00:00 2001 From: Brad Jorsch Date: Wed, 3 Apr 2024 14:28:40 -0400 Subject: [PATCH 07/27] Add `@phan-` prefixes for recognized doc tags The following prefixes are added: * `@phan-assert` * `@phan-assert-if-false` * `@phan-assert-if-true` * `@phan-extends` * `@phan-inherits` * `@phan-method` * `@phan-mixin` * `@phan-param` * `@phan-property` * `@phan-property-read` * `@phan-property-write` * `@phan-real-return` * `@phan-return` * `@phan-template` * `@phan-type` * `@phan-var` No changes to any of the parsing were made, as the syntaxes seem to match what's already done for the existing unprefixed tags and/or the existing `@psalm-` and `@phpstan-` prefixed tags. --- src/Parser/PhpDocParser.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index d7678a3d..e46e0941 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -384,6 +384,7 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph case '@param': case '@phpstan-param': case '@psalm-param': + case '@phan-param': $tagValue = $this->parseParamTagValue($tokens); break; @@ -405,12 +406,15 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph case '@var': case '@phpstan-var': case '@psalm-var': + case '@phan-var': $tagValue = $this->parseVarTagValue($tokens); break; case '@return': case '@phpstan-return': case '@psalm-return': + case '@phan-return': + case '@phan-real-return': $tagValue = $this->parseReturnTagValue($tokens); break; @@ -420,6 +424,7 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph break; case '@mixin': + case '@phan-mixin': $tagValue = $this->parseMixinTagValue($tokens); break; @@ -446,18 +451,23 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph case '@psalm-property': case '@psalm-property-read': case '@psalm-property-write': + case '@phan-property': + case '@phan-property-read': + case '@phan-property-write': $tagValue = $this->parsePropertyTagValue($tokens); break; case '@method': case '@phpstan-method': case '@psalm-method': + case '@phan-method': $tagValue = $this->parseMethodTagValue($tokens); break; case '@template': case '@phpstan-template': case '@psalm-template': + case '@phan-template': case '@template-covariant': case '@phpstan-template-covariant': case '@psalm-template-covariant': @@ -474,6 +484,8 @@ function ($tokens) { case '@extends': case '@phpstan-extends': + case '@phan-extends': + case '@phan-inherits': case '@template-extends': $tagValue = $this->parseExtendsTagValue('@extends', $tokens); break; @@ -492,6 +504,7 @@ function ($tokens) { case '@phpstan-type': case '@psalm-type': + case '@phan-type': $tagValue = $this->parseTypeAliasTagValue($tokens); break; @@ -506,6 +519,9 @@ function ($tokens) { case '@psalm-assert': case '@psalm-assert-if-true': case '@psalm-assert-if-false': + case '@phan-assert': + case '@phan-assert-if-true': + case '@phan-assert-if-false': $tagValue = $this->parseAssertTagValue($tokens); break; @@ -1091,7 +1107,7 @@ private function parseTypeAliasTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeA $alias = $tokens->currentTokenValue(); $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); - // support psalm-type syntax + // support phan-type/psalm-type syntax $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL); if ($this->preserveTypeAliasesWithInvalidTypes) { From 793d14628dd0dea6b9285aa9fd49878e7f78e3fd Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 4 Apr 2024 09:34:24 +0200 Subject: [PATCH 08/27] Update phpdoc-parser in phpstan-src:1.11.x --- .github/workflows/send-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/send-pr.yml b/.github/workflows/send-pr.yml index 37b2a974..61a65267 100644 --- a/.github/workflows/send-pr.yml +++ b/.github/workflows/send-pr.yml @@ -23,7 +23,7 @@ jobs: repository: phpstan/phpstan-src path: phpstan-src token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - ref: 1.10.x + ref: 1.11.x - name: "Install dependencies" working-directory: ./phpstan-src From 54b191f409690206aed725a1c43fb450e499d4e0 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 5 Apr 2024 10:04:25 +0200 Subject: [PATCH 09/27] Test pure-callable and pure-Closure --- tests/PHPStan/Parser/TypeParserTest.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index f4d656dd..babaf9d1 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -768,6 +768,22 @@ public function provideParseData(): array new IdentifierTypeNode('Foo') ), ], + [ + 'pure-callable(): Foo', + new CallableTypeNode( + new IdentifierTypeNode('pure-callable'), + [], + new IdentifierTypeNode('Foo') + ), + ], + [ + 'pure-Closure(): Foo', + new CallableTypeNode( + new IdentifierTypeNode('pure-Closure'), + [], + new IdentifierTypeNode('Foo') + ), + ], [ 'callable(): ?Foo', new CallableTypeNode( From 9bb3855466b6c08936643e86a52cfb9cc072e52d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Sat, 20 Apr 2024 08:39:11 +0200 Subject: [PATCH 10/27] Update lock-closed-issues.yml --- .github/workflows/lock-closed-issues.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml index c2b017b9..69545301 100644 --- a/.github/workflows/lock-closed-issues.yml +++ b/.github/workflows/lock-closed-issues.yml @@ -2,7 +2,7 @@ name: 'Lock Issues' on: schedule: - - cron: '0 0 * * *' + - cron: '8 0 * * *' jobs: lock: From 536889f2b340489d328f5ffb7b02bb6b183ddedc Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Mon, 6 May 2024 14:04:23 +0200 Subject: [PATCH 11/27] Support array and offset access on const types --- src/Ast/Type/OffsetAccessTypeNode.php | 1 - src/Parser/TypeParser.php | 26 +++++++++++++++++-------- src/Printer/Printer.php | 2 -- tests/PHPStan/Parser/TypeParserTest.php | 7 +++++++ tests/PHPStan/Printer/PrinterTest.php | 24 +++++++++++++++++++++++ 5 files changed, 49 insertions(+), 11 deletions(-) diff --git a/src/Ast/Type/OffsetAccessTypeNode.php b/src/Ast/Type/OffsetAccessTypeNode.php index 39e83dfe..c27ec0a3 100644 --- a/src/Ast/Type/OffsetAccessTypeNode.php +++ b/src/Ast/Type/OffsetAccessTypeNode.php @@ -25,7 +25,6 @@ public function __toString(): string { if ( $this->type instanceof CallableTypeNode - || $this->type instanceof ConstTypeNode || $this->type instanceof NullableTypeNode ) { return '(' . $this->type . ')[' . $this->offset . ']'; diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index ebc2fbab..5669fe45 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -232,7 +232,17 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode ); } - return $this->enrichWithAttributes($tokens, new Ast\Type\ConstTypeNode($constExpr), $startLine, $startIndex); + $type = $this->enrichWithAttributes( + $tokens, + new Ast\Type\ConstTypeNode($constExpr), + $startLine, + $startIndex + ); + if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { + $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); + } + + return $type; } catch (LogicException $e) { throw new ParserException( $currentTokenValue, @@ -733,14 +743,14 @@ private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNo ); } - $type = new Ast\Type\ConstTypeNode($constExpr); + $type = $this->enrichWithAttributes( + $tokens, + new Ast\Type\ConstTypeNode($constExpr), + $startLine, + $startIndex + ); if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { - $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes( - $tokens, - $type, - $startLine, - $startIndex - )); + $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); } return $type; diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index b1769932..044d07f8 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -141,7 +141,6 @@ final class Printer CallableTypeNode::class, UnionTypeNode::class, IntersectionTypeNode::class, - ConstTypeNode::class, NullableTypeNode::class, ], ]; @@ -512,7 +511,6 @@ private function printOffsetAccessType(TypeNode $type): string $type instanceof CallableTypeNode || $type instanceof UnionTypeNode || $type instanceof IntersectionTypeNode - || $type instanceof ConstTypeNode || $type instanceof NullableTypeNode ) { return $this->wrapInParentheses($type); diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index babaf9d1..d6c66bb8 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -1067,6 +1067,13 @@ public function provideParseData(): array new IdentifierTypeNode('int') ), ], + [ + 'self::TYPES[ int ]', + new OffsetAccessTypeNode( + new ConstTypeNode(new ConstFetchNode('self', 'TYPES')), + new IdentifierTypeNode('int') + ), + ], [ "?\t\xA009", // edge-case with \h new NullableTypeNode( diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 25a268ee..d73481e2 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -1723,6 +1723,23 @@ public function enterNode(Node $node) }, ]; + + yield [ + '/** @return Foo[abc] */', + '/** @return self::FOO[abc] */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ReturnTagValueNode && $node->type instanceof OffsetAccessTypeNode) { + $node->type->type = new ConstTypeNode(new ConstFetchNode('self', 'FOO')); + } + + return $node; + } + + }, + ]; } /** @@ -1862,6 +1879,13 @@ public function dataPrintType(): iterable ]), 'Foo|Bar|(Baz|Lorem)', ]; + yield [ + new OffsetAccessTypeNode( + new ConstTypeNode(new ConstFetchNode('self', 'TYPES')), + new IdentifierTypeNode('int') + ), + 'self::TYPES[int]', + ]; } /** From fcaefacf2d5c417e928405b71b400d4ce10daaf4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 31 May 2024 10:45:45 +0200 Subject: [PATCH 12/27] `TemplateTagValueNode` name cannot be empty string --- src/Ast/PhpDoc/TemplateTagValueNode.php | 5 ++++- src/Parser/TypeParser.php | 4 ++++ tests/PHPStan/Printer/PrinterTest.php | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Ast/PhpDoc/TemplateTagValueNode.php b/src/Ast/PhpDoc/TemplateTagValueNode.php index 1d3c70e4..78b311ee 100644 --- a/src/Ast/PhpDoc/TemplateTagValueNode.php +++ b/src/Ast/PhpDoc/TemplateTagValueNode.php @@ -11,7 +11,7 @@ class TemplateTagValueNode implements PhpDocTagValueNode use NodeAttributes; - /** @var string */ + /** @var non-empty-string */ public $name; /** @var TypeNode|null */ @@ -23,6 +23,9 @@ class TemplateTagValueNode implements PhpDocTagValueNode /** @var string (may be empty) */ public $description; + /** + * @param non-empty-string $name + */ public function __construct(string $name, ?TypeNode $bound, string $description, ?TypeNode $default = null) { $this->name = $name; diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 5669fe45..2e404655 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -510,6 +510,10 @@ public function parseTemplateTagValue( $description = ''; } + if ($name === '') { + throw new LogicException('Template tag name cannot be empty.'); + } + return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description, $default); } diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index d73481e2..07d68af0 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -1792,7 +1792,7 @@ public function enterNode(Node $node) } /** - * @return iterable + * @return iterable */ public function dataPrintType(): iterable { @@ -1905,7 +1905,7 @@ public function testPrintType(TypeNode $node, string $expectedResult): void } /** - * @return iterable + * @return iterable */ public function dataPrintPhpDocNode(): iterable { From 2afec656054b25c58593083542e2411bc2395f1d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 22 Aug 2024 09:09:50 +0200 Subject: [PATCH 13/27] Update roave/backward-compatibility-check --- .github/workflows/backward-compatibility.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/backward-compatibility.yml b/.github/workflows/backward-compatibility.yml index 213da72c..ba1e3130 100644 --- a/.github/workflows/backward-compatibility.yml +++ b/.github/workflows/backward-compatibility.yml @@ -25,14 +25,11 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "7.4" + php-version: "8.3" - name: "Install dependencies" run: "composer install --no-dev --no-interaction --no-progress --no-suggest" - - name: "allow composer plugins" - run: "composer config --no-plugins --global allow-plugins.ocramius/package-versions true" - - name: "Install BackwardCompatibilityCheck" run: "composer global require --dev roave/backward-compatibility-check" From 8752839bdaaf704c013a0f78b7cc05f8bd1362e1 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 22 Aug 2024 08:12:02 +0200 Subject: [PATCH 14/27] Fix $matches error --- src/Parser/StringUnescaper.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Parser/StringUnescaper.php b/src/Parser/StringUnescaper.php index 70524055..a3bbeedd 100644 --- a/src/Parser/StringUnescaper.php +++ b/src/Parser/StringUnescaper.php @@ -2,6 +2,7 @@ namespace PHPStan\PhpDocParser\Parser; +use PHPStan\ShouldNotHappenException; use function chr; use function hexdec; use function octdec; @@ -56,6 +57,9 @@ static function ($matches) { return chr((int) hexdec(substr($str, 1))); } if ($str[0] === 'u') { + if (!isset($matches[2])) { + throw new ShouldNotHappenException(); + } return self::codePointToUtf8((int) hexdec($matches[2])); } From cc2b26c0b5b5d80b6c9faa0da3bee56cde1fb608 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 26 Aug 2024 13:06:18 +0200 Subject: [PATCH 15/27] Send update PR to phpstan-src 1.12.x branch --- .github/workflows/send-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/send-pr.yml b/.github/workflows/send-pr.yml index 61a65267..ab26eb3e 100644 --- a/.github/workflows/send-pr.yml +++ b/.github/workflows/send-pr.yml @@ -23,7 +23,7 @@ jobs: repository: phpstan/phpstan-src path: phpstan-src token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - ref: 1.11.x + ref: 1.12.x - name: "Install dependencies" working-directory: ./phpstan-src From 5ceb0e384997db59f38774bf79c2a6134252c08f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tinjo=20Sch=C3=B6ni?= <32767367+tscni@users.noreply.github.com> Date: Thu, 29 Aug 2024 11:54:52 +0200 Subject: [PATCH 16/27] Support typing extra items in unsealed array shapes --- src/Ast/Type/ArrayShapeNode.php | 13 +- src/Ast/Type/ArrayShapeUnsealedTypeNode.php | 34 ++ src/Parser/TypeParser.php | 71 +++- src/Printer/Printer.php | 9 +- tests/PHPStan/Parser/TypeParserTest.php | 398 ++++++++++++++++++++ tests/PHPStan/Printer/PrinterTest.php | 124 ++++++ 6 files changed, 645 insertions(+), 4 deletions(-) create mode 100644 src/Ast/Type/ArrayShapeUnsealedTypeNode.php diff --git a/src/Ast/Type/ArrayShapeNode.php b/src/Ast/Type/ArrayShapeNode.php index 806783f9..1f4ed4a9 100644 --- a/src/Ast/Type/ArrayShapeNode.php +++ b/src/Ast/Type/ArrayShapeNode.php @@ -22,15 +22,24 @@ class ArrayShapeNode implements TypeNode /** @var self::KIND_* */ public $kind; + /** @var ArrayShapeUnsealedTypeNode|null */ + public $unsealedType; + /** * @param ArrayShapeItemNode[] $items * @param self::KIND_* $kind */ - public function __construct(array $items, bool $sealed = true, string $kind = self::KIND_ARRAY) + public function __construct( + array $items, + bool $sealed = true, + string $kind = self::KIND_ARRAY, + ?ArrayShapeUnsealedTypeNode $unsealedType = null + ) { $this->items = $items; $this->sealed = $sealed; $this->kind = $kind; + $this->unsealedType = $unsealedType; } @@ -39,7 +48,7 @@ public function __toString(): string $items = $this->items; if (! $this->sealed) { - $items[] = '...'; + $items[] = '...' . $this->unsealedType; } return $this->kind . '{' . implode(', ', $items) . '}'; diff --git a/src/Ast/Type/ArrayShapeUnsealedTypeNode.php b/src/Ast/Type/ArrayShapeUnsealedTypeNode.php new file mode 100644 index 00000000..7ffdf1d2 --- /dev/null +++ b/src/Ast/Type/ArrayShapeUnsealedTypeNode.php @@ -0,0 +1,34 @@ +valueType = $valueType; + $this->keyType = $keyType; + } + + public function __toString(): string + { + if ($this->keyType !== null) { + return sprintf('<%s, %s>', $this->keyType, $this->valueType); + } + return sprintf('<%s>', $this->valueType); + } + +} diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 2e404655..c47ba10f 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -848,6 +848,7 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, $items = []; $sealed = true; + $unsealedType = null; do { $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); @@ -858,6 +859,17 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, if ($tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC)) { $sealed = false; + + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { + if ($kind === Ast\Type\ArrayShapeNode::KIND_ARRAY) { + $unsealedType = $this->parseArrayShapeUnsealedType($tokens); + } else { + $unsealedType = $this->parseListShapeUnsealedType($tokens); + } + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + } + $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA); break; } @@ -870,7 +882,7 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET); - return new Ast\Type\ArrayShapeNode($items, $sealed, $kind); + return new Ast\Type\ArrayShapeNode($items, $sealed, $kind, $unsealedType); } @@ -949,6 +961,63 @@ private function parseArrayShapeKey(TokenIterator $tokens) ); } + /** + * @phpstan-impure + */ + private function parseArrayShapeUnsealedType(TokenIterator $tokens): Ast\Type\ArrayShapeUnsealedTypeNode + { + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); + + $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + + $valueType = $this->parse($tokens); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + + $keyType = null; + if ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + + $keyType = $valueType; + $valueType = $this->parse($tokens); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + } + + $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); + + return $this->enrichWithAttributes( + $tokens, + new Ast\Type\ArrayShapeUnsealedTypeNode($valueType, $keyType), + $startLine, + $startIndex + ); + } + + /** + * @phpstan-impure + */ + private function parseListShapeUnsealedType(TokenIterator $tokens): Ast\Type\ArrayShapeUnsealedTypeNode + { + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); + + $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + + $valueType = $this->parse($tokens); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + + $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); + + return $this->enrichWithAttributes( + $tokens, + new Ast\Type\ArrayShapeUnsealedTypeNode($valueType, null), + $startLine, + $startIndex + ); + } + /** * @phpstan-impure */ diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index 044d07f8..c4b9c356 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -43,6 +43,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; +use PHPStan\PhpDocParser\Ast\Type\ArrayShapeUnsealedTypeNode; use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode; @@ -229,6 +230,12 @@ function (PhpDocChildNode $child): string { $isOptional = $node->isOptional ? '=' : ''; return trim("{$type}{$isReference}{$isVariadic}{$node->parameterName}") . $isOptional; } + if ($node instanceof ArrayShapeUnsealedTypeNode) { + if ($node->keyType !== null) { + return sprintf('<%s, %s>', $this->printType($node->keyType), $this->printType($node->valueType)); + } + return sprintf('<%s>', $this->printType($node->valueType)); + } if ($node instanceof DoctrineAnnotation) { return (string) $node; } @@ -366,7 +373,7 @@ private function printType(TypeNode $node): string }, $node->items); if (! $node->sealed) { - $items[] = '...'; + $items[] = '...' . ($node->unsealedType === null ? '' : $this->print($node->unsealedType)); } return $node->kind . '{' . implode(', ', $items) . '}'; diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index d6c66bb8..3610fa52 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -13,6 +13,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; +use PHPStan\PhpDocParser\Ast\Type\ArrayShapeUnsealedTypeNode; use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode; @@ -760,6 +761,403 @@ public function provideParseData(): array ArrayShapeNode::KIND_LIST ), ], + [ + 'array{...}', + new ArrayShapeNode( + [], + false, + ArrayShapeNode::KIND_ARRAY, + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + null + ) + ), + ], + [ + 'array{a: int, b?: int, ...}', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new IdentifierTypeNode('b'), + true, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_ARRAY, + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + null + ) + ), + ], + [ + 'array{a:int,b?:int,...}', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new IdentifierTypeNode('b'), + true, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_ARRAY, + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + null + ) + ), + ], + [ + 'array{a: int, b?: int, ... ' . PHP_EOL + . ' < ' . PHP_EOL + . ' string ' . PHP_EOL + . ' > ' . PHP_EOL + . ' , ' . PHP_EOL + . ' }', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new IdentifierTypeNode('b'), + true, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_ARRAY, + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + null + ) + ), + ], + [ + 'array{...}', + new ArrayShapeNode( + [], + false, + ArrayShapeNode::KIND_ARRAY, + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + new IdentifierTypeNode('int') + ) + ), + ], + [ + 'array{a: int, b?: int, ...}', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new IdentifierTypeNode('b'), + true, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_ARRAY, + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + new IdentifierTypeNode('int') + ) + ), + ], + [ + 'array{a:int,b?:int,...}', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new IdentifierTypeNode('b'), + true, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_ARRAY, + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + new IdentifierTypeNode('int') + ) + ), + ], + [ + 'array{a: int, b?: int, ... ' . PHP_EOL + . ' < ' . PHP_EOL + . ' int ' . PHP_EOL + . ' , ' . PHP_EOL + . ' string ' . PHP_EOL + . ' > ' . PHP_EOL + . ' , ' . PHP_EOL + . ' }', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new IdentifierTypeNode('b'), + true, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_ARRAY, + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + new IdentifierTypeNode('int') + ) + ), + ], + [ + 'list{...}', + new ArrayShapeNode( + [], + false, + ArrayShapeNode::KIND_LIST, + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + null + ) + ), + ], + [ + 'list{int, int, ...}', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_LIST, + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + null + ) + ), + ], + [ + 'list{int,int,...}', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_LIST, + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + null + ) + ), + ], + [ + 'list{int, int, ... ' . PHP_EOL + . ' < ' . PHP_EOL + . ' string ' . PHP_EOL + . ' > ' . PHP_EOL + . ' , ' . PHP_EOL + . ' }', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_LIST, + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + null + ) + ), + ], + [ + 'list{0: int, 1?: int, ...}', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + new ConstExprIntegerNode('0'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new ConstExprIntegerNode('1'), + true, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_LIST, + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + null + ) + ), + ], + [ + 'list{0:int,1?:int,...}', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + new ConstExprIntegerNode('0'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new ConstExprIntegerNode('1'), + true, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_LIST, + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + null + ) + ), + ], + [ + 'list{0: int, 1?: int, ... ' . PHP_EOL + . ' < ' . PHP_EOL + . ' string ' . PHP_EOL + . ' > ' . PHP_EOL + . ' , ' . PHP_EOL + . ' }', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + new ConstExprIntegerNode('0'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new ConstExprIntegerNode('1'), + true, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_LIST, + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + null + ) + ), + ], + [ + 'array{...<>}', + new ParserException( + '>', + Lexer::TOKEN_CLOSE_ANGLE_BRACKET, + 10, + Lexer::TOKEN_IDENTIFIER + ), + ], + [ + 'array{...}', + new ParserException( + '>', + Lexer::TOKEN_CLOSE_ANGLE_BRACKET, + 14, + Lexer::TOKEN_IDENTIFIER + ), + ], + [ + 'array{...}', + new ParserException( + ',', + Lexer::TOKEN_COMMA, + 21, + Lexer::TOKEN_CLOSE_ANGLE_BRACKET + ), + ], + [ + 'array{...}', + new ParserException( + ',', + Lexer::TOKEN_COMMA, + 21, + Lexer::TOKEN_CLOSE_ANGLE_BRACKET + ), + ], + [ + 'list{...<>}', + new ParserException( + '>', + Lexer::TOKEN_CLOSE_ANGLE_BRACKET, + 9, + Lexer::TOKEN_IDENTIFIER + ), + ], + [ + 'list{...}', + new ParserException( + ',', + Lexer::TOKEN_COMMA, + 12, + Lexer::TOKEN_CLOSE_ANGLE_BRACKET + ), + ], + [ + 'list{...}', + new ParserException( + ',', + Lexer::TOKEN_COMMA, + 12, + Lexer::TOKEN_CLOSE_ANGLE_BRACKET + ), + ], [ 'callable(): Foo', new CallableTypeNode( diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 07d68af0..24a28236 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -28,6 +28,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasImportTagValueNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; +use PHPStan\PhpDocParser\Ast\Type\ArrayShapeUnsealedTypeNode; use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode; @@ -51,6 +52,7 @@ use function array_splice; use function array_unshift; use function array_values; +use function assert; use function count; use const PHP_EOL; @@ -1740,6 +1742,128 @@ public function enterNode(Node $node) }, ]; + + yield [ + '/** @return array{foo: int, ...} */', + '/** @return array{foo: int} */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ArrayShapeNode) { + $node->sealed = true; + } + + return $node; + } + + }, + ]; + + yield [ + '/** @return array{foo: int, ...} */', + '/** @return array{foo: int, ...} */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ArrayShapeNode) { + $node->unsealedType = new ArrayShapeUnsealedTypeNode(new IdentifierTypeNode('string'), null); + } + + return $node; + } + + }, + ]; + + yield [ + '/** @return array{foo: int, ...} */', + '/** @return array{foo: int, ...} */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ArrayShapeNode) { + assert($node->unsealedType !== null); + $node->unsealedType->keyType = new IdentifierTypeNode('int'); + } + + return $node; + } + + }, + ]; + + yield [ + '/** @return array{foo: int, ...} */', + '/** @return array{foo: int} */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ArrayShapeNode) { + $node->sealed = true; + $node->unsealedType = null; + } + + return $node; + } + + }, + ]; + + yield [ + '/** @return list{int, ...} */', + '/** @return list{int} */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ArrayShapeNode) { + $node->sealed = true; + } + + return $node; + } + + }, + ]; + + yield [ + '/** @return list{int, ...} */', + '/** @return list{int, ...} */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ArrayShapeNode) { + $node->unsealedType = new ArrayShapeUnsealedTypeNode(new IdentifierTypeNode('string'), null); + } + + return $node; + } + + }, + ]; + + yield [ + '/** @return list{int, ...} */', + '/** @return list{int} */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ArrayShapeNode) { + $node->sealed = true; + $node->unsealedType = null; + } + + return $node; + } + + }, + ]; } /** From 04548928abe37ecb206fc7e805a44b3c48225c81 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 7 Sep 2024 14:44:54 +0200 Subject: [PATCH 17/27] Pin build-cs --- .github/workflows/build.yml | 1 + Makefile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d356f34e..4ac3eec5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,6 +60,7 @@ jobs: with: repository: "phpstan/build-cs" path: "build-cs" + ref: "1.x" - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/Makefile b/Makefile index 8289111c..4399bed1 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ lint: .PHONY: cs-install cs-install: git clone https://github.com/phpstan/build-cs.git || true - git -C build-cs fetch origin && git -C build-cs reset --hard origin/main + git -C build-cs fetch origin && git -C build-cs reset --hard origin/1.x composer install --working-dir build-cs .PHONY: cs From 9d053a887d4ce618397de488a6bbb58f14b0dfc6 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 7 Sep 2024 14:45:28 +0200 Subject: [PATCH 18/27] Test newer PHP versions --- .github/workflows/build.yml | 6 ++++++ .github/workflows/test-slevomat-coding-standard.yml | 2 ++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4ac3eec5..01b49f52 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,6 +22,8 @@ jobs: - "8.0" - "8.1" - "8.2" + - "8.3" + - "8.4" steps: - name: "Checkout" @@ -99,6 +101,8 @@ jobs: - "8.0" - "8.1" - "8.2" + - "8.3" + - "8.4" dependencies: - "lowest" - "highest" @@ -142,6 +146,8 @@ jobs: - "8.0" - "8.1" - "8.2" + - "8.3" + - "8.4" steps: - name: "Checkout" diff --git a/.github/workflows/test-slevomat-coding-standard.yml b/.github/workflows/test-slevomat-coding-standard.yml index 1967df97..238fed25 100644 --- a/.github/workflows/test-slevomat-coding-standard.yml +++ b/.github/workflows/test-slevomat-coding-standard.yml @@ -22,6 +22,8 @@ jobs: - "8.0" - "8.1" - "8.2" + - "8.3" + - "8.4" steps: - name: "Checkout" From 0e689c5a50d52663333accdd2e4648d78e6bff21 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 7 Sep 2024 22:00:28 +0200 Subject: [PATCH 19/27] Fix bug of repeating last line in AST for textBetweenTagsBelongsToDescription=true --- src/Parser/PhpDocParser.php | 4 +-- tests/PHPStan/Parser/PhpDocParserTest.php | 35 +++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index e46e0941..f6ef5dbb 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -237,7 +237,7 @@ private function parseText(TokenIterator $tokens): Ast\PhpDoc\PhpDocTextNode $text .= $tmpText; // stop if we're not at EOL - meaning it's the end of PHPDoc - if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) { + if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC)) { break; } @@ -293,7 +293,7 @@ private function parseOptionalDescriptionAfterDoctrineTag(TokenIterator $tokens) $text .= $tmpText; // stop if we're not at EOL - meaning it's the end of PHPDoc - if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) { + if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC)) { if (!$tokens->isPrecededByHorizontalWhitespace()) { return trim($text . $this->parseText($tokens)->text, " \t"); } diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 8085ec64..a4da2738 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -7547,6 +7547,41 @@ public function dataTextBetweenTagsBelongsToDescription(): iterable new PhpDocTagNode('@package', new GenericTagValueNode('foo')), ]), ]; + + yield [ + '/** @deprecated in Drupal 8.6.0 and will be removed before Drupal 9.0.0. In + * Drupal 9 there will be no way to set the status and in Drupal 8 this + * ability has been removed because mb_*() functions are supplied using + * Symfony\'s polyfill. */', + new PhpDocNode([ + new PhpDocTagNode( + '@deprecated', + new DeprecatedTagValueNode('in Drupal 8.6.0 and will be removed before Drupal 9.0.0. In + Drupal 9 there will be no way to set the status and in Drupal 8 this + ability has been removed because mb_*() functions are supplied using + Symfony\'s polyfill.') + ), + ]), + ]; + + yield [ + '/** @\ORM\Column() in Drupal 8.6.0 and will be removed before Drupal 9.0.0. In + * Drupal 9 there will be no way to set the status and in Drupal 8 this + * ability has been removed because mb_*() functions are supplied using + * Symfony\'s polyfill. */', + new PhpDocNode([ + new PhpDocTagNode( + '@\ORM\Column', + new DoctrineTagValueNode( + new DoctrineAnnotation('@\ORM\Column', []), + 'in Drupal 8.6.0 and will be removed before Drupal 9.0.0. In + Drupal 9 there will be no way to set the status and in Drupal 8 this + ability has been removed because mb_*() functions are supplied using + Symfony\'s polyfill.' + ) + ), + ]), + ]; } /** From 51b95ec8670af41009e2b2b56873bad96682413e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 7 Sep 2024 22:12:41 +0200 Subject: [PATCH 20/27] Deploy API Reference into a subdirectory with the current branch name --- .github/workflows/apiref.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/apiref.yml b/.github/workflows/apiref.yml index a315a699..24e6cd97 100644 --- a/.github/workflows/apiref.yml +++ b/.github/workflows/apiref.yml @@ -36,7 +36,7 @@ jobs: run: "composer install --no-interaction --no-progress" - name: "Run ApiGen" - run: "apigen/vendor/bin/apigen -c apigen/apigen.neon --output docs -- src" + run: "apigen/vendor/bin/apigen -c apigen/apigen.neon --output docs/${{ github.ref_name }} -- src" - name: "Copy favicon" run: "cp apigen/favicon.png docs/favicon.png" diff --git a/README.md b/README.md index 3b321b22..a522228e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ For the complete list of supported PHPDoc features check out PHPStan documentati * [PHPDoc Basics](https://phpstan.org/writing-php-code/phpdocs-basics) (list of PHPDoc tags) * [PHPDoc Types](https://phpstan.org/writing-php-code/phpdoc-types) (list of PHPDoc types) -* [phpdoc-parser API Reference](https://phpstan.github.io/phpdoc-parser/namespace-PHPStan.PhpDocParser.html) with all the AST node types etc. +* [phpdoc-parser API Reference](https://phpstan.github.io/phpdoc-parser/1.23.x/namespace-PHPStan.PhpDocParser.html) with all the AST node types etc. This parser also supports parsing [Doctrine Annotations](https://github.com/doctrine/annotations). The AST nodes live in the [PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine namespace](https://phpstan.github.io/phpdoc-parser/namespace-PHPStan.PhpDocParser.Ast.PhpDoc.Doctrine.html). The support needs to be turned on by setting `bool $parseDoctrineAnnotations` to `true` in `Lexer` and `PhpDocParser` class constructors. From 7e92a253ea8734a8d30cd04e4735f8b85069ccc7 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 7 Sep 2024 22:33:32 +0200 Subject: [PATCH 21/27] Fix link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a522228e..706b2a3c 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ For the complete list of supported PHPDoc features check out PHPStan documentati * [PHPDoc Types](https://phpstan.org/writing-php-code/phpdoc-types) (list of PHPDoc types) * [phpdoc-parser API Reference](https://phpstan.github.io/phpdoc-parser/1.23.x/namespace-PHPStan.PhpDocParser.html) with all the AST node types etc. -This parser also supports parsing [Doctrine Annotations](https://github.com/doctrine/annotations). The AST nodes live in the [PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine namespace](https://phpstan.github.io/phpdoc-parser/namespace-PHPStan.PhpDocParser.Ast.PhpDoc.Doctrine.html). The support needs to be turned on by setting `bool $parseDoctrineAnnotations` to `true` in `Lexer` and `PhpDocParser` class constructors. +This parser also supports parsing [Doctrine Annotations](https://github.com/doctrine/annotations). The AST nodes live in the [PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine namespace](https://phpstan.github.io/phpdoc-parser/1.23.x/namespace-PHPStan.PhpDocParser.Ast.PhpDoc.Doctrine.html). The support needs to be turned on by setting `bool $parseDoctrineAnnotations` to `true` in `Lexer` and `PhpDocParser` class constructors. ## Installation From 1c7fcc4251f1172c3725da3516b720cf66ed85b8 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Fri, 6 Sep 2024 13:54:31 +0200 Subject: [PATCH 22/27] Add test cases for quote aware array/object keys See #251 --- tests/PHPStan/Printer/PrinterTest.php | 30 +++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 24a28236..d9eab2d0 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -2010,6 +2010,36 @@ public function dataPrintType(): iterable ), 'self::TYPES[int]', ]; + yield [ + new ArrayShapeNode([ + new ArrayShapeItemNode( + new IdentifierTypeNode('name'), + false, + new IdentifierTypeNode('string') + ), + new ArrayShapeItemNode( + new QuoteAwareConstExprStringNode('Full Name', QuoteAwareConstExprStringNode::SINGLE_QUOTED), + false, + new IdentifierTypeNode('string') + ), + ]), + "array{name: string, 'Full Name': string}", + ]; + yield [ + new ObjectShapeNode([ + new ObjectShapeItemNode( + new IdentifierTypeNode('name'), + false, + new IdentifierTypeNode('string') + ), + new ObjectShapeItemNode( + new QuoteAwareConstExprStringNode('Full Name', QuoteAwareConstExprStringNode::SINGLE_QUOTED), + false, + new IdentifierTypeNode('string') + ), + ]), + "object{name: string, 'Full Name': string}", + ]; } /** From 5d48fa449a6855708315c36a79777c935d3293c7 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 9 Sep 2024 11:12:34 +0200 Subject: [PATCH 23/27] Do not deploy API reference on 1.23.x anymore --- .github/workflows/apiref.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/apiref.yml b/.github/workflows/apiref.yml index 24e6cd97..f9384b3f 100644 --- a/.github/workflows/apiref.yml +++ b/.github/workflows/apiref.yml @@ -5,7 +5,7 @@ name: "Deploy API Reference" on: push: branches: - - "1.23.x" + - "2.0.x" concurrency: group: "pages" From a5e938b30cd9efabc77287d89fdb3db147a658c4 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 15 Sep 2024 21:01:23 +0200 Subject: [PATCH 24/27] Test for special multiline PHPDoc --- tests/PHPStan/Parser/PhpDocParserTest.php | 79 +++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index a4da2738..f765015c 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -3881,6 +3881,46 @@ public function provideMultiLinePhpDocData(): iterable new PhpDocTextNode('test'), ]), ]; + + yield [ + 'Real-world test case multiline PHPDoc', + '/**' . PHP_EOL . + ' *' . PHP_EOL . + ' * MultiLine' . PHP_EOL . + ' * description' . PHP_EOL . + ' * @param bool $a' . PHP_EOL . + ' *' . PHP_EOL . + ' * @return void' . PHP_EOL . + ' *' . PHP_EOL . + ' * @throws \Exception' . PHP_EOL . + ' *' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTextNode(''), + new PhpDocTextNode( + 'MultiLine' . PHP_EOL . + 'description' + ), + new PhpDocTagNode('@param', new ParamTagValueNode( + new IdentifierTypeNode('bool'), + false, + '$a', + '', + false + )), + new PhpDocTextNode(''), + new PhpDocTagNode('@return', new ReturnTagValueNode( + new IdentifierTypeNode('void'), + '' + )), + new PhpDocTextNode(''), + new PhpDocTagNode('@throws', new ThrowsTagValueNode( + new IdentifierTypeNode('\Exception'), + '' + )), + new PhpDocTextNode(''), + ]), + ]; } public function provideTemplateTagsData(): Iterator @@ -7582,6 +7622,45 @@ public function dataTextBetweenTagsBelongsToDescription(): iterable ), ]), ]; + + yield [ + '/**' . PHP_EOL . + ' *' . PHP_EOL . + ' * MultiLine' . PHP_EOL . + ' * description' . PHP_EOL . + ' * @param bool $a' . PHP_EOL . + ' *' . PHP_EOL . + ' * @return void' . PHP_EOL . + ' *' . PHP_EOL . + ' * @throws \Exception' . PHP_EOL . + ' *' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTextNode( + PHP_EOL . + 'MultiLine' . PHP_EOL . + 'description' + ), + new PhpDocTagNode('@param', new ParamTagValueNode( + new IdentifierTypeNode('bool'), + false, + '$a', + '', + false + )), + new PhpDocTextNode(''), + new PhpDocTagNode('@return', new ReturnTagValueNode( + new IdentifierTypeNode('void'), + '' + )), + new PhpDocTextNode(''), + new PhpDocTagNode('@throws', new ThrowsTagValueNode( + new IdentifierTypeNode('\Exception'), + '' + )), + new PhpDocTextNode(''), + ]), + ]; } /** From 249f15fb843bf240cf058372dad29e100cee6c17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Fri, 20 Sep 2024 15:32:55 +0200 Subject: [PATCH 25/27] PhpDocParser: support template type lower bounds --- doc/grammars/type.abnf | 5 +- src/Ast/PhpDoc/TemplateTagValueNode.php | 11 +++- src/Parser/TypeParser.php | 11 ++-- src/Printer/Printer.php | 5 +- .../Ast/ToString/PhpDocToStringTest.php | 9 ++- tests/PHPStan/Parser/PhpDocParserTest.php | 31 ++++++++-- tests/PHPStan/Printer/PrinterTest.php | 56 +++++++++++++++++++ 7 files changed, 109 insertions(+), 19 deletions(-) diff --git a/doc/grammars/type.abnf b/doc/grammars/type.abnf index 36118d2b..ad955a9b 100644 --- a/doc/grammars/type.abnf +++ b/doc/grammars/type.abnf @@ -41,7 +41,7 @@ CallableTemplate = TokenAngleBracketOpen CallableTemplateArgument *(TokenComma CallableTemplateArgument) TokenAngleBracketClose CallableTemplateArgument - = TokenIdentifier [1*ByteHorizontalWs TokenOf Type] + = TokenIdentifier [1*ByteHorizontalWs TokenOf Type] [1*ByteHorizontalWs TokenSuper Type] ["=" Type] CallableParameters = CallableParameter *(TokenComma CallableParameter) @@ -201,6 +201,9 @@ TokenNot TokenOf = %s"of" 1*ByteHorizontalWs +TokenSuper + = %s"super" 1*ByteHorizontalWs + TokenContravariant = %s"contravariant" 1*ByteHorizontalWs diff --git a/src/Ast/PhpDoc/TemplateTagValueNode.php b/src/Ast/PhpDoc/TemplateTagValueNode.php index 78b311ee..8bc01f6e 100644 --- a/src/Ast/PhpDoc/TemplateTagValueNode.php +++ b/src/Ast/PhpDoc/TemplateTagValueNode.php @@ -17,6 +17,9 @@ class TemplateTagValueNode implements PhpDocTagValueNode /** @var TypeNode|null */ public $bound; + /** @var TypeNode|null */ + public $lowerBound; + /** @var TypeNode|null */ public $default; @@ -26,10 +29,11 @@ class TemplateTagValueNode implements PhpDocTagValueNode /** * @param non-empty-string $name */ - public function __construct(string $name, ?TypeNode $bound, string $description, ?TypeNode $default = null) + public function __construct(string $name, ?TypeNode $bound, string $description, ?TypeNode $default = null, ?TypeNode $lowerBound = null) { $this->name = $name; $this->bound = $bound; + $this->lowerBound = $lowerBound; $this->default = $default; $this->description = $description; } @@ -37,9 +41,10 @@ public function __construct(string $name, ?TypeNode $bound, string $description, public function __toString(): string { - $bound = $this->bound !== null ? " of {$this->bound}" : ''; + $upperBound = $this->bound !== null ? " of {$this->bound}" : ''; + $lowerBound = $this->lowerBound !== null ? " super {$this->lowerBound}" : ''; $default = $this->default !== null ? " = {$this->default}" : ''; - return trim("{$this->name}{$bound}{$default} {$this->description}"); + return trim("{$this->name}{$upperBound}{$lowerBound}{$default} {$this->description}"); } } diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index c47ba10f..2be28398 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -491,11 +491,14 @@ public function parseTemplateTagValue( $name = $tokens->currentTokenValue(); $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); + $upperBound = $lowerBound = null; + if ($tokens->tryConsumeTokenValue('of') || $tokens->tryConsumeTokenValue('as')) { - $bound = $this->parse($tokens); + $upperBound = $this->parse($tokens); + } - } else { - $bound = null; + if ($tokens->tryConsumeTokenValue('super')) { + $lowerBound = $this->parse($tokens); } if ($tokens->tryConsumeTokenValue('=')) { @@ -514,7 +517,7 @@ public function parseTemplateTagValue( throw new LogicException('Template tag name cannot be empty.'); } - return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description, $default); + return new Ast\PhpDoc\TemplateTagValueNode($name, $upperBound, $description, $default, $lowerBound); } diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index c4b9c356..75500780 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -335,9 +335,10 @@ private function printTagValue(PhpDocTagValueNode $node): string return trim($type . ' ' . $node->description); } if ($node instanceof TemplateTagValueNode) { - $bound = $node->bound !== null ? ' of ' . $this->printType($node->bound) : ''; + $upperBound = $node->bound !== null ? ' of ' . $this->printType($node->bound) : ''; + $lowerBound = $node->lowerBound !== null ? ' super ' . $this->printType($node->lowerBound) : ''; $default = $node->default !== null ? ' = ' . $this->printType($node->default) : ''; - return trim("{$node->name}{$bound}{$default} {$node->description}"); + return trim("{$node->name}{$upperBound}{$lowerBound}{$default} {$node->description}"); } if ($node instanceof ThrowsTagValueNode) { $type = $this->printType($node->type); diff --git a/tests/PHPStan/Ast/ToString/PhpDocToStringTest.php b/tests/PHPStan/Ast/ToString/PhpDocToStringTest.php index b57b7db6..74d257ab 100644 --- a/tests/PHPStan/Ast/ToString/PhpDocToStringTest.php +++ b/tests/PHPStan/Ast/ToString/PhpDocToStringTest.php @@ -155,12 +155,15 @@ public static function provideOtherCases(): Generator $baz = new IdentifierTypeNode('Foo\\Baz'); yield from [ - ['TValue', new TemplateTagValueNode('TValue', null, '', null)], - ['TValue of Foo\\Bar', new TemplateTagValueNode('TValue', $bar, '', null)], + ['TValue', new TemplateTagValueNode('TValue', null, '')], + ['TValue of Foo\\Bar', new TemplateTagValueNode('TValue', $bar, '')], + ['TValue super Foo\\Bar', new TemplateTagValueNode('TValue', null, '', null, $bar)], ['TValue = Foo\\Bar', new TemplateTagValueNode('TValue', null, '', $bar)], ['TValue of Foo\\Bar = Foo\\Baz', new TemplateTagValueNode('TValue', $bar, '', $baz)], - ['TValue Description.', new TemplateTagValueNode('TValue', null, 'Description.', null)], + ['TValue Description.', new TemplateTagValueNode('TValue', null, 'Description.')], ['TValue of Foo\\Bar = Foo\\Baz Description.', new TemplateTagValueNode('TValue', $bar, 'Description.', $baz)], + ['TValue super Foo\\Bar = Foo\\Baz Description.', new TemplateTagValueNode('TValue', null, 'Description.', $baz, $bar)], + ['TValue of Foo\\Bar super Foo\\Baz Description.', new TemplateTagValueNode('TValue', $bar, 'Description.', null, $baz)], ]; } diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index f765015c..f1efa5f6 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -3986,7 +3986,7 @@ public function provideTemplateTagsData(): Iterator ]; yield [ - 'OK with bound and description', + 'OK with upper bound and description', '/** @template T of DateTime the value type */', new PhpDocNode([ new PhpDocTagNode( @@ -4001,22 +4001,41 @@ public function provideTemplateTagsData(): Iterator ]; yield [ - 'OK with bound and description', - '/** @template T as DateTime the value type */', + 'OK with lower bound and description', + '/** @template T super DateTimeImmutable the value type */', new PhpDocNode([ new PhpDocTagNode( '@template', new TemplateTagValueNode( 'T', - new IdentifierTypeNode('DateTime'), - 'the value type' + null, + 'the value type', + null, + new IdentifierTypeNode('DateTimeImmutable') + ) + ), + ]), + ]; + + yield [ + 'OK with both bounds and description', + '/** @template T of DateTimeInterface super DateTimeImmutable the value type */', + new PhpDocNode([ + new PhpDocTagNode( + '@template', + new TemplateTagValueNode( + 'T', + new IdentifierTypeNode('DateTimeInterface'), + 'the value type', + null, + new IdentifierTypeNode('DateTimeImmutable') ) ), ]), ]; yield [ - 'invalid without bound and description', + 'invalid without bounds and description', '/** @template */', new PhpDocNode([ new PhpDocTagNode( diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index d9eab2d0..ee549c7e 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -947,6 +947,12 @@ public function enterNode(Node $node) $addTemplateTagBound, ]; + yield [ + '/** @template T super string */', + '/** @template T of int super string */', + $addTemplateTagBound, + ]; + $removeTemplateTagBound = new class extends AbstractNodeVisitor { public function enterNode(Node $node) @@ -966,6 +972,56 @@ public function enterNode(Node $node) $removeTemplateTagBound, ]; + $addTemplateTagLowerBound = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof TemplateTagValueNode) { + $node->lowerBound = new IdentifierTypeNode('int'); + } + + return $node; + } + + }; + + yield [ + '/** @template T */', + '/** @template T super int */', + $addTemplateTagLowerBound, + ]; + + yield [ + '/** @template T super string */', + '/** @template T super int */', + $addTemplateTagLowerBound, + ]; + + yield [ + '/** @template T of string */', + '/** @template T of string super int */', + $addTemplateTagLowerBound, + ]; + + $removeTemplateTagLowerBound = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof TemplateTagValueNode) { + $node->lowerBound = null; + } + + return $node; + } + + }; + + yield [ + '/** @template T super int */', + '/** @template T */', + $removeTemplateTagLowerBound, + ]; + $addKeyNameToArrayShapeItemNode = new class extends AbstractNodeVisitor { public function enterNode(Node $node) From 6ca22b154efdd9e3c68c56f5d94670920a1c19a4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 18 Sep 2024 16:08:30 +0200 Subject: [PATCH 26/27] Support for `@pure-unless-callable-is-impure` --- src/Ast/PhpDoc/PhpDocNode.php | 12 +++++++ ...PureUnlessCallableIsImpureTagValueNode.php | 30 +++++++++++++++++ src/Parser/PhpDocParser.php | 12 +++++++ src/Printer/Printer.php | 4 +++ tests/PHPStan/Parser/PhpDocParserTest.php | 33 +++++++++++++++++++ tests/PHPStan/Printer/PrinterTest.php | 19 +++++++++++ 6 files changed, 110 insertions(+) create mode 100644 src/Ast/PhpDoc/PureUnlessCallableIsImpureTagValueNode.php diff --git a/src/Ast/PhpDoc/PhpDocNode.php b/src/Ast/PhpDoc/PhpDocNode.php index ade55b78..a08b777f 100644 --- a/src/Ast/PhpDoc/PhpDocNode.php +++ b/src/Ast/PhpDoc/PhpDocNode.php @@ -131,6 +131,18 @@ static function (PhpDocTagValueNode $value): bool { ); } + /** + * @return PureUnlessCallableIsImpureTagValueNode[] + */ + public function getPureUnlessCallableIsImpureTagValues(string $tagName = '@pure-unless-callable-is-impure'): array + { + return array_filter( + array_column($this->getTagsByName($tagName), 'value'), + static function (PhpDocTagValueNode $value): bool { + return $value instanceof PureUnlessCallableIsImpureTagValueNode; + } + ); + } /** * @return TemplateTagValueNode[] diff --git a/src/Ast/PhpDoc/PureUnlessCallableIsImpureTagValueNode.php b/src/Ast/PhpDoc/PureUnlessCallableIsImpureTagValueNode.php new file mode 100644 index 00000000..4cf0937d --- /dev/null +++ b/src/Ast/PhpDoc/PureUnlessCallableIsImpureTagValueNode.php @@ -0,0 +1,30 @@ +parameterName = $parameterName; + $this->description = $description; + } + + public function __toString(): string + { + return trim("{$this->parameterName} {$this->description}"); + } + +} diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index f6ef5dbb..b6cd85ea 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -403,6 +403,11 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph $tagValue = $this->parseParamClosureThisTagValue($tokens); break; + case '@pure-unless-callable-is-impure': + case '@phpstan-pure-unless-callable-is-impure': + $tagValue = $this->parsePureUnlessCallableIsImpureTagValue($tokens); + break; + case '@var': case '@phpstan-var': case '@psalm-var': @@ -919,6 +924,13 @@ private function parseParamClosureThisTagValue(TokenIterator $tokens): Ast\PhpDo return new Ast\PhpDoc\ParamClosureThisTagValueNode($type, $parameterName, $description); } + private function parsePureUnlessCallableIsImpureTagValue(TokenIterator $tokens): Ast\PhpDoc\PureUnlessCallableIsImpureTagValueNode + { + $parameterName = $this->parseRequiredVariableName($tokens); + $description = $this->parseOptionalDescription($tokens); + + return new Ast\PhpDoc\PureUnlessCallableIsImpureTagValueNode($parameterName, $description); + } private function parseVarTagValue(TokenIterator $tokens): Ast\PhpDoc\VarTagValueNode { diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index 75500780..f6665987 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -31,6 +31,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\PureUnlessCallableIsImpureTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; @@ -322,6 +323,9 @@ private function printTagValue(PhpDocTagValueNode $node): string if ($node instanceof ParamClosureThisTagValueNode) { return trim("{$node->type} {$node->parameterName} {$node->description}"); } + if ($node instanceof PureUnlessCallableIsImpureTagValueNode) { + return trim("{$node->parameterName} {$node->description}"); + } if ($node instanceof PropertyTagValueNode) { $type = $this->printType($node->type); return trim("{$type} {$node->propertyName} {$node->description}"); diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index f1efa5f6..1017fd31 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -38,6 +38,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\PureUnlessCallableIsImpureTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; @@ -104,6 +105,7 @@ protected function setUp(): void * @dataProvider provideParamLaterInvokedCallableTagsData * @dataProvider provideTypelessParamTagsData * @dataProvider provideParamClosureThisTagsData + * @dataProvider providePureUnlessCallableIsImpureTagsData * @dataProvider provideVarTagsData * @dataProvider provideReturnTagsData * @dataProvider provideThrowsTagsData @@ -736,6 +738,37 @@ public function provideParamClosureThisTagsData(): Iterator ]; } + public function providePureUnlessCallableIsImpureTagsData(): Iterator + { + yield [ + 'OK', + '/** @pure-unless-callable-is-impure $foo */', + new PhpDocNode([ + new PhpDocTagNode( + '@pure-unless-callable-is-impure', + new PureUnlessCallableIsImpureTagValueNode( + '$foo', + '' + ) + ), + ]), + ]; + + yield [ + 'OK with description', + '/** @pure-unless-callable-is-impure $foo test two three */', + new PhpDocNode([ + new PhpDocTagNode( + '@pure-unless-callable-is-impure', + new PureUnlessCallableIsImpureTagValueNode( + '$foo', + 'test two three' + ) + ), + ]), + ]; + } + public function provideVarTagsData(): Iterator { yield [ diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index ee549c7e..e4effc31 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -23,6 +23,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\PureUnlessCallableIsImpureTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasImportTagValueNode; @@ -1782,6 +1783,24 @@ public function enterNode(Node $node) }, ]; + yield [ + '/** @pure-unless-callable-is-impure $foo test */', + '/** @pure-unless-callable-is-impure $bar foo */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof PureUnlessCallableIsImpureTagValueNode) { + $node->parameterName = '$bar'; + $node->description = 'foo'; + } + + return $node; + } + + }, + ]; + yield [ '/** @return Foo[abc] */', '/** @return self::FOO[abc] */', From 82a311fd3690fb2bf7b64d5c98f912b3dd746140 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 Oct 2024 13:20:03 +0200 Subject: [PATCH 27/27] Support for `non-empty-array` and `non-empty-list` array shape kind --- src/Ast/Type/ArrayShapeNode.php | 2 + src/Parser/TypeParser.php | 16 ++++- tests/PHPStan/Parser/TypeParserTest.php | 82 +++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 2 deletions(-) diff --git a/src/Ast/Type/ArrayShapeNode.php b/src/Ast/Type/ArrayShapeNode.php index 1f4ed4a9..73d162de 100644 --- a/src/Ast/Type/ArrayShapeNode.php +++ b/src/Ast/Type/ArrayShapeNode.php @@ -10,6 +10,8 @@ class ArrayShapeNode implements TypeNode public const KIND_ARRAY = 'array'; public const KIND_LIST = 'list'; + public const KIND_NON_EMPTY_ARRAY = 'non-empty-array'; + public const KIND_NON_EMPTY_LIST = 'non-empty-list'; use NodeAttributes; diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 2be28398..982eba7d 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -180,7 +180,13 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); - } elseif (in_array($type->name, ['array', 'list', 'object'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { + } elseif (in_array($type->name, [ + Ast\Type\ArrayShapeNode::KIND_ARRAY, + Ast\Type\ArrayShapeNode::KIND_LIST, + Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_ARRAY, + Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_LIST, + 'object', + ], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { if ($type->name === 'object') { $type = $this->parseObjectShape($tokens); } else { @@ -690,7 +696,13 @@ private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNo $startIndex )); - } elseif (in_array($type->name, ['array', 'list', 'object'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { + } elseif (in_array($type->name, [ + Ast\Type\ArrayShapeNode::KIND_ARRAY, + Ast\Type\ArrayShapeNode::KIND_LIST, + Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_ARRAY, + Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_LIST, + 'object', + ], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { if ($type->name === 'object') { $type = $this->parseObjectShape($tokens); } else { diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 3610fa52..5ff5dd83 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -761,6 +761,88 @@ public function provideParseData(): array ArrayShapeNode::KIND_LIST ), ], + [ + 'non-empty-array{ + int, + string + }', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('string') + ), + ], + true, + ArrayShapeNode::KIND_NON_EMPTY_ARRAY + ), + ], + [ + 'callable(): non-empty-array{int, string}', + new CallableTypeNode(new IdentifierTypeNode('callable'), [], new ArrayShapeNode( + [ + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('string') + ), + ], + true, + ArrayShapeNode::KIND_NON_EMPTY_ARRAY + )), + ], + [ + 'callable(): non-empty-list{int, string}', + new CallableTypeNode(new IdentifierTypeNode('callable'), [], new ArrayShapeNode( + [ + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('string') + ), + ], + true, + ArrayShapeNode::KIND_NON_EMPTY_LIST + )), + ], + [ + 'non-empty-list{ + int, + string + }', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('string') + ), + ], + true, + ArrayShapeNode::KIND_NON_EMPTY_LIST + ), + ], [ 'array{...}', new ArrayShapeNode(