Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add generics support #1

Merged
merged 1 commit into from
Sep 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "nikic/php-parser",
"name": "mrsuh/php-parser",
"type": "library",
"description": "A PHP parser written in PHP",
"keywords": [
Expand Down
51 changes: 40 additions & 11 deletions grammar/php7.y
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
%pure_parser
%expect 2
%expect 8

%tokens

Expand Down Expand Up @@ -33,7 +33,7 @@ reserved_non_modifiers:
| T_FUNCTION | T_CONST | T_RETURN | T_PRINT | T_YIELD | T_LIST | T_SWITCH | T_ENDSWITCH | T_CASE | T_DEFAULT
| T_BREAK | T_ARRAY | T_CALLABLE | T_EXTENDS | T_IMPLEMENTS | T_NAMESPACE | T_TRAIT | T_INTERFACE | T_CLASS
| T_CLASS_C | T_TRAIT_C | T_FUNC_C | T_METHOD_C | T_LINE | T_FILE | T_DIR | T_NS_C | T_HALT_COMPILER | T_FN
| T_MATCH | T_ENUM
| T_MATCH | T_ENUM | T_GENERIC_PARAMETER_COVARIANT | T_GENERIC_PARAMETER_CONTRAVARIANT
;

semi_reserved:
Expand Down Expand Up @@ -350,6 +350,35 @@ block_or_error:
| error { $$ = []; }
;

optional_generic_variant:
/* empty */ { $$ = NULL; }
| T_GENERIC_PARAMETER_COVARIANT { $$ = Node\GenericParameter::COVARIANT; }
| T_GENERIC_PARAMETER_CONTRAVARIANT { $$ = Node\GenericParameter::CONTRAVARIANT; }
;

optional_generic_params:
/* empty */ { $$ = NULL; }
| '<' generic_params '>' { $$ = $2; }
| T_IS_NOT_EQUAL { $$ = []; } /* hack for symbol <> */
;

generic_params:
optional_generic_variant generic_param { init($2); $2->setVariance($1); }
| generic_params ',' optional_generic_variant generic_param { push($1, $4); $4->setVariance($3); }
;

generic_param:
generic_type { $$ = Node\GenericParameter[$1]; }
| generic_type ':' generic_type { $$ = Node\GenericParameter[$1]; $$->setConstraint($3); }
| generic_type '=' generic_type { $$ = Node\GenericParameter[$1]; $$->setDefault($3); }
| generic_type ':' generic_type '=' generic_type { $$ = Node\GenericParameter[$1]; $$->setConstraint($3); $$->setDefault($5); }
;

generic_type:
T_STRING { $$ = $1; }
| T_ARRAY { $$ = $1; }
| T_CALLABLE { $$ = $1; }

function_declaration_statement:
T_FUNCTION optional_ref identifier '(' parameter_list ')' optional_return_type block_or_error
{ $$ = Stmt\Function_[$3, ['byRef' => $2, 'params' => $5, 'returnType' => $7, 'stmts' => $8, 'attrGroups' => []]]; }
Expand All @@ -358,14 +387,14 @@ function_declaration_statement:
;

class_declaration_statement:
optional_attributes class_entry_type identifier extends_from implements_list '{' class_statement_list '}'
{ $$ = Stmt\Class_[$3, ['type' => $2, 'extends' => $4, 'implements' => $5, 'stmts' => $7, 'attrGroups' => $1]];
optional_attributes class_entry_type identifier optional_generic_params extends_from implements_list '{' class_statement_list '}'
{ $$ = Stmt\Class_[$3, ['type' => $2, 'extends' => $5, 'implements' => $6, 'stmts' => $8, 'attrGroups' => $1], ['generics' => $4 ]];
$this->checkClass($$, #3); }
| optional_attributes T_INTERFACE identifier interface_extends_list '{' class_statement_list '}'
{ $$ = Stmt\Interface_[$3, ['extends' => $4, 'stmts' => $6, 'attrGroups' => $1]];
| optional_attributes T_INTERFACE identifier optional_generic_params interface_extends_list '{' class_statement_list '}'
{ $$ = Stmt\Interface_[$3, ['extends' => $5, 'stmts' => $7, 'attrGroups' => $1], ['generics' => $4 ]];
$this->checkInterface($$, #3); }
| optional_attributes T_TRAIT identifier '{' class_statement_list '}'
{ $$ = Stmt\Trait_[$3, ['stmts' => $5, 'attrGroups' => $1]]; }
| optional_attributes T_TRAIT identifier optional_generic_params '{' class_statement_list '}'
{ $$ = Stmt\Trait_[$3, ['stmts' => $6, 'attrGroups' => $1], ['generics' => $4 ]]; }
| optional_attributes T_ENUM identifier enum_scalar_type implements_list '{' class_statement_list '}'
{ $$ = Stmt\Enum_[$3, ['scalarType' => $4, 'implements' => $5, 'stmts' => $7, 'attrGroups' => $1]];
$this->checkEnum($$, #3); }
Expand Down Expand Up @@ -570,7 +599,7 @@ type:
;

type_without_static:
name { $$ = $this->handleBuiltinTypes($1); }
name optional_generic_params { $$ = $this->handleBuiltinTypes($1); $$->setAttribute('generics', $2); }
| T_ARRAY { $$ = Node\Identifier['array']; }
| T_CALLABLE { $$ = Node\Identifier['callable']; }
;
Expand Down Expand Up @@ -911,7 +940,7 @@ anonymous_class:
;

new_expr:
T_NEW class_name_reference ctor_arguments { $$ = Expr\New_[$2, $3]; }
T_NEW class_name_reference optional_generic_params ctor_arguments { $$ = Expr\New_[$2, $4, ['generics' => $3]]; }
| T_NEW anonymous_class
{ list($class, $ctorArgs) = $2; $$ = Expr\New_[$class, $ctorArgs]; }
;
Expand Down Expand Up @@ -943,7 +972,7 @@ function_call:

class_name:
T_STATIC { $$ = Name[$1]; }
| name { $$ = $1; }
| name optional_generic_params { $$ = $1; $$->setAttribute('generics', $2); }
;

name:
Expand Down
2 changes: 2 additions & 0 deletions grammar/tokens.y
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,5 @@
%token T_NAME_RELATIVE
%token T_ATTRIBUTE
%token T_ENUM
%token T_GENERIC_PARAMETER_COVARIANT
%token T_GENERIC_PARAMETER_CONTRAVARIANT
14 changes: 8 additions & 6 deletions lib/PhpParser/Lexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ private function handleInvalidCharacterRange($start, $end, $line, ErrorHandler $
*/
private function isUnterminatedComment($token) : bool {
return ($token[0] === \T_COMMENT || $token[0] === \T_DOC_COMMENT)
&& substr($token[1], 0, 2) === '/*'
&& substr($token[1], -2) !== '*/';
&& substr($token[1], 0, 2) === '/*'
&& substr($token[1], -2) !== '*/';
}

protected function postprocessTokens(ErrorHandler $errorHandler) {
Expand Down Expand Up @@ -153,7 +153,7 @@ protected function postprocessTokens(ErrorHandler $errorHandler) {
}

if ($token[0] === \T_COMMENT && substr($token[1], 0, 2) !== '/*'
&& preg_match('/(\r\n|\n|\r)$/D', $token[1], $matches)) {
&& preg_match('/(\r\n|\n|\r)$/D', $token[1], $matches)) {
$trailingNewline = $matches[0];
$token[1] = substr($token[1], 0, -strlen($trailingNewline));
$this->tokens[$i] = $token;
Expand All @@ -173,7 +173,7 @@ protected function postprocessTokens(ErrorHandler $errorHandler) {
// Emulate PHP 8 T_NAME_* tokens, by combining sequences of T_NS_SEPARATOR and T_STRING
// into a single token.
if (\is_array($token)
&& ($token[0] === \T_NS_SEPARATOR || isset($this->identifierTokens[$token[0]]))) {
&& ($token[0] === \T_NS_SEPARATOR || isset($this->identifierTokens[$token[0]]))) {
$lastWasSeparator = $token[0] === \T_NS_SEPARATOR;
$text = $token[1];
for ($j = $i + 1; isset($this->tokens[$j]); $j++) {
Expand Down Expand Up @@ -215,7 +215,7 @@ protected function postprocessTokens(ErrorHandler $errorHandler) {
$next++;
}
$followedByVarOrVarArg = isset($this->tokens[$next]) &&
($this->tokens[$next][0] === \T_VARIABLE || $this->tokens[$next][0] === \T_ELLIPSIS);
($this->tokens[$next][0] === \T_VARIABLE || $this->tokens[$next][0] === \T_ELLIPSIS);
$this->tokens[$i] = $token = [
$followedByVarOrVarArg
? \T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG
Expand Down Expand Up @@ -340,7 +340,7 @@ public function getNextToken(&$value = null, &$startAttributes = null, &$endAttr
$id = $this->tokenMap[$token[0]];
if (\T_CLOSE_TAG === $token[0]) {
$this->prevCloseTagHasNewline = false !== strpos($token[1], "\n")
|| false !== strpos($token[1], "\r");
|| false !== strpos($token[1], "\r");
} elseif (\T_INLINE_HTML === $token[0]) {
$startAttributes['hasLeadingNewline'] = $this->prevCloseTagHasNewline;
}
Expand Down Expand Up @@ -538,6 +538,8 @@ protected function createTokenMap() : array {
$tokenMap[\T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG] = Tokens::T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG;
$tokenMap[\T_ENUM] = Tokens::T_ENUM;
$tokenMap[\T_READONLY] = Tokens::T_READONLY;
$tokenMap[Tokens::T_GENERIC_PARAMETER_COVARIANT] = Tokens::T_GENERIC_PARAMETER_COVARIANT;
$tokenMap[Tokens::T_GENERIC_PARAMETER_CONTRAVARIANT] = Tokens::T_GENERIC_PARAMETER_CONTRAVARIANT;

return $tokenMap;
}
Expand Down
4 changes: 4 additions & 0 deletions lib/PhpParser/Lexer/Emulative.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
use PhpParser\Lexer\TokenEmulator\ExplicitOctalEmulator;
use PhpParser\Lexer\TokenEmulator\FlexibleDocStringEmulator;
use PhpParser\Lexer\TokenEmulator\FnTokenEmulator;
use PhpParser\Lexer\TokenEmulator\GenericParameterContravariantEmulator;
use PhpParser\Lexer\TokenEmulator\GenericParameterCovariantEmulator;
use PhpParser\Lexer\TokenEmulator\MatchTokenEmulator;
use PhpParser\Lexer\TokenEmulator\NullsafeTokenEmulator;
use PhpParser\Lexer\TokenEmulator\NumericLiteralSeparatorEmulator;
Expand Down Expand Up @@ -57,6 +59,8 @@ public function __construct(array $options = [])
new EnumTokenEmulator(),
new ReadonlyTokenEmulator(),
new ExplicitOctalEmulator(),
new GenericParameterCovariantEmulator(),
new GenericParameterContravariantEmulator()
];

// Collect emulators that are relevant for the PHP version we're running
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php declare(strict_types=1);

namespace PhpParser\Lexer\TokenEmulator;

use PhpParser\Lexer\Emulative;
use PhpParser\Parser\Tokens;

final class GenericParameterContravariantEmulator extends KeywordEmulator
{
public function getPhpVersion(): string
{
return Emulative::PHP_8_1;
}

public function getKeywordString(): string
{
return 'out';
}

public function getKeywordToken(): int
{
return Tokens::T_GENERIC_PARAMETER_CONTRAVARIANT;
}

protected function isKeywordContext(array $tokens, int $pos): bool
{
return parent::isKeywordContext($tokens, $pos)
&& isset($tokens[$pos + 2])
&& $tokens[$pos + 1][0] === \T_WHITESPACE
&& $tokens[$pos + 2][0] === \T_STRING;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php declare(strict_types=1);

namespace PhpParser\Lexer\TokenEmulator;

use PhpParser\Lexer\Emulative;
use PhpParser\Parser\Tokens;

final class GenericParameterCovariantEmulator extends KeywordEmulator
{
public function getPhpVersion(): string
{
return Emulative::PHP_8_1;
}

public function getKeywordString(): string
{
return 'in';
}

public function getKeywordToken(): int
{
return Tokens::T_GENERIC_PARAMETER_COVARIANT;
}

protected function isKeywordContext(array $tokens, int $pos): bool
{
return parent::isKeywordContext($tokens, $pos)
&& isset($tokens[$pos + 2])
&& $tokens[$pos + 1][0] === \T_WHITESPACE
&& $tokens[$pos + 2][0] === \T_STRING;
}
}
51 changes: 51 additions & 0 deletions lib/PhpParser/Node/GenericParameter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php declare(strict_types=1);

namespace PhpParser\Node;

use PhpParser\NodeAbstract;

class GenericParameter extends NodeAbstract
{
public $name;
public $constraint;
public $default;
public $variance;

const COVARIANT = 'in';
const CONTRAVARIANT = 'out';

public function __construct(string $name)
{
$this->name = new Name($name);
parent::__construct([]);
}

public function setConstraint($constraint)
{
$this->constraint = new Name($constraint);
}

public function setDefault($default)
{
$this->default = new Name($default);
}

public function setVariance($variance)
{
if (!in_array($variance, [self::COVARIANT, self::CONTRAVARIANT, null])) {
throw new \InvalidArgumentException(sprintf('Invalid generic type variance "%s"', $variance));
}

$this->variance = $variance;
}

public function getSubNodeNames(): array
{
return ['name', 'constraint', 'default', 'variance'];
}

public function getType(): string
{
return 'GenericParameter';
}
}
Loading