diff --git a/.gitattributes b/.gitattributes index 1daa7443..984945c7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,5 +13,6 @@ tmp export-ignore build-abnfgen.sh export-ignore CODE_OF_CONDUCT.md export-ignore Makefile export-ignore +phpstan-baseline.neon export-ignore phpstan.neon export-ignore phpunit.xml export-ignore diff --git a/.github/workflows/apiref.yml b/.github/workflows/apiref.yml index 85f91693..a315a699 100644 --- a/.github/workflows/apiref.yml +++ b/.github/workflows/apiref.yml @@ -42,7 +42,7 @@ jobs: run: "cp apigen/favicon.png docs/favicon.png" - name: Upload artifact - uses: actions/upload-pages-artifact@v2 + uses: actions/upload-pages-artifact@v3 with: path: 'docs' @@ -63,4 +63,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v2 + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml index 4c7990df..69545301 100644 --- a/.github/workflows/lock-closed-issues.yml +++ b/.github/workflows/lock-closed-issues.yml @@ -2,13 +2,13 @@ name: 'Lock Issues' on: schedule: - - cron: '0 0 * * *' + - cron: '8 0 * * *' jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v4 + - uses: dessant/lock-threads@v5 with: github-token: ${{ github.token }} issue-inactive-days: '31' 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/release.yml b/.github/workflows/release.yml index e4a8ac62..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.1.0 + uses: metcalfc/changelog-generator@v4.3.1 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} diff --git a/.github/workflows/send-pr.yml b/.github/workflows/send-pr.yml index 023293c7..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 @@ -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 diff --git a/.github/workflows/test-slevomat-coding-standard.yml b/.github/workflows/test-slevomat-coding-standard.yml index fb266231..1967df97 100644 --- a/.github/workflows/test-slevomat-coding-standard.yml +++ b/.github/workflows/test-slevomat-coding-standard.yml @@ -32,7 +32,7 @@ jobs: with: repository: slevomat/coding-standard path: slevomat-cs - ref: 8.13.2 + ref: ad4eab6f2cf9aa099c8853e4e3a250a9ae5fb4bd - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/apigen/theme/blocks/head.latte b/apigen/theme/blocks/head.latte index e7f54525..6077db0a 100644 --- a/apigen/theme/blocks/head.latte +++ b/apigen/theme/blocks/head.latte @@ -1,4 +1,4 @@ {define head} - + {/define} 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/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/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 25f1939c..ade55b78 100644 --- a/src/Ast/PhpDoc/PhpDocNode.php +++ b/src/Ast/PhpDoc/PhpDocNode.php @@ -90,6 +90,48 @@ 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 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[] */ @@ -187,6 +229,31 @@ static function (PhpDocTagValueNode $value): bool { ); } + /** + * @return RequireExtendsTagValueNode[] + */ + public function getRequireExtendsTagValues(string $tagName = '@phpstan-require-extends'): array + { + return array_filter( + array_column($this->getTagsByName($tagName), 'value'), + static function (PhpDocTagValueNode $value): bool { + return $value instanceof RequireExtendsTagValueNode; + } + ); + } + + /** + * @return RequireImplementsTagValueNode[] + */ + public function getRequireImplementsTagValues(string $tagName = '@phpstan-require-implements'): array + { + return array_filter( + array_column($this->getTagsByName($tagName), 'value'), + static function (PhpDocTagValueNode $value): bool { + return $value instanceof RequireImplementsTagValueNode; + } + ); + } /** * @return DeprecatedTagValueNode[] diff --git a/src/Ast/PhpDoc/RequireExtendsTagValueNode.php b/src/Ast/PhpDoc/RequireExtendsTagValueNode.php new file mode 100644 index 00000000..91c26892 --- /dev/null +++ b/src/Ast/PhpDoc/RequireExtendsTagValueNode.php @@ -0,0 +1,32 @@ +type = $type; + $this->description = $description; + } + + + public function __toString(): string + { + return trim("{$this->type} {$this->description}"); + } + +} diff --git a/src/Ast/PhpDoc/RequireImplementsTagValueNode.php b/src/Ast/PhpDoc/RequireImplementsTagValueNode.php new file mode 100644 index 00000000..65c9213f --- /dev/null +++ b/src/Ast/PhpDoc/RequireImplementsTagValueNode.php @@ -0,0 +1,32 @@ +type = $type; + $this->description = $description; + } + + + public function __toString(): string + { + return trim("{$this->type} {$this->description}"); + } + +} diff --git a/src/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/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/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/PhpDocParser.php b/src/Parser/PhpDocParser.php index 15a2aa5c..e46e0941 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -384,18 +384,37 @@ 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; + 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 '@param-closure-this': + case '@phpstan-param-closure-this': + $tagValue = $this->parseParamClosureThisTagValue($tokens); + break; + 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; @@ -405,9 +424,20 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph break; case '@mixin': + case '@phan-mixin': $tagValue = $this->parseMixinTagValue($tokens); break; + case '@psalm-require-extends': + case '@phpstan-require-extends': + $tagValue = $this->parseRequireExtendsTagValue($tokens); + break; + + case '@psalm-require-implements': + case '@phpstan-require-implements': + $tagValue = $this->parseRequireImplementsTagValue($tokens); + break; + case '@deprecated': $tagValue = $this->parseDeprecatedTagValue($tokens); break; @@ -421,29 +451,41 @@ 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': 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': case '@phpstan-extends': + case '@phan-extends': + case '@phan-inherits': case '@template-extends': $tagValue = $this->parseExtendsTagValue('@extends', $tokens); break; @@ -462,6 +504,7 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph case '@phpstan-type': case '@psalm-type': + case '@phan-type': $tagValue = $this->parseTypeAliasTagValue($tokens); break; @@ -476,6 +519,9 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph 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; @@ -846,6 +892,34 @@ 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 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); @@ -877,6 +951,20 @@ private function parseMixinTagValue(TokenIterator $tokens): Ast\PhpDoc\MixinTagV return new Ast\PhpDoc\MixinTagValueNode($type, $description); } + private function parseRequireExtendsTagValue(TokenIterator $tokens): Ast\PhpDoc\RequireExtendsTagValueNode + { + $type = $this->typeParser->parse($tokens); + $description = $this->parseOptionalDescription($tokens, true); + return new Ast\PhpDoc\RequireExtendsTagValueNode($type, $description); + } + + private function parseRequireImplementsTagValue(TokenIterator $tokens): Ast\PhpDoc\RequireImplementsTagValueNode + { + $type = $this->typeParser->parse($tokens); + $description = $this->parseOptionalDescription($tokens, true); + return new Ast\PhpDoc\RequireImplementsTagValueNode($type, $description); + } + private function parseDeprecatedTagValue(TokenIterator $tokens): Ast\PhpDoc\DeprecatedTagValueNode { $description = $this->parseOptionalDescription($tokens); @@ -895,10 +983,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; @@ -906,9 +1000,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; @@ -923,7 +1015,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); } @@ -979,33 +1076,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(); @@ -1037,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) { diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 46de7aae..2e404655 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -4,10 +4,13 @@ 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; +use function strlen; use function strpos; +use function substr_compare; use function trim; class TypeParser @@ -162,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); @@ -225,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, @@ -380,10 +397,16 @@ public function isHtml(TokenIterator $tokens): bool return false; } + $endTag = ''; + $endTagSearchOffset = - strlen($endTag); + while (!$tokens->isCurrentTokenType(Lexer::TOKEN_END)) { if ( - $tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET) - && strpos($tokens->currentTokenValue(), '/' . $htmlTagName . '>') !== false + ( + $tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET) + && strpos($tokens->currentTokenValue(), '/' . $htmlTagName . '>') !== false + ) + || substr_compare($tokens->currentTokenValue(), $endTag, $endTagSearchOffset) === 0 ) { return true; } @@ -456,10 +479,52 @@ 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 = ''; + } + + if ($name === '') { + throw new LogicException('Template tag name cannot be empty.'); + } + + 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); @@ -484,7 +549,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 + ); } @@ -637,14 +747,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; @@ -662,11 +772,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 d7feaf91..044d07f8 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -20,6 +20,9 @@ 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; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocChildNode; @@ -28,6 +31,8 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\SelfOutTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; @@ -97,6 +102,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' => ', ', @@ -135,7 +141,6 @@ final class Printer CallableTypeNode::class, UnionTypeNode::class, IntersectionTypeNode::class, - ConstTypeNode::class, NullableTypeNode::class, ], ]; @@ -283,6 +288,14 @@ private function printTagValue(PhpDocTagValueNode $node): string $type = $this->printType($node->type); return trim("{$type} {$node->description}"); } + if ($node instanceof RequireExtendsTagValueNode) { + $type = $this->printType($node->type); + return trim("{$type} {$node->description}"); + } + if ($node instanceof RequireImplementsTagValueNode) { + $type = $this->printType($node->type); + return trim("{$type} {$node->description}"); + } if ($node instanceof ParamOutTagValueNode) { $type = $this->printType($node->type); return trim("{$type} {$node->parameterName} {$node->description}"); @@ -293,6 +306,15 @@ 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 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}"); @@ -370,10 +392,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( @@ -484,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); @@ -795,6 +821,10 @@ private function printNodeFormatPreserving(Node $node, TokenIterator $originalTo throw new LogicException(); } + if ($subEndPos < $subStartPos) { + return $this->print($node); + } + if ($subNode === null) { return $this->print($node); } diff --git a/tests/PHPStan/Ast/ToString/PhpDocToStringTest.php b/tests/PHPStan/Ast/ToString/PhpDocToStringTest.php index faac0616..b57b7db6 100644 --- a/tests/PHPStan/Ast/ToString/PhpDocToStringTest.php +++ b/tests/PHPStan/Ast/ToString/PhpDocToStringTest.php @@ -28,6 +28,8 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\SelfOutTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; @@ -213,6 +215,15 @@ public static function provideClassCases(): Generator ['Foo\\Bar Baz', new MixinTagValueNode(new IdentifierTypeNode('Foo\\Bar'), 'Baz')], ]; + yield from [ + ['PHPUnit\\TestCase', new RequireExtendsTagValueNode(new IdentifierTypeNode('PHPUnit\\TestCase'), '')], + ['Foo\\Bar Baz', new RequireExtendsTagValueNode(new IdentifierTypeNode('Foo\\Bar'), 'Baz')], + ]; + yield from [ + ['PHPUnit\\TestCase', new RequireImplementsTagValueNode(new IdentifierTypeNode('PHPUnit\\TestCase'), '')], + ['Foo\\Bar Baz', new RequireImplementsTagValueNode(new IdentifierTypeNode('Foo\\Bar'), 'Baz')], + ]; + yield from [ ['Foo array', new TypeAliasTagValueNode('Foo', $arrayOfStrings)], ['Test from Foo\Bar', new TypeAliasImportTagValueNode('Test', $bar, null)], diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 97d6cbfe..8085ec64 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -29,12 +29,17 @@ 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; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\SelfOutTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; @@ -95,11 +100,16 @@ protected function setUp(): void * @dataProvider provideTagsWithNumbers * @dataProvider provideSpecializedTags * @dataProvider provideParamTagsData + * @dataProvider provideParamImmediatelyInvokedCallableTagsData + * @dataProvider provideParamLaterInvokedCallableTagsData * @dataProvider provideTypelessParamTagsData + * @dataProvider provideParamClosureThisTagsData * @dataProvider provideVarTagsData * @dataProvider provideReturnTagsData * @dataProvider provideThrowsTagsData * @dataProvider provideMixinTagsData + * @dataProvider provideRequireExtendsTagsData + * @dataProvider provideRequireImplementsTagsData * @dataProvider provideDeprecatedTagsData * @dataProvider providePropertyTagsData * @dataProvider provideMethodTagsData @@ -117,6 +127,7 @@ protected function setUp(): void * @dataProvider provideParamOutTagsData * @dataProvider provideDoctrineData * @dataProvider provideDoctrineWithoutDoctrineCheckData + * @dataProvider provideCommentLikeDescriptions */ public function testParse( string $label, @@ -615,6 +626,116 @@ 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 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 [ @@ -1300,6 +1421,20 @@ public function provideReturnTagsData(): Iterator ]), ]; + yield [ + 'OK with HTML description', + '/** @return MongoCollection

Returns a collection object representing the new collection.

*/', + new PhpDocNode([ + new PhpDocTagNode( + '@return', + new ReturnTagValueNode( + new IdentifierTypeNode('MongoCollection'), + '

Returns a collection object representing the new collection.

' + ) + ), + ]), + ]; + yield [ 'invalid without type and description', '/** @return */', @@ -1893,6 +2028,138 @@ public function provideMixinTagsData(): Iterator ]; } + public function provideRequireExtendsTagsData(): Iterator + { + yield [ + 'OK without description', + '/** @phpstan-require-extends Foo */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-require-extends', + new RequireExtendsTagValueNode( + new IdentifierTypeNode('Foo'), + '' + ) + ), + ]), + ]; + + yield [ + 'OK with description', + '/** @phpstan-require-extends Foo optional description */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-require-extends', + new RequireExtendsTagValueNode( + new IdentifierTypeNode('Foo'), + 'optional description' + ) + ), + ]), + ]; + + yield [ + 'OK with psalm-prefix description', + '/** @psalm-require-extends Foo optional description */', + new PhpDocNode([ + new PhpDocTagNode( + '@psalm-require-extends', + new RequireExtendsTagValueNode( + new IdentifierTypeNode('Foo'), + 'optional description' + ) + ), + ]), + ]; + + yield [ + 'invalid without type and description', + '/** @phpstan-require-extends */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-require-extends', + new InvalidTagValueNode( + '', + new ParserException( + '*/', + Lexer::TOKEN_CLOSE_PHPDOC, + 29, + Lexer::TOKEN_IDENTIFIER, + null, + 1 + ) + ) + ), + ]), + ]; + } + + public function provideRequireImplementsTagsData(): Iterator + { + yield [ + 'OK without description', + '/** @phpstan-require-implements Foo */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-require-implements', + new RequireImplementsTagValueNode( + new IdentifierTypeNode('Foo'), + '' + ) + ), + ]), + ]; + + yield [ + 'OK with description', + '/** @phpstan-require-implements Foo optional description */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-require-implements', + new RequireImplementsTagValueNode( + new IdentifierTypeNode('Foo'), + 'optional description' + ) + ), + ]), + ]; + + yield [ + 'OK with psalm-prefix description', + '/** @psalm-require-implements Foo optional description */', + new PhpDocNode([ + new PhpDocTagNode( + '@psalm-require-implements', + new RequireImplementsTagValueNode( + new IdentifierTypeNode('Foo'), + 'optional description' + ) + ), + ]), + ]; + + yield [ + 'invalid without type and description', + '/** @phpstan-require-implements */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-require-implements', + new InvalidTagValueNode( + '', + new ParserException( + '*/', + Lexer::TOKEN_CLOSE_PHPDOC, + 32, + Lexer::TOKEN_IDENTIFIER, + null, + 1 + ) + ) + ), + ]), + ]; + } + public function provideDeprecatedTagsData(): Iterator { yield [ @@ -2475,6 +2742,26 @@ public function provideMethodTagsData(): Iterator ), ]), ]; + + yield [ + 'OK non-static with return type that starts with static type', + '/** @method static|null foo() */', + new PhpDocNode([ + new PhpDocTagNode( + '@method', + new MethodTagValueNode( + false, + new UnionTypeNode([ + new IdentifierTypeNode('static'), + new IdentifierTypeNode('null'), + ]), + 'foo', + [], + '' + ) + ), + ]), + ]; } @@ -5561,6 +5848,98 @@ public function provideSelfOutTagsData(): Iterator ]; } + public function provideCommentLikeDescriptions(): Iterator + { + yield [ + 'Comment after @param', + '/** @param int $a // this is a description */', + new PhpDocNode([ + new PhpDocTagNode('@param', new ParamTagValueNode( + new IdentifierTypeNode('int'), + false, + '$a', + '// this is a description' + )), + ]), + ]; + + yield [ + 'Comment on a separate line', + '/**' . PHP_EOL . + ' * @param int $a' . PHP_EOL . + ' * // this is a comment' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTagNode('@param', new ParamTagValueNode( + new IdentifierTypeNode('int'), + false, + '$a', + '' + )), + new PhpDocTextNode('// this is a comment'), + ]), + ]; + yield [ + 'Comment on a separate line 2', + '/**' . PHP_EOL . + ' * @param int $a' . PHP_EOL . + ' *' . PHP_EOL . + ' * // this is a comment' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTagNode('@param', new ParamTagValueNode( + new IdentifierTypeNode('int'), + false, + '$a', + '' + )), + new PhpDocTextNode(''), + new PhpDocTextNode('// this is a comment'), + ]), + ]; + yield [ + 'Comment after Doctrine tag 1', + '/** @ORM\Doctrine // this is a description */', + new PhpDocNode([ + new PhpDocTagNode('@ORM\Doctrine', new GenericTagValueNode('// this is a description')), + ]), + ]; + yield [ + 'Comment after Doctrine tag 2', + '/** @\ORM\Doctrine // this is a description */', + new PhpDocNode([ + new PhpDocTagNode('@\ORM\Doctrine', new DoctrineTagValueNode( + new DoctrineAnnotation('@\ORM\Doctrine', []), + '// this is a description' + )), + ]), + ]; + yield [ + 'Comment after Doctrine tag 3', + '/** @\ORM\Doctrine() // this is a description */', + new PhpDocNode([ + new PhpDocTagNode('@\ORM\Doctrine', new DoctrineTagValueNode( + new DoctrineAnnotation('@\ORM\Doctrine', []), + '// this is a description' + )), + ]), + ]; + yield [ + 'Comment after Doctrine tag 4', + '/** @\ORM\Doctrine() @\ORM\Entity() // this is a description */', + new PhpDocNode([ + new PhpDocTagNode('@\ORM\Doctrine', new DoctrineTagValueNode( + new DoctrineAnnotation('@\ORM\Doctrine', []), + '' + )), + new PhpDocTagNode('@\ORM\Entity', new DoctrineTagValueNode( + new DoctrineAnnotation('@\ORM\Entity', []), + '// this is a description' + )), + ]), + ]; + } + public function provideParamOutTagsData(): Iterator { yield [ @@ -6658,6 +7037,13 @@ public function dataLinesAndIndexes(): iterable [1, 1, 1, 3], ], ]; + + yield [ + '/** @api */', + [ + [1, 1, 1, 1], + ], + ]; } /** @@ -6732,6 +7118,15 @@ public function dataDeepNodesLinesAndIndexes(): iterable [2, 4, 4, 6], // DoctrineArray ], ]; + + yield [ + '/** @api */', + [ + [1, 1, 0, 3], + [1, 1, 1, 1], + [1, 1, 3, 1], // GenericTagValueNode is empty so start index is higher than end index + ], + ]; } @@ -6838,6 +7233,9 @@ public function testReturnTypeLinesAndIndexes(string $phpDoc, array $lines): voi * @dataProvider provideSpecializedTags * @dataProvider provideParamTagsData * @dataProvider provideTypelessParamTagsData + * @dataProvider provideParamImmediatelyInvokedCallableTagsData + * @dataProvider provideParamLaterInvokedCallableTagsData + * @dataProvider provideParamClosureThisTagsData * @dataProvider provideVarTagsData * @dataProvider provideReturnTagsData * @dataProvider provideThrowsTagsData diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 2c66d98d..d6c66bb8 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -10,6 +10,7 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\QuoteAwareConstExprStringNode; use PHPStan\PhpDocParser\Ast\Node; use PHPStan\PhpDocParser\Ast\NodeTraverser; +use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; @@ -767,6 +768,22 @@ public function provideParseData(): array new IdentifierTypeNode('Foo') ), ], + [ + 'pure-callable(): Foo', + new CallableTypeNode( + new IdentifierTypeNode('pure-callable'), + [], + new IdentifierTypeNode('Foo') + ), + ], + [ + 'pure-Closure(): Foo', + new CallableTypeNode( + new IdentifierTypeNode('pure-Closure'), + [], + new IdentifierTypeNode('Foo') + ), + ], [ 'callable(): ?Foo', new CallableTypeNode( @@ -897,6 +914,104 @@ public function provideParseData(): array new IdentifierTypeNode('Foo') ), ], + [ + 'callable(B): C', + new CallableTypeNode( + new IdentifierTypeNode('callable'), + [ + new CallableTypeParameterNode( + new IdentifierTypeNode('B'), + false, + false, + '', + false + ), + ], + new IdentifierTypeNode('C'), + [ + new TemplateTagValueNode('A', null, ''), + ] + ), + ], + [ + 'callable<>(): void', + new ParserException( + '>', + Lexer::TOKEN_END, + 9, + Lexer::TOKEN_IDENTIFIER + ), + ], + [ + '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([ @@ -952,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( @@ -2166,6 +2288,11 @@ public function provideParseData(): array false )), ], + [ + 'MongoCollection

Returns a collection object representing the new collection.

', + new IdentifierTypeNode('MongoCollection'), + Lexer::TOKEN_OPEN_ANGLE_BRACKET, + ], ]; } diff --git a/tests/PHPStan/Printer/PhpPrinterIndentationDetectorVisitor.php b/tests/PHPStan/Printer/PhpPrinterIndentationDetectorVisitor.php index 9e2b9248..58a807eb 100644 --- a/tests/PHPStan/Printer/PhpPrinterIndentationDetectorVisitor.php +++ b/tests/PHPStan/Printer/PhpPrinterIndentationDetectorVisitor.php @@ -4,8 +4,8 @@ use PhpParser\Internal\TokenStream; use PhpParser\Node; +use PhpParser\NodeTraverser; use PhpParser\NodeVisitorAbstract; -use PHPStan\PhpDocParser\Ast\NodeTraverser; use function count; use function preg_match; use function preg_match_all; diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 746ad027..07d68af0 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -17,6 +17,9 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineArray; use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineArrayItem; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamClosureThisTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; @@ -84,6 +87,7 @@ public function dataPrintFormatPreserving(): iterable }; yield ['/** */', '/** */', $noopVisitor]; + yield ['/** @api */', '/** @api */', $noopVisitor]; yield ['/** */', '/** */', $noopVisitor]; @@ -589,6 +593,35 @@ public function enterNode(Node $node) }; + $addCallableTemplateType = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof CallableTypeNode) { + $node->templateTypes[] = new TemplateTagValueNode( + 'T', + new IdentifierTypeNode('int'), + '' + ); + } + + return $node; + } + + }; + + yield [ + '/** @var Closure(): T */', + '/** @var Closure(): T */', + $addCallableTemplateType, + ]; + + yield [ + '/** @var \Closure(U): T */', + '/** @var \Closure(U): T */', + $addCallableTemplateType, + ]; + yield [ '/** * @param callable(): void $cb @@ -1635,6 +1668,78 @@ 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; + } + + }, + ]; + + 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; + } + + }, + ]; + + 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; + } + + }, + ]; } /** @@ -1687,7 +1792,7 @@ public function enterNode(Node $node) } /** - * @return iterable + * @return iterable */ public function dataPrintType(): iterable { @@ -1774,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]', + ]; } /** @@ -1793,7 +1905,7 @@ public function testPrintType(TypeNode $node, string $expectedResult): void } /** - * @return iterable + * @return iterable */ public function dataPrintPhpDocNode(): iterable {