2024-06-20 14:10:42 +00:00
< ? php
/*
* This file is part of Twig .
*
* ( c ) Fabien Potencier
* ( c ) Armin Ronacher
*
* For the full copyright and license information , please view the LICENSE
* file that was distributed with this source code .
*/
namespace Twig ;
use Twig\Error\SyntaxError ;
use Twig\Node\Expression\AbstractExpression ;
use Twig\Node\Expression\ArrayExpression ;
use Twig\Node\Expression\ArrowFunctionExpression ;
use Twig\Node\Expression\AssignNameExpression ;
use Twig\Node\Expression\Binary\AbstractBinary ;
use Twig\Node\Expression\Binary\ConcatBinary ;
use Twig\Node\Expression\BlockReferenceExpression ;
use Twig\Node\Expression\ConditionalExpression ;
use Twig\Node\Expression\ConstantExpression ;
use Twig\Node\Expression\GetAttrExpression ;
use Twig\Node\Expression\MethodCallExpression ;
use Twig\Node\Expression\NameExpression ;
use Twig\Node\Expression\ParentExpression ;
use Twig\Node\Expression\TestExpression ;
use Twig\Node\Expression\Unary\AbstractUnary ;
use Twig\Node\Expression\Unary\NegUnary ;
use Twig\Node\Expression\Unary\NotUnary ;
use Twig\Node\Expression\Unary\PosUnary ;
use Twig\Node\Node ;
/**
* Parses expressions .
*
* This parser implements a " Precedence climbing " algorithm .
*
* @ see https :// www . engr . mun . ca /~ theo / Misc / exp_parsing . htm
* @ see https :// en . wikipedia . org / wiki / Operator - precedence_parser
*
* @ author Fabien Potencier < fabien @ symfony . com >
*/
class ExpressionParser
{
public const OPERATOR_LEFT = 1 ;
public const OPERATOR_RIGHT = 2 ;
private $parser ;
private $env ;
/** @var array<string, array{precedence: int, class: class-string<AbstractUnary>}> */
private $unaryOperators ;
/** @var array<string, array{precedence: int, class: class-string<AbstractBinary>, associativity: self::OPERATOR_*}> */
private $binaryOperators ;
public function __construct ( Parser $parser , Environment $env )
{
$this -> parser = $parser ;
$this -> env = $env ;
$this -> unaryOperators = $env -> getUnaryOperators ();
$this -> binaryOperators = $env -> getBinaryOperators ();
}
public function parseExpression ( $precedence = 0 , $allowArrow = false )
{
if ( $allowArrow && $arrow = $this -> parseArrow ()) {
return $arrow ;
}
$expr = $this -> getPrimary ();
$token = $this -> parser -> getCurrentToken ();
while ( $this -> isBinary ( $token ) && $this -> binaryOperators [ $token -> getValue ()][ 'precedence' ] >= $precedence ) {
$op = $this -> binaryOperators [ $token -> getValue ()];
$this -> parser -> getStream () -> next ();
if ( 'is not' === $token -> getValue ()) {
$expr = $this -> parseNotTestExpression ( $expr );
} elseif ( 'is' === $token -> getValue ()) {
$expr = $this -> parseTestExpression ( $expr );
} elseif ( isset ( $op [ 'callable' ])) {
$expr = $op [ 'callable' ]( $this -> parser , $expr );
} else {
$expr1 = $this -> parseExpression ( self :: OPERATOR_LEFT === $op [ 'associativity' ] ? $op [ 'precedence' ] + 1 : $op [ 'precedence' ], true );
$class = $op [ 'class' ];
$expr = new $class ( $expr , $expr1 , $token -> getLine ());
}
$token = $this -> parser -> getCurrentToken ();
}
if ( 0 === $precedence ) {
return $this -> parseConditionalExpression ( $expr );
}
return $expr ;
}
/**
* @ return ArrowFunctionExpression | null
*/
private function parseArrow ()
{
$stream = $this -> parser -> getStream ();
// short array syntax (one argument, no parentheses)?
if ( $stream -> look ( 1 ) -> test ( /* Token::ARROW_TYPE */ 12 )) {
$line = $stream -> getCurrent () -> getLine ();
$token = $stream -> expect ( /* Token::NAME_TYPE */ 5 );
$names = [ new AssignNameExpression ( $token -> getValue (), $token -> getLine ())];
$stream -> expect ( /* Token::ARROW_TYPE */ 12 );
return new ArrowFunctionExpression ( $this -> parseExpression ( 0 ), new Node ( $names ), $line );
}
// first, determine if we are parsing an arrow function by finding => (long form)
$i = 0 ;
if ( ! $stream -> look ( $i ) -> test ( /* Token::PUNCTUATION_TYPE */ 9 , '(' )) {
return null ;
}
++ $i ;
while ( true ) {
// variable name
++ $i ;
if ( ! $stream -> look ( $i ) -> test ( /* Token::PUNCTUATION_TYPE */ 9 , ',' )) {
break ;
}
++ $i ;
}
if ( ! $stream -> look ( $i ) -> test ( /* Token::PUNCTUATION_TYPE */ 9 , ')' )) {
return null ;
}
++ $i ;
if ( ! $stream -> look ( $i ) -> test ( /* Token::ARROW_TYPE */ 12 )) {
return null ;
}
// yes, let's parse it properly
$token = $stream -> expect ( /* Token::PUNCTUATION_TYPE */ 9 , '(' );
$line = $token -> getLine ();
$names = [];
while ( true ) {
$token = $stream -> expect ( /* Token::NAME_TYPE */ 5 );
$names [] = new AssignNameExpression ( $token -> getValue (), $token -> getLine ());
if ( ! $stream -> nextIf ( /* Token::PUNCTUATION_TYPE */ 9 , ',' )) {
break ;
}
}
$stream -> expect ( /* Token::PUNCTUATION_TYPE */ 9 , ')' );
$stream -> expect ( /* Token::ARROW_TYPE */ 12 );
return new ArrowFunctionExpression ( $this -> parseExpression ( 0 ), new Node ( $names ), $line );
}
private function getPrimary () : AbstractExpression
{
$token = $this -> parser -> getCurrentToken ();
if ( $this -> isUnary ( $token )) {
$operator = $this -> unaryOperators [ $token -> getValue ()];
$this -> parser -> getStream () -> next ();
$expr = $this -> parseExpression ( $operator [ 'precedence' ]);
$class = $operator [ 'class' ];
return $this -> parsePostfixExpression ( new $class ( $expr , $token -> getLine ()));
} elseif ( $token -> test ( /* Token::PUNCTUATION_TYPE */ 9 , '(' )) {
$this -> parser -> getStream () -> next ();
$expr = $this -> parseExpression ();
$this -> parser -> getStream () -> expect ( /* Token::PUNCTUATION_TYPE */ 9 , ')' , 'An opened parenthesis is not properly closed' );
return $this -> parsePostfixExpression ( $expr );
}
return $this -> parsePrimaryExpression ();
}
private function parseConditionalExpression ( $expr ) : AbstractExpression
{
while ( $this -> parser -> getStream () -> nextIf ( /* Token::PUNCTUATION_TYPE */ 9 , '?' )) {
if ( ! $this -> parser -> getStream () -> nextIf ( /* Token::PUNCTUATION_TYPE */ 9 , ':' )) {
$expr2 = $this -> parseExpression ();
if ( $this -> parser -> getStream () -> nextIf ( /* Token::PUNCTUATION_TYPE */ 9 , ':' )) {
// Ternary operator (expr ? expr2 : expr3)
$expr3 = $this -> parseExpression ();
} else {
// Ternary without else (expr ? expr2)
$expr3 = new ConstantExpression ( '' , $this -> parser -> getCurrentToken () -> getLine ());
}
} else {
// Ternary without then (expr ?: expr3)
$expr2 = $expr ;
$expr3 = $this -> parseExpression ();
}
$expr = new ConditionalExpression ( $expr , $expr2 , $expr3 , $this -> parser -> getCurrentToken () -> getLine ());
}
return $expr ;
}
private function isUnary ( Token $token ) : bool
{
return $token -> test ( /* Token::OPERATOR_TYPE */ 8 ) && isset ( $this -> unaryOperators [ $token -> getValue ()]);
}
private function isBinary ( Token $token ) : bool
{
return $token -> test ( /* Token::OPERATOR_TYPE */ 8 ) && isset ( $this -> binaryOperators [ $token -> getValue ()]);
}
public function parsePrimaryExpression ()
{
$token = $this -> parser -> getCurrentToken ();
switch ( $token -> getType ()) {
case /* Token::NAME_TYPE */ 5 :
$this -> parser -> getStream () -> next ();
switch ( $token -> getValue ()) {
case 'true' :
case 'TRUE' :
$node = new ConstantExpression ( true , $token -> getLine ());
break ;
case 'false' :
case 'FALSE' :
$node = new ConstantExpression ( false , $token -> getLine ());
break ;
case 'none' :
case 'NONE' :
case 'null' :
case 'NULL' :
$node = new ConstantExpression ( null , $token -> getLine ());
break ;
default :
if ( '(' === $this -> parser -> getCurrentToken () -> getValue ()) {
$node = $this -> getFunctionNode ( $token -> getValue (), $token -> getLine ());
} else {
$node = new NameExpression ( $token -> getValue (), $token -> getLine ());
}
}
break ;
case /* Token::NUMBER_TYPE */ 6 :
$this -> parser -> getStream () -> next ();
$node = new ConstantExpression ( $token -> getValue (), $token -> getLine ());
break ;
case /* Token::STRING_TYPE */ 7 :
case /* Token::INTERPOLATION_START_TYPE */ 10 :
$node = $this -> parseStringExpression ();
break ;
case /* Token::OPERATOR_TYPE */ 8 :
if ( preg_match ( Lexer :: REGEX_NAME , $token -> getValue (), $matches ) && $matches [ 0 ] == $token -> getValue ()) {
// in this context, string operators are variable names
$this -> parser -> getStream () -> next ();
$node = new NameExpression ( $token -> getValue (), $token -> getLine ());
break ;
}
if ( isset ( $this -> unaryOperators [ $token -> getValue ()])) {
$class = $this -> unaryOperators [ $token -> getValue ()][ 'class' ];
if ( ! \in_array ( $class , [ NegUnary :: class , PosUnary :: class ])) {
2024-09-05 17:51:48 +00:00
throw new SyntaxError ( \sprintf ( 'Unexpected unary operator "%s".' , $token -> getValue ()), $token -> getLine (), $this -> parser -> getStream () -> getSourceContext ());
2024-06-20 14:10:42 +00:00
}
$this -> parser -> getStream () -> next ();
$expr = $this -> parsePrimaryExpression ();
$node = new $class ( $expr , $token -> getLine ());
break ;
}
// no break
default :
if ( $token -> test ( /* Token::PUNCTUATION_TYPE */ 9 , '[' )) {
2024-09-05 17:51:48 +00:00
$node = $this -> parseSequenceExpression ();
2024-06-20 14:10:42 +00:00
} elseif ( $token -> test ( /* Token::PUNCTUATION_TYPE */ 9 , '{' )) {
2024-09-05 17:51:48 +00:00
$node = $this -> parseMappingExpression ();
2024-06-20 14:10:42 +00:00
} elseif ( $token -> test ( /* Token::OPERATOR_TYPE */ 8 , '=' ) && ( '==' === $this -> parser -> getStream () -> look ( - 1 ) -> getValue () || '!=' === $this -> parser -> getStream () -> look ( - 1 ) -> getValue ())) {
2024-09-05 17:51:48 +00:00
throw new SyntaxError ( \sprintf ( 'Unexpected operator of value "%s". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.' , $token -> getValue ()), $token -> getLine (), $this -> parser -> getStream () -> getSourceContext ());
2024-06-20 14:10:42 +00:00
} else {
2024-09-05 17:51:48 +00:00
throw new SyntaxError ( \sprintf ( 'Unexpected token "%s" of value "%s".' , Token :: typeToEnglish ( $token -> getType ()), $token -> getValue ()), $token -> getLine (), $this -> parser -> getStream () -> getSourceContext ());
2024-06-20 14:10:42 +00:00
}
}
return $this -> parsePostfixExpression ( $node );
}
public function parseStringExpression ()
{
$stream = $this -> parser -> getStream ();
$nodes = [];
// a string cannot be followed by another string in a single expression
$nextCanBeString = true ;
while ( true ) {
if ( $nextCanBeString && $token = $stream -> nextIf ( /* Token::STRING_TYPE */ 7 )) {
$nodes [] = new ConstantExpression ( $token -> getValue (), $token -> getLine ());
$nextCanBeString = false ;
} elseif ( $stream -> nextIf ( /* Token::INTERPOLATION_START_TYPE */ 10 )) {
$nodes [] = $this -> parseExpression ();
$stream -> expect ( /* Token::INTERPOLATION_END_TYPE */ 11 );
$nextCanBeString = true ;
} else {
break ;
}
}
$expr = array_shift ( $nodes );
foreach ( $nodes as $node ) {
$expr = new ConcatBinary ( $expr , $node , $node -> getTemplateLine ());
}
return $expr ;
}
2024-09-05 17:51:48 +00:00
/**
* @ deprecated since 3.11 , use parseSequenceExpression () instead
*/
2024-06-20 14:10:42 +00:00
public function parseArrayExpression ()
2024-09-05 17:51:48 +00:00
{
trigger_deprecation ( 'twig/twig' , '3.11' , 'Calling "%s()" is deprecated, use "parseSequenceExpression()" instead.' , __METHOD__ );
return $this -> parseSequenceExpression ();
}
public function parseSequenceExpression ()
2024-06-20 14:10:42 +00:00
{
$stream = $this -> parser -> getStream ();
2024-09-05 17:51:48 +00:00
$stream -> expect ( /* Token::PUNCTUATION_TYPE */ 9 , '[' , 'A sequence element was expected' );
2024-06-20 14:10:42 +00:00
$node = new ArrayExpression ([], $stream -> getCurrent () -> getLine ());
$first = true ;
while ( ! $stream -> test ( /* Token::PUNCTUATION_TYPE */ 9 , ']' )) {
if ( ! $first ) {
2024-09-05 17:51:48 +00:00
$stream -> expect ( /* Token::PUNCTUATION_TYPE */ 9 , ',' , 'A sequence element must be followed by a comma' );
2024-06-20 14:10:42 +00:00
// trailing ,?
if ( $stream -> test ( /* Token::PUNCTUATION_TYPE */ 9 , ']' )) {
break ;
}
}
$first = false ;
if ( $stream -> test ( /* Token::SPREAD_TYPE */ 13 )) {
$stream -> next ();
$expr = $this -> parseExpression ();
$expr -> setAttribute ( 'spread' , true );
$node -> addElement ( $expr );
} else {
$node -> addElement ( $this -> parseExpression ());
}
}
2024-09-05 17:51:48 +00:00
$stream -> expect ( /* Token::PUNCTUATION_TYPE */ 9 , ']' , 'An opened sequence is not properly closed' );
2024-06-20 14:10:42 +00:00
return $node ;
}
2024-09-05 17:51:48 +00:00
/**
* @ deprecated since 3.11 , use parseMappingExpression () instead
*/
2024-06-20 14:10:42 +00:00
public function parseHashExpression ()
2024-09-05 17:51:48 +00:00
{
trigger_deprecation ( 'twig/twig' , '3.11' , 'Calling "%s()" is deprecated, use "parseMappingExpression()" instead.' , __METHOD__ );
return $this -> parseMappingExpression ();
}
public function parseMappingExpression ()
2024-06-20 14:10:42 +00:00
{
$stream = $this -> parser -> getStream ();
2024-09-05 17:51:48 +00:00
$stream -> expect ( /* Token::PUNCTUATION_TYPE */ 9 , '{' , 'A mapping element was expected' );
2024-06-20 14:10:42 +00:00
$node = new ArrayExpression ([], $stream -> getCurrent () -> getLine ());
$first = true ;
while ( ! $stream -> test ( /* Token::PUNCTUATION_TYPE */ 9 , '}' )) {
if ( ! $first ) {
2024-09-05 17:51:48 +00:00
$stream -> expect ( /* Token::PUNCTUATION_TYPE */ 9 , ',' , 'A mapping value must be followed by a comma' );
2024-06-20 14:10:42 +00:00
// trailing ,?
if ( $stream -> test ( /* Token::PUNCTUATION_TYPE */ 9 , '}' )) {
break ;
}
}
$first = false ;
if ( $stream -> test ( /* Token::SPREAD_TYPE */ 13 )) {
$stream -> next ();
$value = $this -> parseExpression ();
$value -> setAttribute ( 'spread' , true );
$node -> addElement ( $value );
continue ;
}
2024-09-05 17:51:48 +00:00
// a mapping key can be:
2024-06-20 14:10:42 +00:00
//
// * a number -- 12
// * a string -- 'a'
// * a name, which is equivalent to a string -- a
// * an expression, which must be enclosed in parentheses -- (1 + 2)
if ( $token = $stream -> nextIf ( /* Token::NAME_TYPE */ 5 )) {
$key = new ConstantExpression ( $token -> getValue (), $token -> getLine ());
// {a} is a shortcut for {a:a}
if ( $stream -> test ( Token :: PUNCTUATION_TYPE , [ ',' , '}' ])) {
$value = new NameExpression ( $key -> getAttribute ( 'value' ), $key -> getTemplateLine ());
$node -> addElement ( $value , $key );
continue ;
}
} elseif (( $token = $stream -> nextIf ( /* Token::STRING_TYPE */ 7 )) || $token = $stream -> nextIf ( /* Token::NUMBER_TYPE */ 6 )) {
$key = new ConstantExpression ( $token -> getValue (), $token -> getLine ());
} elseif ( $stream -> test ( /* Token::PUNCTUATION_TYPE */ 9 , '(' )) {
$key = $this -> parseExpression ();
} else {
$current = $stream -> getCurrent ();
2024-09-05 17:51:48 +00:00
throw new SyntaxError ( \sprintf ( 'A mapping key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".' , Token :: typeToEnglish ( $current -> getType ()), $current -> getValue ()), $current -> getLine (), $stream -> getSourceContext ());
2024-06-20 14:10:42 +00:00
}
2024-09-05 17:51:48 +00:00
$stream -> expect ( /* Token::PUNCTUATION_TYPE */ 9 , ':' , 'A mapping key must be followed by a colon (:)' );
2024-06-20 14:10:42 +00:00
$value = $this -> parseExpression ();
$node -> addElement ( $value , $key );
}
2024-09-05 17:51:48 +00:00
$stream -> expect ( /* Token::PUNCTUATION_TYPE */ 9 , '}' , 'An opened mapping is not properly closed' );
2024-06-20 14:10:42 +00:00
return $node ;
}
public function parsePostfixExpression ( $node )
{
while ( true ) {
$token = $this -> parser -> getCurrentToken ();
if ( /* Token::PUNCTUATION_TYPE */ 9 == $token -> getType ()) {
if ( '.' == $token -> getValue () || '[' == $token -> getValue ()) {
$node = $this -> parseSubscriptExpression ( $node );
} elseif ( '|' == $token -> getValue ()) {
$node = $this -> parseFilterExpression ( $node );
} else {
break ;
}
} else {
break ;
}
}
return $node ;
}
public function getFunctionNode ( $name , $line )
{
switch ( $name ) {
case 'parent' :
$this -> parseArguments ();
if ( ! \count ( $this -> parser -> getBlockStack ())) {
throw new SyntaxError ( 'Calling "parent" outside a block is forbidden.' , $line , $this -> parser -> getStream () -> getSourceContext ());
}
if ( ! $this -> parser -> getParent () && ! $this -> parser -> hasTraits ()) {
throw new SyntaxError ( 'Calling "parent" on a template that does not extend nor "use" another template is forbidden.' , $line , $this -> parser -> getStream () -> getSourceContext ());
}
return new ParentExpression ( $this -> parser -> peekBlockStack (), $line );
case 'block' :
$args = $this -> parseArguments ();
if ( \count ( $args ) < 1 ) {
throw new SyntaxError ( 'The "block" function takes one argument (the block name).' , $line , $this -> parser -> getStream () -> getSourceContext ());
}
return new BlockReferenceExpression ( $args -> getNode ( '0' ), \count ( $args ) > 1 ? $args -> getNode ( '1' ) : null , $line );
case 'attribute' :
$args = $this -> parseArguments ();
if ( \count ( $args ) < 2 ) {
throw new SyntaxError ( 'The "attribute" function takes at least two arguments (the variable and the attributes).' , $line , $this -> parser -> getStream () -> getSourceContext ());
}
return new GetAttrExpression ( $args -> getNode ( '0' ), $args -> getNode ( '1' ), \count ( $args ) > 2 ? $args -> getNode ( '2' ) : null , Template :: ANY_CALL , $line );
default :
if ( null !== $alias = $this -> parser -> getImportedSymbol ( 'function' , $name )) {
$arguments = new ArrayExpression ([], $line );
foreach ( $this -> parseArguments () as $n ) {
$arguments -> addElement ( $n );
}
$node = new MethodCallExpression ( $alias [ 'node' ], $alias [ 'name' ], $arguments , $line );
$node -> setAttribute ( 'safe' , true );
return $node ;
}
$args = $this -> parseArguments ( true );
$class = $this -> getFunctionNodeClass ( $name , $line );
return new $class ( $name , $args , $line );
}
}
public function parseSubscriptExpression ( $node )
{
$stream = $this -> parser -> getStream ();
$token = $stream -> next ();
$lineno = $token -> getLine ();
$arguments = new ArrayExpression ([], $lineno );
$type = Template :: ANY_CALL ;
if ( '.' == $token -> getValue ()) {
$token = $stream -> next ();
if (
/* Token::NAME_TYPE */ 5 == $token -> getType ()
||
/* Token::NUMBER_TYPE */ 6 == $token -> getType ()
||
( /* Token::OPERATOR_TYPE */ 8 == $token -> getType () && preg_match ( Lexer :: REGEX_NAME , $token -> getValue ()))
) {
$arg = new ConstantExpression ( $token -> getValue (), $lineno );
if ( $stream -> test ( /* Token::PUNCTUATION_TYPE */ 9 , '(' )) {
$type = Template :: METHOD_CALL ;
foreach ( $this -> parseArguments () as $n ) {
$arguments -> addElement ( $n );
}
}
} else {
2024-09-05 17:51:48 +00:00
throw new SyntaxError ( \sprintf ( 'Expected name or number, got value "%s" of type %s.' , $token -> getValue (), Token :: typeToEnglish ( $token -> getType ())), $lineno , $stream -> getSourceContext ());
2024-06-20 14:10:42 +00:00
}
if ( $node instanceof NameExpression && null !== $this -> parser -> getImportedSymbol ( 'template' , $node -> getAttribute ( 'name' ))) {
$name = $arg -> getAttribute ( 'value' );
$node = new MethodCallExpression ( $node , 'macro_' . $name , $arguments , $lineno );
$node -> setAttribute ( 'safe' , true );
return $node ;
}
} else {
$type = Template :: ARRAY_CALL ;
// slice?
$slice = false ;
if ( $stream -> test ( /* Token::PUNCTUATION_TYPE */ 9 , ':' )) {
$slice = true ;
$arg = new ConstantExpression ( 0 , $token -> getLine ());
} else {
$arg = $this -> parseExpression ();
}
if ( $stream -> nextIf ( /* Token::PUNCTUATION_TYPE */ 9 , ':' )) {
$slice = true ;
}
if ( $slice ) {
if ( $stream -> test ( /* Token::PUNCTUATION_TYPE */ 9 , ']' )) {
$length = new ConstantExpression ( null , $token -> getLine ());
} else {
$length = $this -> parseExpression ();
}
$class = $this -> getFilterNodeClass ( 'slice' , $token -> getLine ());
$arguments = new Node ([ $arg , $length ]);
$filter = new $class ( $node , new ConstantExpression ( 'slice' , $token -> getLine ()), $arguments , $token -> getLine ());
$stream -> expect ( /* Token::PUNCTUATION_TYPE */ 9 , ']' );
return $filter ;
}
$stream -> expect ( /* Token::PUNCTUATION_TYPE */ 9 , ']' );
}
return new GetAttrExpression ( $node , $arg , $arguments , $type , $lineno );
}
public function parseFilterExpression ( $node )
{
$this -> parser -> getStream () -> next ();
return $this -> parseFilterExpressionRaw ( $node );
}
public function parseFilterExpressionRaw ( $node , $tag = null )
{
while ( true ) {
$token = $this -> parser -> getStream () -> expect ( /* Token::NAME_TYPE */ 5 );
$name = new ConstantExpression ( $token -> getValue (), $token -> getLine ());
if ( ! $this -> parser -> getStream () -> test ( /* Token::PUNCTUATION_TYPE */ 9 , '(' )) {
$arguments = new Node ();
} else {
$arguments = $this -> parseArguments ( true , false , true );
}
$class = $this -> getFilterNodeClass ( $name -> getAttribute ( 'value' ), $token -> getLine ());
$node = new $class ( $node , $name , $arguments , $token -> getLine (), $tag );
if ( ! $this -> parser -> getStream () -> test ( /* Token::PUNCTUATION_TYPE */ 9 , '|' )) {
break ;
}
$this -> parser -> getStream () -> next ();
}
return $node ;
}
/**
* Parses arguments .
*
* @ param bool $namedArguments Whether to allow named arguments or not
* @ param bool $definition Whether we are parsing arguments for a function definition
*
* @ return Node
*
* @ throws SyntaxError
*/
public function parseArguments ( $namedArguments = false , $definition = false , $allowArrow = false )
{
$args = [];
$stream = $this -> parser -> getStream ();
$stream -> expect ( /* Token::PUNCTUATION_TYPE */ 9 , '(' , 'A list of arguments must begin with an opening parenthesis' );
while ( ! $stream -> test ( /* Token::PUNCTUATION_TYPE */ 9 , ')' )) {
if ( ! empty ( $args )) {
$stream -> expect ( /* Token::PUNCTUATION_TYPE */ 9 , ',' , 'Arguments must be separated by a comma' );
// if the comma above was a trailing comma, early exit the argument parse loop
if ( $stream -> test ( /* Token::PUNCTUATION_TYPE */ 9 , ')' )) {
break ;
}
}
if ( $definition ) {
$token = $stream -> expect ( /* Token::NAME_TYPE */ 5 , null , 'An argument must be a name' );
$value = new NameExpression ( $token -> getValue (), $this -> parser -> getCurrentToken () -> getLine ());
} else {
$value = $this -> parseExpression ( 0 , $allowArrow );
}
$name = null ;
if ( $namedArguments && $token = $stream -> nextIf ( /* Token::OPERATOR_TYPE */ 8 , '=' )) {
if ( ! $value instanceof NameExpression ) {
2024-09-05 17:51:48 +00:00
throw new SyntaxError ( \sprintf ( 'A parameter name must be a string, "%s" given.' , \get_class ( $value )), $token -> getLine (), $stream -> getSourceContext ());
2024-06-20 14:10:42 +00:00
}
$name = $value -> getAttribute ( 'name' );
if ( $definition ) {
$value = $this -> parsePrimaryExpression ();
if ( ! $this -> checkConstantExpression ( $value )) {
2024-09-05 17:51:48 +00:00
throw new SyntaxError ( 'A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).' , $token -> getLine (), $stream -> getSourceContext ());
2024-06-20 14:10:42 +00:00
}
} else {
$value = $this -> parseExpression ( 0 , $allowArrow );
}
}
if ( $definition ) {
if ( null === $name ) {
$name = $value -> getAttribute ( 'name' );
$value = new ConstantExpression ( null , $this -> parser -> getCurrentToken () -> getLine ());
}
$args [ $name ] = $value ;
} else {
if ( null === $name ) {
$args [] = $value ;
} else {
$args [ $name ] = $value ;
}
}
}
$stream -> expect ( /* Token::PUNCTUATION_TYPE */ 9 , ')' , 'A list of arguments must be closed by a parenthesis' );
return new Node ( $args );
}
public function parseAssignmentExpression ()
{
$stream = $this -> parser -> getStream ();
$targets = [];
while ( true ) {
$token = $this -> parser -> getCurrentToken ();
if ( $stream -> test ( /* Token::OPERATOR_TYPE */ 8 ) && preg_match ( Lexer :: REGEX_NAME , $token -> getValue ())) {
// in this context, string operators are variable names
$this -> parser -> getStream () -> next ();
} else {
$stream -> expect ( /* Token::NAME_TYPE */ 5 , null , 'Only variables can be assigned to' );
}
$value = $token -> getValue ();
if ( \in_array ( strtr ( $value , 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' , 'abcdefghijklmnopqrstuvwxyz' ), [ 'true' , 'false' , 'none' , 'null' ])) {
2024-09-05 17:51:48 +00:00
throw new SyntaxError ( \sprintf ( 'You cannot assign a value to "%s".' , $value ), $token -> getLine (), $stream -> getSourceContext ());
2024-06-20 14:10:42 +00:00
}
$targets [] = new AssignNameExpression ( $value , $token -> getLine ());
if ( ! $stream -> nextIf ( /* Token::PUNCTUATION_TYPE */ 9 , ',' )) {
break ;
}
}
return new Node ( $targets );
}
public function parseMultitargetExpression ()
{
$targets = [];
while ( true ) {
$targets [] = $this -> parseExpression ();
if ( ! $this -> parser -> getStream () -> nextIf ( /* Token::PUNCTUATION_TYPE */ 9 , ',' )) {
break ;
}
}
return new Node ( $targets );
}
private function parseNotTestExpression ( Node $node ) : NotUnary
{
return new NotUnary ( $this -> parseTestExpression ( $node ), $this -> parser -> getCurrentToken () -> getLine ());
}
private function parseTestExpression ( Node $node ) : TestExpression
{
$stream = $this -> parser -> getStream ();
[ $name , $test ] = $this -> getTest ( $node -> getTemplateLine ());
$class = $this -> getTestNodeClass ( $test );
$arguments = null ;
if ( $stream -> test ( /* Token::PUNCTUATION_TYPE */ 9 , '(' )) {
$arguments = $this -> parseArguments ( true );
} elseif ( $test -> hasOneMandatoryArgument ()) {
$arguments = new Node ([ 0 => $this -> parsePrimaryExpression ()]);
}
if ( 'defined' === $name && $node instanceof NameExpression && null !== $alias = $this -> parser -> getImportedSymbol ( 'function' , $node -> getAttribute ( 'name' ))) {
$node = new MethodCallExpression ( $alias [ 'node' ], $alias [ 'name' ], new ArrayExpression ([], $node -> getTemplateLine ()), $node -> getTemplateLine ());
$node -> setAttribute ( 'safe' , true );
}
return new $class ( $node , $name , $arguments , $this -> parser -> getCurrentToken () -> getLine ());
}
private function getTest ( int $line ) : array
{
$stream = $this -> parser -> getStream ();
$name = $stream -> expect ( /* Token::NAME_TYPE */ 5 ) -> getValue ();
if ( $test = $this -> env -> getTest ( $name )) {
return [ $name , $test ];
}
if ( $stream -> test ( /* Token::NAME_TYPE */ 5 )) {
// try 2-words tests
$name = $name . ' ' . $this -> parser -> getCurrentToken () -> getValue ();
if ( $test = $this -> env -> getTest ( $name )) {
$stream -> next ();
return [ $name , $test ];
}
}
2024-09-05 17:51:48 +00:00
$e = new SyntaxError ( \sprintf ( 'Unknown "%s" test.' , $name ), $line , $stream -> getSourceContext ());
2024-06-20 14:10:42 +00:00
$e -> addSuggestions ( $name , array_keys ( $this -> env -> getTests ()));
throw $e ;
}
private function getTestNodeClass ( TwigTest $test ) : string
{
if ( $test -> isDeprecated ()) {
$stream = $this -> parser -> getStream ();
2024-09-05 17:51:48 +00:00
$message = \sprintf ( 'Twig Test "%s" is deprecated' , $test -> getName ());
2024-06-20 14:10:42 +00:00
if ( $test -> getAlternative ()) {
2024-09-05 17:51:48 +00:00
$message .= \sprintf ( '. Use "%s" instead' , $test -> getAlternative ());
2024-06-20 14:10:42 +00:00
}
$src = $stream -> getSourceContext ();
2024-09-05 17:51:48 +00:00
$message .= \sprintf ( ' in %s at line %d.' , $src -> getPath () ? : $src -> getName (), $stream -> getCurrent () -> getLine ());
2024-06-20 14:10:42 +00:00
2024-09-05 17:51:48 +00:00
trigger_deprecation ( $test -> getDeprecatingPackage (), $test -> getDeprecatedVersion (), $message );
2024-06-20 14:10:42 +00:00
}
return $test -> getNodeClass ();
}
private function getFunctionNodeClass ( string $name , int $line ) : string
{
if ( ! $function = $this -> env -> getFunction ( $name )) {
2024-09-05 17:51:48 +00:00
$e = new SyntaxError ( \sprintf ( 'Unknown "%s" function.' , $name ), $line , $this -> parser -> getStream () -> getSourceContext ());
2024-06-20 14:10:42 +00:00
$e -> addSuggestions ( $name , array_keys ( $this -> env -> getFunctions ()));
throw $e ;
}
if ( $function -> isDeprecated ()) {
2024-09-05 17:51:48 +00:00
$message = \sprintf ( 'Twig Function "%s" is deprecated' , $function -> getName ());
2024-06-20 14:10:42 +00:00
if ( $function -> getAlternative ()) {
2024-09-05 17:51:48 +00:00
$message .= \sprintf ( '. Use "%s" instead' , $function -> getAlternative ());
2024-06-20 14:10:42 +00:00
}
$src = $this -> parser -> getStream () -> getSourceContext ();
2024-09-05 17:51:48 +00:00
$message .= \sprintf ( ' in %s at line %d.' , $src -> getPath () ? : $src -> getName (), $line );
2024-06-20 14:10:42 +00:00
2024-09-05 17:51:48 +00:00
trigger_deprecation ( $function -> getDeprecatingPackage (), $function -> getDeprecatedVersion (), $message );
2024-06-20 14:10:42 +00:00
}
return $function -> getNodeClass ();
}
private function getFilterNodeClass ( string $name , int $line ) : string
{
if ( ! $filter = $this -> env -> getFilter ( $name )) {
2024-09-05 17:51:48 +00:00
$e = new SyntaxError ( \sprintf ( 'Unknown "%s" filter.' , $name ), $line , $this -> parser -> getStream () -> getSourceContext ());
2024-06-20 14:10:42 +00:00
$e -> addSuggestions ( $name , array_keys ( $this -> env -> getFilters ()));
throw $e ;
}
if ( $filter -> isDeprecated ()) {
2024-09-05 17:51:48 +00:00
$message = \sprintf ( 'Twig Filter "%s" is deprecated' , $filter -> getName ());
2024-06-20 14:10:42 +00:00
if ( $filter -> getAlternative ()) {
2024-09-05 17:51:48 +00:00
$message .= \sprintf ( '. Use "%s" instead' , $filter -> getAlternative ());
2024-06-20 14:10:42 +00:00
}
$src = $this -> parser -> getStream () -> getSourceContext ();
2024-09-05 17:51:48 +00:00
$message .= \sprintf ( ' in %s at line %d.' , $src -> getPath () ? : $src -> getName (), $line );
2024-06-20 14:10:42 +00:00
2024-09-05 17:51:48 +00:00
trigger_deprecation ( $filter -> getDeprecatingPackage (), $filter -> getDeprecatedVersion (), $message );
2024-06-20 14:10:42 +00:00
}
return $filter -> getNodeClass ();
}
// checks that the node only contains "constant" elements
private function checkConstantExpression ( Node $node ) : bool
{
if ( ! ( $node instanceof ConstantExpression || $node instanceof ArrayExpression
|| $node instanceof NegUnary || $node instanceof PosUnary
)) {
return false ;
}
foreach ( $node as $n ) {
if ( ! $this -> checkConstantExpression ( $n )) {
return false ;
}
}
return true ;
}
}