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')))), + ], ]; }