mirror of
https://github.com/twigphp/Twig.git
synced 2026-08-17 12:31:38 +00:00
Add getOperatorTokens() to ExpressionParserInterface to separate operator token registration from parser identity
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
# 3.24.0 (2026-XX-XX)
|
||||
|
||||
* Deprecate not implementing the `getOperatorTokens()` method in `ExpressionParserInterface` implementations
|
||||
* Deprecate passing a non-`AbstractExpression` node to `ParserTwig\Node\Expression\Binary\MatchesBinary` constructor
|
||||
* Deprecate passing a non-`AbstractExpression` node to `Parser::setParent()`
|
||||
* Add support for renaming variables in object destructuring (`{name: userName} = user`)
|
||||
|
||||
@@ -485,3 +485,13 @@ Operators
|
||||
|
||||
* The ``Twig\OperatorPrecedenceChange`` class is deprecated as of Twig 3.21,
|
||||
use ``Twig\ExpressionParser\PrecedenceChange`` instead.
|
||||
|
||||
* Not implementing the ``getOperatorTokens()`` method in
|
||||
``Twig\ExpressionParser\ExpressionParserInterface`` implementations is
|
||||
deprecated as of Twig 3.24. This method will be added to the interface in
|
||||
Twig 4.0. It returns the operator token strings that the expression parser
|
||||
handles (used by the Lexer and the parser registry). If your custom
|
||||
expression parser extends ``Twig\ExpressionParser\AbstractExpressionParser``,
|
||||
the default implementation returns ``[$this->getName(), ...$this->getAliases()]``.
|
||||
Override it if your parser doesn't handle operator tokens (return ``[]``) or if
|
||||
the operator tokens differ from the parser name.
|
||||
|
||||
@@ -92,12 +92,12 @@
|
||||
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
|
||||
| 0 | ``(`` | prefix | n/a | Explicit group expression (a) |
|
||||
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
|
||||
| | ``literal`` | | | A literal value (boolean, string, number, sequence, mapping, ...) |
|
||||
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
|
||||
| | ``?`` | infix | Left | Conditional operator (a ? b : c) |
|
||||
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
|
||||
| | ``=`` | | Right | Assignment operator |
|
||||
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
|
||||
| | ``literal`` | prefix | n/a | A literal value (boolean, string, number, sequence, mapping, ...) |
|
||||
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
|
||||
|
||||
When a precedence will change in 4.0, the new precedence is indicated by the arrow ``=>``.
|
||||
|
||||
@@ -196,9 +196,9 @@ Here is the same table for Twig 4.0 with adjusted precedences:
|
||||
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
|
||||
| 0 | ``(`` | prefix | n/a | Explicit group expression (a) |
|
||||
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
|
||||
| | ``literal`` | | | A literal value (boolean, string, number, sequence, mapping, ...) |
|
||||
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
|
||||
| | ``?`` | infix | Left | Conditional operator (a ? b : c) |
|
||||
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
|
||||
| | ``=`` | | Right | Assignment operator |
|
||||
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
|
||||
| | ``literal`` | prefix | n/a | A literal value (boolean, string, number, sequence, mapping, ...) |
|
||||
+------------+------------------+---------+---------------+-------------------------------------------------------------------+
|
||||
|
||||
@@ -27,4 +27,9 @@ abstract class AbstractExpressionParser implements ExpressionParserInterface
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getOperatorTokens(): array
|
||||
{
|
||||
return [$this->getName(), ...$this->getAliases()];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,14 @@
|
||||
|
||||
namespace Twig\ExpressionParser;
|
||||
|
||||
/**
|
||||
* @method list<string> getOperatorTokens() Returns the operator token strings that this expression parser handles.
|
||||
* These are the strings that should be recognized as operator tokens by the Lexer,
|
||||
* and used to look up the parser in the registry.
|
||||
* For most parsers, this returns the name and aliases. Parsers that don't handle
|
||||
* operator tokens (like LiteralExpressionParser) should return an empty array.
|
||||
* This method will be added to the interface in Twig 4.0.
|
||||
*/
|
||||
interface ExpressionParserInterface
|
||||
{
|
||||
public function __toString(): string;
|
||||
|
||||
@@ -54,10 +54,9 @@ final class ExpressionParsers implements \IteratorAggregate
|
||||
// throw new \InvalidArgumentException(\sprintf('Precedence for "%s" must be between 0 and 512, got %d.', $parser->getName(), $parser->getPrecedence()));
|
||||
}
|
||||
$interface = $parser instanceof PrefixExpressionParserInterface ? PrefixExpressionParserInterface::class : InfixExpressionParserInterface::class;
|
||||
$this->parsersByName[$interface][$parser->getName()] = $parser;
|
||||
$this->parsersByClass[$parser::class] = $parser;
|
||||
foreach ($parser->getAliases() as $alias) {
|
||||
$this->parsersByName[$interface][$alias] = $parser;
|
||||
foreach (self::getOperatorTokensFor($parser) as $token) {
|
||||
$this->parsersByName[$interface][$token] = $parser;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,9 +89,22 @@ final class ExpressionParsers implements \IteratorAggregate
|
||||
|
||||
public function getIterator(): \Traversable
|
||||
{
|
||||
$seen = [];
|
||||
foreach ($this->parsersByName as $parsers) {
|
||||
// we don't yield the keys
|
||||
yield from $parsers;
|
||||
foreach ($parsers as $parser) {
|
||||
$id = spl_object_id($parser);
|
||||
if (!isset($seen[$id])) {
|
||||
$seen[$id] = true;
|
||||
yield $parser;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($this->parsersByClass as $parser) {
|
||||
$id = spl_object_id($parser);
|
||||
if (!isset($seen[$id])) {
|
||||
$seen[$id] = true;
|
||||
yield $parser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,4 +136,20 @@ final class ExpressionParsers implements \IteratorAggregate
|
||||
|
||||
return $this->precedenceChanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function getOperatorTokensFor(ExpressionParserInterface $parser): array
|
||||
{
|
||||
if (method_exists($parser, 'getOperatorTokens')) {
|
||||
return $parser->getOperatorTokens();
|
||||
}
|
||||
|
||||
trigger_deprecation('twig/twig', '3.24', 'Not implementing the "getOperatorTokens()" method in "%s" is deprecated. This method will be part of the "%s" interface in 4.0.', $parser::class, ExpressionParserInterface::class);
|
||||
|
||||
return [$parser->getName(), ...$parser->getAliases()];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,6 @@ use Twig\Token;
|
||||
*/
|
||||
final class LiteralExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface, ExpressionParserDescriptionInterface
|
||||
{
|
||||
private string $type = 'literal';
|
||||
|
||||
public function parse(Parser $parser, Token $token): AbstractExpression
|
||||
{
|
||||
$stream = $parser->getStream();
|
||||
@@ -41,41 +39,30 @@ final class LiteralExpressionParser extends AbstractExpressionParser implements
|
||||
switch ($token->getValue()) {
|
||||
case 'true':
|
||||
case 'TRUE':
|
||||
$this->type = 'constant';
|
||||
|
||||
return new ConstantExpression(true, $token->getLine());
|
||||
|
||||
case 'false':
|
||||
case 'FALSE':
|
||||
$this->type = 'constant';
|
||||
|
||||
return new ConstantExpression(false, $token->getLine());
|
||||
|
||||
case 'none':
|
||||
case 'NONE':
|
||||
case 'null':
|
||||
case 'NULL':
|
||||
$this->type = 'constant';
|
||||
|
||||
return new ConstantExpression(null, $token->getLine());
|
||||
|
||||
default:
|
||||
$this->type = 'variable';
|
||||
|
||||
return new ContextVariable($token->getValue(), $token->getLine());
|
||||
}
|
||||
|
||||
// no break
|
||||
case $token->test(Token::NUMBER_TYPE):
|
||||
$stream->next();
|
||||
$this->type = 'constant';
|
||||
|
||||
return new ConstantExpression($token->getValue(), $token->getLine());
|
||||
|
||||
case $token->test(Token::STRING_TYPE):
|
||||
case $token->test(Token::INTERPOLATION_START_TYPE):
|
||||
$this->type = 'string';
|
||||
|
||||
return $this->parseStringExpression($parser);
|
||||
|
||||
case $token->test(Token::PUNCTUATION_TYPE):
|
||||
@@ -96,7 +83,6 @@ final class LiteralExpressionParser extends AbstractExpressionParser implements
|
||||
if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) {
|
||||
// in this context, string operators are variable names
|
||||
$stream->next();
|
||||
$this->type = 'variable';
|
||||
|
||||
return new ContextVariable($token->getValue(), $token->getLine());
|
||||
}
|
||||
@@ -109,7 +95,12 @@ final class LiteralExpressionParser extends AbstractExpressionParser implements
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->type;
|
||||
return 'literal';
|
||||
}
|
||||
|
||||
public function getOperatorTokens(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
@@ -153,8 +144,6 @@ final class LiteralExpressionParser extends AbstractExpressionParser implements
|
||||
|
||||
private function parseSequenceExpression(Parser $parser)
|
||||
{
|
||||
$this->type = 'sequence';
|
||||
|
||||
$stream = $parser->getStream();
|
||||
$stream->expect(Token::OPERATOR_TYPE, '[', 'A sequence element was expected');
|
||||
|
||||
@@ -185,8 +174,6 @@ final class LiteralExpressionParser extends AbstractExpressionParser implements
|
||||
|
||||
private function parseMappingExpression(Parser $parser)
|
||||
{
|
||||
$this->type = 'mapping';
|
||||
|
||||
$stream = $parser->getStream();
|
||||
$stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected');
|
||||
|
||||
|
||||
+2
-1
@@ -13,6 +13,7 @@
|
||||
namespace Twig;
|
||||
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\ExpressionParser\ExpressionParsers;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
@@ -527,7 +528,7 @@ class Lexer
|
||||
{
|
||||
$expressionParsers = [];
|
||||
foreach ($this->env->getExpressionParsers() as $expressionParser) {
|
||||
$expressionParsers = array_merge($expressionParsers, [$expressionParser->getName()], $expressionParser->getAliases());
|
||||
$expressionParsers = array_merge($expressionParsers, ExpressionParsers::getOperatorTokensFor($expressionParser));
|
||||
}
|
||||
|
||||
$expressionParsers = array_combine($expressionParsers, array_map('strlen', $expressionParsers));
|
||||
|
||||
@@ -27,7 +27,10 @@ use Twig\Compiler;
|
||||
use Twig\Environment;
|
||||
use Twig\Error\RuntimeError;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\ExpressionParser\InfixExpressionParserInterface;
|
||||
use Twig\ExpressionParser\Prefix\LiteralExpressionParser;
|
||||
use Twig\ExpressionParser\Prefix\UnaryOperatorExpressionParser;
|
||||
use Twig\ExpressionParser\PrefixExpressionParserInterface;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\Loader\ArrayLoader;
|
||||
use Twig\Node\Expression\ArrayExpression;
|
||||
@@ -796,6 +799,36 @@ class ExpressionParserTest extends TestCase
|
||||
yield '= stronger than logical' => ['{% do a = false or true %}{{ a }}', '{% do a = (false or true) %}{{ a }}', eval('$a = false || true; return $a;')];
|
||||
yield '= stronger than ternary' => ['{% do c = 4 ? 0 : -1 %}{{ c }}', '{% do c = (4 ? 0 : -1) %}{{ c }}', eval('return 4 ? 0 : -1;')];
|
||||
}
|
||||
|
||||
public function testLiteralExpressionParserGetOperatorTokensReturnsEmptyArray()
|
||||
{
|
||||
$env = new Environment(new ArrayLoader());
|
||||
$parser = $env->getExpressionParsers()->getByClass(LiteralExpressionParser::class);
|
||||
|
||||
$this->assertSame([], $parser->getOperatorTokens());
|
||||
$this->assertSame('literal', $parser->getName());
|
||||
}
|
||||
|
||||
public function testExpressionParserGetOperatorTokensDefaultBehavior()
|
||||
{
|
||||
$env = new Environment(new ArrayLoader());
|
||||
|
||||
foreach ($env->getExpressionParsers() as $parser) {
|
||||
if ($parser instanceof LiteralExpressionParser) {
|
||||
continue;
|
||||
}
|
||||
$expected = [$parser->getName(), ...$parser->getAliases()];
|
||||
$this->assertSame($expected, $parser->getOperatorTokens(), \sprintf('getOperatorTokens() for %s should return name + aliases.', $parser::class));
|
||||
}
|
||||
}
|
||||
|
||||
public function testLiteralIsNotRegisteredAsOperator()
|
||||
{
|
||||
// Ensure "literal" is not in the operator registry
|
||||
$env = new Environment(new ArrayLoader());
|
||||
$this->assertNull($env->getExpressionParsers()->getByName(PrefixExpressionParserInterface::class, 'literal'));
|
||||
$this->assertNull($env->getExpressionParsers()->getByName(InfixExpressionParserInterface::class, 'literal'));
|
||||
}
|
||||
}
|
||||
|
||||
class NotReadyFunctionExpression extends FunctionExpression
|
||||
|
||||
@@ -454,6 +454,19 @@ class LexerTest extends TestCase
|
||||
|
||||
// add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above
|
||||
// can be executed without throwing any exceptions
|
||||
}
|
||||
|
||||
public function testLiteralIsNotAnOperator()
|
||||
{
|
||||
// "literal" is the name of the LiteralExpressionParser but should not be treated as an operator token
|
||||
$template = '{{ literal }}';
|
||||
|
||||
$lexer = new Lexer(new Environment(new ArrayLoader()));
|
||||
$stream = $lexer->tokenize(new Source($template, 'index'));
|
||||
$stream->expect(Token::VAR_START_TYPE);
|
||||
$stream->expect(Token::NAME_TYPE, 'literal');
|
||||
$stream->expect(Token::VAR_END_TYPE);
|
||||
|
||||
$this->addToAssertionCount(1);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user