bug #4774 Ensure filters/attributes aren't mistaken for operators (brandonkelly)

This PR was merged into the 3.x branch.

Discussion
----------

Ensure filters/attributes aren't mistaken for operators

Updates the regex in `Lexer::getOperatorRegex()` to account for filters/attributes that have a space between their `|`/`.` operator and the filter/attribute name, to ensure they aren’t mistaken for operators.

A test is included that checks the following template.

```twig
{{ 'foo'|and }}
{{ 'bar' | and }}
{{ foo.and }}
{{ bar . and }}
{{ foo and bar }}
```

(Only the `and` in the last tag should be considered an operator.)

Fixes #4767

Commits
-------

a9ac993938 Ensure filters/attributes aren't mistaken for operators
This commit is contained in:
Fabien Potencier
2026-02-24 21:10:30 +01:00
2 changed files with 38 additions and 1 deletions
+1 -1
View File
@@ -544,7 +544,7 @@ class Lexer
// an operator that begins with a character must not have a dot or pipe before
if (ctype_alpha($expressionParser[0])) {
$r = '(?<![\.\|])'.$r;
$r = '(?<![\.\|]\s|.[\.\|])'.$r;
}
// an operator with a space can be any amount of whitespaces
+37
View File
@@ -420,6 +420,43 @@ class LexerTest extends TestCase
$this->addToAssertionCount(1);
}
public function testFilterAndAttributeNamedAfterOperator()
{
// Ensure that filters/attributes aren't mistaken for operators when their names conflict
// (see https://github.com/twigphp/Twig/issues/4767)
$template = '{{ \'foo\'|and }}'
.'{{ \'bar\' | and }}'
.'{{ foo.and }}'
.'{{ bar . and }}'
.'{{ foo and bar }}';
$lexer = new Lexer(new Environment(new ArrayLoader()));
$stream = $lexer->tokenize(new Source($template, 'index'));
foreach (['foo', 'bar'] as $value) {
$stream->expect(Token::VAR_START_TYPE);
$stream->expect(Token::STRING_TYPE, $value);
$stream->expect(Token::OPERATOR_TYPE, '|');
$stream->expect(Token::NAME_TYPE, 'and');
$stream->expect(Token::VAR_END_TYPE);
}
foreach (['foo', 'bar'] as $value) {
$stream->expect(Token::VAR_START_TYPE);
$stream->expect(Token::NAME_TYPE, $value);
$stream->expect(Token::OPERATOR_TYPE, '.');
$stream->expect(Token::NAME_TYPE, 'and');
$stream->expect(Token::VAR_END_TYPE);
}
$stream->expect(Token::VAR_START_TYPE);
$stream->expect(Token::NAME_TYPE, 'foo');
$stream->expect(Token::OPERATOR_TYPE, 'and');
$stream->expect(Token::NAME_TYPE, 'bar');
$stream->expect(Token::VAR_END_TYPE);
// 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
$this->addToAssertionCount(1);
}
public function testUnterminatedVariable()
{
$template = '