diff --git a/src/Parser.php b/src/Parser.php index 2139ee1a..7033d842 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -828,11 +828,22 @@ private function parseParameterFn() { $parameter->visibilityToken = $this->eatOptional([TokenKind::PublicKeyword, TokenKind::ProtectedKeyword, TokenKind::PrivateKeyword]); $parameter->questionToken = $this->eatOptional1(TokenKind::QuestionToken); $parameter->typeDeclarationList = $this->tryParseParameterTypeDeclarationList($parameter); - if ($parameter->questionToken && !$parameter->typeDeclarationList) { + if ($parameter->typeDeclarationList) { + $children = $parameter->typeDeclarationList->children; + if (end($children) instanceof MissingToken && ($children[count($children) - 2]->kind ?? null) === TokenKind::AmpersandToken) { + array_pop($parameter->typeDeclarationList->children); + $parameter->byRefToken = array_pop($parameter->typeDeclarationList->children); + if (!$parameter->typeDeclarationList->children) { + unset($parameter->typeDeclarationList); + } + } + } elseif ($parameter->questionToken) { // TODO ParameterType? $parameter->typeDeclarationList = new MissingToken(TokenKind::PropertyType, $this->token->fullStart); } - $parameter->byRefToken = $this->eatOptional1(TokenKind::AmpersandToken); + if (!$parameter->byRefToken) { + $parameter->byRefToken = $this->eatOptional1(TokenKind::AmpersandToken); + } // TODO add post-parse rule that prevents assignment // TODO add post-parse rule that requires only last parameter be variadic $parameter->dotDotDotToken = $this->eatOptional1(TokenKind::DotDotDotToken); @@ -858,6 +869,11 @@ private function parseAndSetReturnTypeDeclarationList($parentNode) { $parentNode->returnTypeList = $returnTypeList; } + const TYPE_DELIMITER_TOKENS = [ + TokenKind::BarToken, + TokenKind::AmpersandToken, + ]; + /** * Attempt to parse the return type after the `:` and optional `?` token. * @@ -866,7 +882,7 @@ private function parseAndSetReturnTypeDeclarationList($parentNode) { private function parseReturnTypeDeclarationList($parentNode) { $result = $this->parseDelimitedList( DelimitedList\QualifiedNameList::class, - TokenKind::BarToken, + self::TYPE_DELIMITER_TOKENS, function ($token) { return \in_array($token->kind, $this->returnTypeDeclarationTokens, true) || $this->isQualifiedNameStart($token); }, @@ -878,7 +894,7 @@ function ($parentNode) { // Add a MissingToken so that this will warn about `function () : T| {}` // TODO: Make this a reusable abstraction? - if ($result && (end($result->children)->kind ?? null) === TokenKind::BarToken) { + if ($result && in_array(end($result->children)->kind ?? null, self::TYPE_DELIMITER_TOKENS)) { $result->children[] = new MissingToken(TokenKind::ReturnType, $this->token->fullStart); } return $result; @@ -902,7 +918,7 @@ private function tryParseParameterTypeDeclaration($parentNode) { private function tryParseParameterTypeDeclarationList($parentNode) { $result = $this->parseDelimitedList( DelimitedList\QualifiedNameList::class, - TokenKind::BarToken, + self::TYPE_DELIMITER_TOKENS, function ($token) { return \in_array($token->kind, $this->parameterTypeDeclarationTokens, true) || $this->isQualifiedNameStart($token); }, @@ -914,7 +930,7 @@ function ($parentNode) { // Add a MissingToken so that this will Warn about `function (T| $x) {}` // TODO: Make this a reusable abstraction? - if ($result && (end($result->children)->kind ?? null) === TokenKind::BarToken) { + if ($result && in_array(end($result->children)->kind ?? null, self::TYPE_DELIMITER_TOKENS)) { $result->children[] = new MissingToken(TokenKind::Name, $this->token->fullStart); } return $result; diff --git a/src/PhpTokenizer.php b/src/PhpTokenizer.php index 95faed3e..598cf674 100644 --- a/src/PhpTokenizer.php +++ b/src/PhpTokenizer.php @@ -16,6 +16,8 @@ define(__NAMESPACE__ . '\T_ATTRIBUTE', defined('T_ATTRIBUTE') ? constant('T_ATTRIBUTE') : 'T_ATTRIBUTE'); // If this predates PHP 8.1, T_ENUM is unavailable. The replacement value is arbitrary - it just has to be different from other values of token constants. define(__NAMESPACE__ . '\T_ENUM', defined('T_ENUM') ? constant('T_ENUM') : 'T_ENUM'); +define(__NAMESPACE__ . '\T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG', defined('T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG') ? constant('T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG') : 'T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG'); +define(__NAMESPACE__ . '\T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG', defined('T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG') ? constant('T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG') : 'T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG'); /** * Tokenizes content using PHP's built-in `token_get_all`, and converts to "lightweight" Token representation. @@ -338,6 +340,8 @@ protected static function tokenGetAll(string $content, $parseContext): array "^" => TokenKind::CaretToken, "|" => TokenKind::BarToken, "&" => TokenKind::AmpersandToken, + T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG => TokenKind::AmpersandToken, + T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG => TokenKind::AmpersandToken, T_BOOLEAN_AND => TokenKind::AmpersandAmpersandToken, T_BOOLEAN_OR => TokenKind::BarBarToken, ":" => TokenKind::ColonToken, diff --git a/tests/cases/parser81/intersection_type.php b/tests/cases/parser81/intersection_type.php new file mode 100644 index 00000000..428b9845 --- /dev/null +++ b/tests/cases/parser81/intersection_type.php @@ -0,0 +1,9 @@ +