diff --git a/.github/workflows/backward-compatibility.yml b/.github/workflows/backward-compatibility.yml index a7abd897..1cd62723 100644 --- a/.github/workflows/backward-compatibility.yml +++ b/.github/workflows/backward-compatibility.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "1.9.x" + - "1.21.x" jobs: backward-compatibility: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b6ed9990..19401af3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "1.9.x" + - "1.21.x" jobs: lint: diff --git a/.github/workflows/test-slevomat-coding-standard.yml b/.github/workflows/test-slevomat-coding-standard.yml index cc9e1b0b..ab90c303 100644 --- a/.github/workflows/test-slevomat-coding-standard.yml +++ b/.github/workflows/test-slevomat-coding-standard.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "1.9.x" + - "1.21.x" jobs: tests: diff --git a/build-cs/composer.lock b/build-cs/composer.lock index 0cea3f9e..c59ffc5f 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -154,16 +154,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.20.2", + "version": "1.20.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "90490bd8fd8530a272043c4950c180b6d0cf5f81" + "reference": "6c04009f6cae6eda2f040745b6b846080ef069c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/90490bd8fd8530a272043c4950c180b6d0cf5f81", - "reference": "90490bd8fd8530a272043c4950c180b6d0cf5f81", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/6c04009f6cae6eda2f040745b6b846080ef069c2", + "reference": "6c04009f6cae6eda2f040745b6b846080ef069c2", "shasum": "" }, "require": { @@ -193,22 +193,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.20.2" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.20.3" }, - "time": "2023-04-22T12:59:35+00:00" + "time": "2023-04-25T09:01:03+00:00" }, { "name": "slevomat/coding-standard", - "version": "8.11.0", + "version": "8.11.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "91428d5bcf7db93a842bcf97f465edf62527f3ea" + "reference": "af87461316b257e46e15bb041dca6fca3796d822" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/91428d5bcf7db93a842bcf97f465edf62527f3ea", - "reference": "91428d5bcf7db93a842bcf97f465edf62527f3ea", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/af87461316b257e46e15bb041dca6fca3796d822", + "reference": "af87461316b257e46e15bb041dca6fca3796d822", "shasum": "" }, "require": { @@ -248,7 +248,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.11.0" + "source": "https://github.com/slevomat/coding-standard/tree/8.11.1" }, "funding": [ { @@ -260,7 +260,7 @@ "type": "tidelift" } ], - "time": "2023-04-21T15:51:44+00:00" + "time": "2023-04-24T08:19:01+00:00" }, { "name": "squizlabs/php_codesniffer", diff --git a/composer.json b/composer.json index 30b879b7..aab3969f 100644 --- a/composer.json +++ b/composer.json @@ -12,8 +12,8 @@ "phpstan/phpstan": "^1.5", "phpstan/phpstan-phpunit": "^1.1", "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5", - "symfony/process": "^5.2" + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.0" }, "config": { "platform": { diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index 7f0e4a44..d3eed465 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -333,9 +333,7 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph private function parseParamTagValue(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode { if ( - $tokens->isCurrentTokenType(Lexer::TOKEN_REFERENCE) - || $tokens->isCurrentTokenType(Lexer::TOKEN_VARIADIC) - || $tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE) + $tokens->isCurrentTokenType(Lexer::TOKEN_REFERENCE, Lexer::TOKEN_VARIADIC, Lexer::TOKEN_VARIABLE) ) { $type = null; } else { diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index ff62fca3..4b429809 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -522,47 +522,126 @@ private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNo $startLine = $tokens->currentTokenLine(); $startIndex = $tokens->currentTokenIndex(); if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) { - $type = $this->parseNullable($tokens); + return $this->parseNullable($tokens); } elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) { $type = $this->parse($tokens); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES); + if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { + $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); + } - } else { - $type = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue()); - $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); - - if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { - $type = $this->parseGeneric( - $tokens, - $this->enrichWithAttributes( - $tokens, - $type, - $startLine, - $startIndex - ) - ); - - } elseif (in_array($type->name, ['array', 'list'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { - $type = $this->parseArrayShape($tokens, $this->enrichWithAttributes( + return $type; + } elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_THIS_VARIABLE)) { + $type = new Ast\Type\ThisTypeNode(); + if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { + $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes( $tokens, $type, $startLine, $startIndex - ), $type->name); + )); + } + + return $type; + } else { + $currentTokenValue = $tokens->currentTokenValue(); + $tokens->pushSavePoint(); // because of ConstFetchNode + if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) { + $type = new Ast\Type\IdentifierTypeNode($currentTokenValue); + + if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) { + if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { + $type = $this->parseGeneric( + $tokens, + $this->enrichWithAttributes( + $tokens, + $type, + $startLine, + $startIndex + ) + ); + if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { + $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes( + $tokens, + $type, + $startLine, + $startIndex + )); + } + + } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { + $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes( + $tokens, + $type, + $startLine, + $startIndex + )); + + } elseif (in_array($type->name, ['array', 'list', 'object'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { + if ($type->name === 'object') { + $type = $this->parseObjectShape($tokens); + } else { + $type = $this->parseArrayShape($tokens, $this->enrichWithAttributes( + $tokens, + $type, + $startLine, + $startIndex + ), $type->name); + } + + if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { + $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes( + $tokens, + $type, + $startLine, + $startIndex + )); + } + } + + return $type; + } else { + $tokens->rollback(); // because of ConstFetchNode + } + } else { + $tokens->dropSavePoint(); // because of ConstFetchNode } } - if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { - $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes( - $tokens, - $type, - $startLine, - $startIndex - )); + $exception = new ParserException( + $tokens->currentTokenValue(), + $tokens->currentTokenType(), + $tokens->currentTokenOffset(), + Lexer::TOKEN_IDENTIFIER, + null, + $tokens->currentTokenLine() + ); + + if ($this->constExprParser === null) { + throw $exception; } - return $type; + try { + $constExpr = $this->constExprParser->parse($tokens, true); + if ($constExpr instanceof Ast\ConstExpr\ConstExprArrayNode) { + throw $exception; + } + + $type = new Ast\Type\ConstTypeNode($constExpr); + if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { + $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes( + $tokens, + $type, + $startLine, + $startIndex + )); + } + + return $type; + } catch (LogicException $e) { + throw $exception; + } } diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 2af4e1d4..74a27be4 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -770,6 +770,22 @@ public function provideParseData(): array ) ), ], + [ + 'callable(): Foo[]', + new CallableTypeNode( + new IdentifierTypeNode('callable'), + [], + new ArrayTypeNode(new GenericTypeNode( + new IdentifierTypeNode('Foo'), + [ + new IdentifierTypeNode('Bar'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + ] + )) + ), + ], [ 'callable(): Foo|Bar', new UnionTypeNode([ @@ -1956,6 +1972,77 @@ public function provideParseData(): array 'callable(): ?int', new CallableTypeNode(new IdentifierTypeNode('callable'), [], new NullableTypeNode(new IdentifierTypeNode('int'))), ], + [ + 'callable(): object{foo: int}', + new CallableTypeNode(new IdentifierTypeNode('callable'), [], new ObjectShapeNode([ + new ObjectShapeItemNode(new IdentifierTypeNode('foo'), false, new IdentifierTypeNode('int')), + ])), + ], + [ + 'callable(): object{foo: int}[]', + new CallableTypeNode( + new IdentifierTypeNode('callable'), + [], + new ArrayTypeNode( + new ObjectShapeNode([ + new ObjectShapeItemNode(new IdentifierTypeNode('foo'), false, new IdentifierTypeNode('int')), + ]) + ) + ), + ], + [ + 'callable(): $this', + new CallableTypeNode(new IdentifierTypeNode('callable'), [], new ThisTypeNode()), + ], + [ + 'callable(): $this[]', + new CallableTypeNode(new IdentifierTypeNode('callable'), [], new ArrayTypeNode(new ThisTypeNode())), + ], + [ + '2.5|3', + new UnionTypeNode([ + new ConstTypeNode(new ConstExprFloatNode('2.5')), + new ConstTypeNode(new ConstExprIntegerNode('3')), + ]), + ], + [ + 'callable(): 3.5', + new CallableTypeNode(new IdentifierTypeNode('callable'), [], new ConstTypeNode(new ConstExprFloatNode('3.5'))), + ], + [ + 'callable(): 3.5[]', + new CallableTypeNode(new IdentifierTypeNode('callable'), [], new ArrayTypeNode( + new ConstTypeNode(new ConstExprFloatNode('3.5')) + )), + ], + [ + 'callable(): Foo', + new CallableTypeNode(new IdentifierTypeNode('callable'), [], new IdentifierTypeNode('Foo')), + ], + [ + 'callable(): (Foo)[]', + new CallableTypeNode(new IdentifierTypeNode('callable'), [], new ArrayTypeNode(new IdentifierTypeNode('Foo'))), + ], + [ + 'callable(): Foo::BAR', + new CallableTypeNode(new IdentifierTypeNode('callable'), [], new ConstTypeNode(new ConstFetchNode('Foo', 'BAR'))), + ], + [ + 'callable(): Foo::*', + new CallableTypeNode(new IdentifierTypeNode('callable'), [], new ConstTypeNode(new ConstFetchNode('Foo', '*'))), + ], + [ + '?Foo[]', + new NullableTypeNode(new ArrayTypeNode(new IdentifierTypeNode('Foo'))), + ], + [ + 'callable(): ?Foo', + new CallableTypeNode(new IdentifierTypeNode('callable'), [], new NullableTypeNode(new IdentifierTypeNode('Foo'))), + ], + [ + 'callable(): ?Foo[]', + new CallableTypeNode(new IdentifierTypeNode('callable'), [], new NullableTypeNode(new ArrayTypeNode(new IdentifierTypeNode('Foo')))), + ], ]; } diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index b689b440..ea236b75 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -26,6 +26,7 @@ use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode; use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\ConstExprParser; @@ -42,6 +43,26 @@ class PrinterTest extends TestCase { + /** @var TypeParser */ + private $typeParser; + + /** @var PhpDocParser */ + private $phpDocParser; + + protected function setUp(): void + { + $usedAttributes = ['lines' => true, 'indexes' => true]; + $constExprParser = new ConstExprParser(true, true, $usedAttributes); + $this->typeParser = new TypeParser($constExprParser, true, $usedAttributes); + $this->phpDocParser = new PhpDocParser( + $this->typeParser, + $constExprParser, + true, + true, + $usedAttributes + ); + } + /** * @return iterable */ @@ -1153,18 +1174,9 @@ public function enterNode(Node $node) */ public function testPrintFormatPreserving(string $phpDoc, string $expectedResult, NodeVisitor $visitor): void { - $usedAttributes = ['lines' => true, 'indexes' => true]; - $constExprParser = new ConstExprParser(true, true, $usedAttributes); - $phpDocParser = new PhpDocParser( - new TypeParser($constExprParser, true, $usedAttributes), - $constExprParser, - true, - true, - $usedAttributes - ); $lexer = new Lexer(); $tokens = new TokenIterator($lexer->tokenize($phpDoc)); - $phpDocNode = $phpDocParser->parse($tokens); + $phpDocNode = $this->phpDocParser->parse($tokens); $cloningTraverser = new NodeTraverser([new NodeVisitor\CloningVisitor()]); $newNodes = $cloningTraverser->traverse([$phpDocNode]); @@ -1177,14 +1189,13 @@ public function testPrintFormatPreserving(string $phpDoc, string $expectedResult $newPhpDoc = $printer->printFormatPreserving($newNode, $phpDocNode, $tokens); $this->assertSame($expectedResult, $newPhpDoc); - $newTokens = new TokenIterator($lexer->tokenize($newPhpDoc)); $this->assertEquals( $this->unsetAttributes($newNode), - $this->unsetAttributes($phpDocParser->parse($newTokens)) + $this->unsetAttributes($this->phpDocParser->parse(new TokenIterator($lexer->tokenize($newPhpDoc)))) ); } - private function unsetAttributes(PhpDocNode $node): PhpDocNode + private function unsetAttributes(Node $node): Node { $visitor = new class extends AbstractNodeVisitor { @@ -1207,4 +1218,67 @@ public function enterNode(Node $node) return $traverser->traverse([$node])[0]; } + /** + * @return iterable + */ + public function dataPrintType(): iterable + { + yield [ + new IdentifierTypeNode('int'), + 'int', + ]; + } + + /** + * @dataProvider dataPrintType + */ + public function testPrintType(TypeNode $node, string $expectedResult): void + { + $printer = new Printer(); + $phpDoc = $printer->print($node); + $this->assertSame($expectedResult, $phpDoc); + + $lexer = new Lexer(); + $this->assertEquals( + $this->unsetAttributes($node), + $this->unsetAttributes($this->typeParser->parse(new TokenIterator($lexer->tokenize($phpDoc)))) + ); + } + + /** + * @return iterable + */ + public function dataPrintPhpDocNode(): iterable + { + yield [ + new PhpDocNode([ + new PhpDocTagNode('@param', new ParamTagValueNode( + new IdentifierTypeNode('int'), + false, + '$a', + '' + )), + ]), + '/** + * @param int $a + */', + ]; + } + + /** + * @dataProvider dataPrintPhpDocNode + */ + public function testPrintPhpDocNode(PhpDocNode $node, string $expectedResult): void + { + $printer = new Printer(); + $phpDoc = $printer->print($node); + $this->assertSame($expectedResult, $phpDoc); + + $lexer = new Lexer(); + $this->assertEquals( + $this->unsetAttributes($node), + $this->unsetAttributes($this->phpDocParser->parse(new TokenIterator($lexer->tokenize($phpDoc)))) + ); + } + }