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 ;
2025-01-13 09:56:01 +00:00
use Twig\Attribute\FirstClassTwigCallableReady ;
2024-06-20 14:10:42 +00:00
use Twig\Error\SyntaxError ;
2025-01-13 09:56:01 +00:00
use Twig\Node\EmptyNode ;
2024-06-20 14:10:42 +00:00
use Twig\Node\Expression\AbstractExpression ;
use Twig\Node\Expression\ArrayExpression ;
use Twig\Node\Expression\ArrowFunctionExpression ;
use Twig\Node\Expression\Binary\AbstractBinary ;
use Twig\Node\Expression\Binary\ConcatBinary ;
use Twig\Node\Expression\ConstantExpression ;
use Twig\Node\Expression\GetAttrExpression ;
2025-01-13 09:56:01 +00:00
use Twig\Node\Expression\MacroReferenceExpression ;
2024-06-20 14:10:42 +00:00
use Twig\Node\Expression\NameExpression ;
2025-01-13 09:56:01 +00:00
use Twig\Node\Expression\Ternary\ConditionalTernary ;
2024-06-20 14:10:42 +00:00
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 ;
2025-01-13 09:56:01 +00:00
use Twig\Node\Expression\Unary\SpreadUnary ;
use Twig\Node\Expression\Variable\AssignContextVariable ;
use Twig\Node\Expression\Variable\ContextVariable ;
use Twig\Node\Expression\Variable\LocalVariable ;
use Twig\Node\Expression\Variable\TemplateVariable ;
2024-06-20 14:10:42 +00:00
use Twig\Node\Node ;
2025-01-13 09:56:01 +00:00
use Twig\Node\Nodes ;
2024-06-20 14:10:42 +00:00
/**
* 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 ;
2025-01-13 09:56:01 +00:00
/** @var array<string, array{precedence: int, precedence_change?: OperatorPrecedenceChange, class: class-string<AbstractUnary>}> */
2024-06-20 14:10:42 +00:00
private $unaryOperators ;
2025-01-13 09:56:01 +00:00
/** @var array<string, array{precedence: int, precedence_change?: OperatorPrecedenceChange, class: class-string<AbstractBinary>, associativity: self::OPERATOR_*}> */
2024-06-20 14:10:42 +00:00
private $binaryOperators ;
2025-01-13 09:56:01 +00:00
private $readyNodes = [];
private array $precedenceChanges = [];
private bool $deprecationCheck = true ;
public function __construct (
private Parser $parser ,
private Environment $env ,
) {
2024-06-20 14:10:42 +00:00
$this -> unaryOperators = $env -> getUnaryOperators ();
$this -> binaryOperators = $env -> getBinaryOperators ();
2025-01-13 09:56:01 +00:00
$ops = [];
foreach ( $this -> unaryOperators as $n => $c ) {
$ops [] = $c + [ 'name' => $n , 'type' => 'unary' ];
}
foreach ( $this -> binaryOperators as $n => $c ) {
$ops [] = $c + [ 'name' => $n , 'type' => 'binary' ];
}
foreach ( $ops as $config ) {
if ( ! isset ( $config [ 'precedence_change' ])) {
continue ;
}
$name = $config [ 'type' ] . '_' . $config [ 'name' ];
$min = min ( $config [ 'precedence_change' ] -> getNewPrecedence (), $config [ 'precedence' ]);
$max = max ( $config [ 'precedence_change' ] -> getNewPrecedence (), $config [ 'precedence' ]);
foreach ( $ops as $c ) {
if ( $c [ 'precedence' ] > $min && $c [ 'precedence' ] < $max ) {
$this -> precedenceChanges [ $c [ 'type' ] . '_' . $c [ 'name' ]][] = $name ;
}
}
}
2024-06-20 14:10:42 +00:00
}
2025-01-13 09:56:01 +00:00
public function parseExpression ( $precedence = 0 )
2024-06-20 14:10:42 +00:00
{
2025-01-13 09:56:01 +00:00
if ( \func_num_args () > 1 ) {
trigger_deprecation ( 'twig/twig' , '3.15' , 'Passing a second argument ($allowArrow) to "%s()" is deprecated.' , __METHOD__ );
}
if ( $arrow = $this -> parseArrow ()) {
2024-06-20 14:10:42 +00:00
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 {
2025-01-13 09:56:01 +00:00
$previous = $this -> setDeprecationCheck ( true );
try {
$expr1 = $this -> parseExpression ( self :: OPERATOR_LEFT === $op [ 'associativity' ] ? $op [ 'precedence' ] + 1 : $op [ 'precedence' ]);
} finally {
$this -> setDeprecationCheck ( $previous );
}
2024-06-20 14:10:42 +00:00
$class = $op [ 'class' ];
$expr = new $class ( $expr , $expr1 , $token -> getLine ());
}
2025-01-13 09:56:01 +00:00
$expr -> setAttribute ( 'operator' , 'binary_' . $token -> getValue ());
$this -> triggerPrecedenceDeprecations ( $expr );
2024-06-20 14:10:42 +00:00
$token = $this -> parser -> getCurrentToken ();
}
if ( 0 === $precedence ) {
return $this -> parseConditionalExpression ( $expr );
}
return $expr ;
}
2025-01-13 09:56:01 +00:00
private function triggerPrecedenceDeprecations ( AbstractExpression $expr ) : void
{
// Check that the all nodes that are between the 2 precedences have explicit parentheses
if ( ! $expr -> hasAttribute ( 'operator' ) || ! isset ( $this -> precedenceChanges [ $expr -> getAttribute ( 'operator' )])) {
return ;
}
if ( str_starts_with ( $unaryOp = $expr -> getAttribute ( 'operator' ), 'unary' )) {
if ( $expr -> hasExplicitParentheses ()) {
return ;
}
$target = explode ( '_' , $unaryOp )[ 1 ];
/** @var AbstractExpression $node */
$node = $expr -> getNode ( 'node' );
foreach ( $this -> precedenceChanges as $operatorName => $changes ) {
if ( ! \in_array ( $unaryOp , $changes )) {
continue ;
}
if ( $node -> hasAttribute ( 'operator' ) && $operatorName === $node -> getAttribute ( 'operator' )) {
$change = $this -> unaryOperators [ $target ][ 'precedence_change' ];
trigger_deprecation ( $change -> getPackage (), $change -> getVersion (), \sprintf ( 'Add explicit parentheses around the "%s" unary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.' , $target , $this -> parser -> getStream () -> getSourceContext () -> getName (), $node -> getTemplateLine ()));
}
}
} else {
foreach ( $this -> precedenceChanges [ $expr -> getAttribute ( 'operator' )] as $operatorName ) {
foreach ( $expr as $node ) {
/** @var AbstractExpression $node */
if ( $node -> hasAttribute ( 'operator' ) && $operatorName === $node -> getAttribute ( 'operator' ) && ! $node -> hasExplicitParentheses ()) {
$op = explode ( '_' , $operatorName )[ 1 ];
$change = $this -> binaryOperators [ $op ][ 'precedence_change' ];
trigger_deprecation ( $change -> getPackage (), $change -> getVersion (), \sprintf ( 'Add explicit parentheses around the "%s" binary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.' , $op , $this -> parser -> getStream () -> getSourceContext () -> getName (), $node -> getTemplateLine ()));
}
}
}
}
}
2024-06-20 14:10:42 +00:00
/**
* @ return ArrowFunctionExpression | null
*/
private function parseArrow ()
{
$stream = $this -> parser -> getStream ();
// short array syntax (one argument, no parentheses)?
2025-01-13 09:56:01 +00:00
if ( $stream -> look ( 1 ) -> test ( Token :: ARROW_TYPE )) {
2024-06-20 14:10:42 +00:00
$line = $stream -> getCurrent () -> getLine ();
2025-01-13 09:56:01 +00:00
$token = $stream -> expect ( Token :: NAME_TYPE );
$names = [ new AssignContextVariable ( $token -> getValue (), $token -> getLine ())];
$stream -> expect ( Token :: ARROW_TYPE );
2024-06-20 14:10:42 +00:00
2025-01-13 09:56:01 +00:00
return new ArrowFunctionExpression ( $this -> parseExpression (), new Nodes ( $names ), $line );
2024-06-20 14:10:42 +00:00
}
// first, determine if we are parsing an arrow function by finding => (long form)
$i = 0 ;
2025-01-13 09:56:01 +00:00
if ( ! $stream -> look ( $i ) -> test ( Token :: PUNCTUATION_TYPE , '(' )) {
2024-06-20 14:10:42 +00:00
return null ;
}
++ $i ;
while ( true ) {
// variable name
++ $i ;
2025-01-13 09:56:01 +00:00
if ( ! $stream -> look ( $i ) -> test ( Token :: PUNCTUATION_TYPE , ',' )) {
2024-06-20 14:10:42 +00:00
break ;
}
++ $i ;
}
2025-01-13 09:56:01 +00:00
if ( ! $stream -> look ( $i ) -> test ( Token :: PUNCTUATION_TYPE , ')' )) {
2024-06-20 14:10:42 +00:00
return null ;
}
++ $i ;
2025-01-13 09:56:01 +00:00
if ( ! $stream -> look ( $i ) -> test ( Token :: ARROW_TYPE )) {
2024-06-20 14:10:42 +00:00
return null ;
}
// yes, let's parse it properly
2025-01-13 09:56:01 +00:00
$token = $stream -> expect ( Token :: PUNCTUATION_TYPE , '(' );
2024-06-20 14:10:42 +00:00
$line = $token -> getLine ();
$names = [];
while ( true ) {
2025-01-13 09:56:01 +00:00
$token = $stream -> expect ( Token :: NAME_TYPE );
$names [] = new AssignContextVariable ( $token -> getValue (), $token -> getLine ());
2024-06-20 14:10:42 +00:00
2025-01-13 09:56:01 +00:00
if ( ! $stream -> nextIf ( Token :: PUNCTUATION_TYPE , ',' )) {
2024-06-20 14:10:42 +00:00
break ;
}
}
2025-01-13 09:56:01 +00:00
$stream -> expect ( Token :: PUNCTUATION_TYPE , ')' );
$stream -> expect ( Token :: ARROW_TYPE );
2024-06-20 14:10:42 +00:00
2025-01-13 09:56:01 +00:00
return new ArrowFunctionExpression ( $this -> parseExpression (), new Nodes ( $names ), $line );
2024-06-20 14:10:42 +00:00
}
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' ];
2025-01-13 09:56:01 +00:00
$expr = new $class ( $expr , $token -> getLine ());
$expr -> setAttribute ( 'operator' , 'unary_' . $token -> getValue ());
if ( $this -> deprecationCheck ) {
$this -> triggerPrecedenceDeprecations ( $expr );
}
return $this -> parsePostfixExpression ( $expr );
} elseif ( $token -> test ( Token :: PUNCTUATION_TYPE , '(' )) {
2024-06-20 14:10:42 +00:00
$this -> parser -> getStream () -> next ();
2025-01-13 09:56:01 +00:00
$previous = $this -> setDeprecationCheck ( false );
try {
$expr = $this -> parseExpression () -> setExplicitParentheses ();
} finally {
$this -> setDeprecationCheck ( $previous );
}
$this -> parser -> getStream () -> expect ( Token :: PUNCTUATION_TYPE , ')' , 'An opened parenthesis is not properly closed' );
2024-06-20 14:10:42 +00:00
return $this -> parsePostfixExpression ( $expr );
}
return $this -> parsePrimaryExpression ();
}
private function parseConditionalExpression ( $expr ) : AbstractExpression
{
2025-01-13 09:56:01 +00:00
while ( $this -> parser -> getStream () -> nextIf ( Token :: PUNCTUATION_TYPE , '?' )) {
$expr2 = $this -> parseExpression ();
if ( $this -> parser -> getStream () -> nextIf ( Token :: PUNCTUATION_TYPE , ':' )) {
// Ternary operator (expr ? expr2 : expr3)
2024-06-20 14:10:42 +00:00
$expr3 = $this -> parseExpression ();
2025-01-13 09:56:01 +00:00
} else {
// Ternary without else (expr ? expr2)
$expr3 = new ConstantExpression ( '' , $this -> parser -> getCurrentToken () -> getLine ());
2024-06-20 14:10:42 +00:00
}
2025-01-13 09:56:01 +00:00
$expr = new ConditionalTernary ( $expr , $expr2 , $expr3 , $this -> parser -> getCurrentToken () -> getLine ());
2024-06-20 14:10:42 +00:00
}
return $expr ;
}
private function isUnary ( Token $token ) : bool
{
2025-01-13 09:56:01 +00:00
return $token -> test ( Token :: OPERATOR_TYPE ) && isset ( $this -> unaryOperators [ $token -> getValue ()]);
2024-06-20 14:10:42 +00:00
}
private function isBinary ( Token $token ) : bool
{
2025-01-13 09:56:01 +00:00
return $token -> test ( Token :: OPERATOR_TYPE ) && isset ( $this -> binaryOperators [ $token -> getValue ()]);
2024-06-20 14:10:42 +00:00
}
public function parsePrimaryExpression ()
{
$token = $this -> parser -> getCurrentToken ();
switch ( $token -> getType ()) {
2025-01-13 09:56:01 +00:00
case Token :: NAME_TYPE :
2024-06-20 14:10:42 +00:00
$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 {
2025-01-13 09:56:01 +00:00
$node = new ContextVariable ( $token -> getValue (), $token -> getLine ());
2024-06-20 14:10:42 +00:00
}
}
break ;
2025-01-13 09:56:01 +00:00
case Token :: NUMBER_TYPE :
2024-06-20 14:10:42 +00:00
$this -> parser -> getStream () -> next ();
$node = new ConstantExpression ( $token -> getValue (), $token -> getLine ());
break ;
2025-01-13 09:56:01 +00:00
case Token :: STRING_TYPE :
case Token :: INTERPOLATION_START_TYPE :
2024-06-20 14:10:42 +00:00
$node = $this -> parseStringExpression ();
break ;
2025-01-13 09:56:01 +00:00
case Token :: PUNCTUATION_TYPE :
$node = match ( $token -> getValue ()) {
'[' => $this -> parseSequenceExpression (),
'{' => $this -> parseMappingExpression (),
default => throw new SyntaxError ( \sprintf ( 'Unexpected token "%s" of value "%s".' , Token :: typeToEnglish ( $token -> getType ()), $token -> getValue ()), $token -> getLine (), $this -> parser -> getStream () -> getSourceContext ()),
};
break ;
case Token :: OPERATOR_TYPE :
2024-06-20 14:10:42 +00:00
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 ();
2025-01-13 09:56:01 +00:00
$node = new ContextVariable ( $token -> getValue (), $token -> getLine ());
2024-06-20 14:10:42 +00:00
break ;
}
2025-01-13 09:56:01 +00:00
if ( '=' === $token -> getValue () && ( '==' === $this -> parser -> getStream () -> look ( - 1 ) -> getValue () || '!=' === $this -> parser -> getStream () -> look ( - 1 ) -> getValue ())) {
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
}
// no break
default :
2025-01-13 09:56:01 +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 ) {
2025-01-13 09:56:01 +00:00
if ( $nextCanBeString && $token = $stream -> nextIf ( Token :: STRING_TYPE )) {
2024-06-20 14:10:42 +00:00
$nodes [] = new ConstantExpression ( $token -> getValue (), $token -> getLine ());
$nextCanBeString = false ;
2025-01-13 09:56:01 +00:00
} elseif ( $stream -> nextIf ( Token :: INTERPOLATION_START_TYPE )) {
2024-06-20 14:10:42 +00:00
$nodes [] = $this -> parseExpression ();
2025-01-13 09:56:01 +00:00
$stream -> expect ( Token :: INTERPOLATION_END_TYPE );
2024-06-20 14:10:42 +00:00
$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
/**
2025-01-13 09:56:01 +00:00
* @ deprecated since Twig 3.11 , use parseSequenceExpression () instead
2024-09-05 17:51:48 +00:00
*/
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 ();
2025-01-13 09:56:01 +00:00
$stream -> expect ( Token :: PUNCTUATION_TYPE , '[' , 'A sequence element was expected' );
2024-06-20 14:10:42 +00:00
$node = new ArrayExpression ([], $stream -> getCurrent () -> getLine ());
$first = true ;
2025-01-13 09:56:01 +00:00
while ( ! $stream -> test ( Token :: PUNCTUATION_TYPE , ']' )) {
2024-06-20 14:10:42 +00:00
if ( ! $first ) {
2025-01-13 09:56:01 +00:00
$stream -> expect ( Token :: PUNCTUATION_TYPE , ',' , 'A sequence element must be followed by a comma' );
2024-06-20 14:10:42 +00:00
// trailing ,?
2025-01-13 09:56:01 +00:00
if ( $stream -> test ( Token :: PUNCTUATION_TYPE , ']' )) {
2024-06-20 14:10:42 +00:00
break ;
}
}
$first = false ;
2025-01-13 09:56:01 +00:00
if ( $stream -> nextIf ( Token :: SPREAD_TYPE )) {
2024-06-20 14:10:42 +00:00
$expr = $this -> parseExpression ();
$expr -> setAttribute ( 'spread' , true );
$node -> addElement ( $expr );
} else {
$node -> addElement ( $this -> parseExpression ());
}
}
2025-01-13 09:56:01 +00:00
$stream -> expect ( Token :: PUNCTUATION_TYPE , ']' , '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
/**
2025-01-13 09:56:01 +00:00
* @ deprecated since Twig 3.11 , use parseMappingExpression () instead
2024-09-05 17:51:48 +00:00
*/
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 ();
2025-01-13 09:56:01 +00:00
$stream -> expect ( Token :: PUNCTUATION_TYPE , '{' , 'A mapping element was expected' );
2024-06-20 14:10:42 +00:00
$node = new ArrayExpression ([], $stream -> getCurrent () -> getLine ());
$first = true ;
2025-01-13 09:56:01 +00:00
while ( ! $stream -> test ( Token :: PUNCTUATION_TYPE , '}' )) {
2024-06-20 14:10:42 +00:00
if ( ! $first ) {
2025-01-13 09:56:01 +00:00
$stream -> expect ( Token :: PUNCTUATION_TYPE , ',' , 'A mapping value must be followed by a comma' );
2024-06-20 14:10:42 +00:00
// trailing ,?
2025-01-13 09:56:01 +00:00
if ( $stream -> test ( Token :: PUNCTUATION_TYPE , '}' )) {
2024-06-20 14:10:42 +00:00
break ;
}
}
$first = false ;
2025-01-13 09:56:01 +00:00
if ( $stream -> nextIf ( Token :: SPREAD_TYPE )) {
2024-06-20 14:10:42 +00:00
$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)
2025-01-13 09:56:01 +00:00
if ( $token = $stream -> nextIf ( Token :: NAME_TYPE )) {
2024-06-20 14:10:42 +00:00
$key = new ConstantExpression ( $token -> getValue (), $token -> getLine ());
// {a} is a shortcut for {a:a}
if ( $stream -> test ( Token :: PUNCTUATION_TYPE , [ ',' , '}' ])) {
2025-01-13 09:56:01 +00:00
$value = new ContextVariable ( $key -> getAttribute ( 'value' ), $key -> getTemplateLine ());
2024-06-20 14:10:42 +00:00
$node -> addElement ( $value , $key );
continue ;
}
2025-01-13 09:56:01 +00:00
} elseif (( $token = $stream -> nextIf ( Token :: STRING_TYPE )) || $token = $stream -> nextIf ( Token :: NUMBER_TYPE )) {
2024-06-20 14:10:42 +00:00
$key = new ConstantExpression ( $token -> getValue (), $token -> getLine ());
2025-01-13 09:56:01 +00:00
} elseif ( $stream -> test ( Token :: PUNCTUATION_TYPE , '(' )) {
2024-06-20 14:10:42 +00:00
$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
}
2025-01-13 09:56:01 +00:00
$stream -> expect ( Token :: PUNCTUATION_TYPE , ':' , 'A mapping key must be followed by a colon (:)' );
2024-06-20 14:10:42 +00:00
$value = $this -> parseExpression ();
$node -> addElement ( $value , $key );
}
2025-01-13 09:56:01 +00:00
$stream -> expect ( Token :: PUNCTUATION_TYPE , '}' , '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 ();
2025-01-13 09:56:01 +00:00
if ( Token :: PUNCTUATION_TYPE == $token -> getType ()) {
2024-06-20 14:10:42 +00:00
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 )
{
2025-01-13 09:56:01 +00:00
if ( null !== $alias = $this -> parser -> getImportedSymbol ( 'function' , $name )) {
return new MacroReferenceExpression ( $alias [ 'node' ] -> getNode ( 'var' ), $alias [ 'name' ], $this -> createArguments ( $line ), $line );
}
2024-06-20 14:10:42 +00:00
2025-01-13 09:56:01 +00:00
$args = $this -> parseOnlyArguments ();
$function = $this -> getFunction ( $name , $line );
2024-06-20 14:10:42 +00:00
2025-01-13 09:56:01 +00:00
if ( $function -> getParserCallable ()) {
$fakeNode = new EmptyNode ( $line );
$fakeNode -> setSourceContext ( $this -> parser -> getStream () -> getSourceContext ());
2024-06-20 14:10:42 +00:00
2025-01-13 09:56:01 +00:00
return ( $function -> getParserCallable ())( $this -> parser , $fakeNode , $args , $line );
}
2024-06-20 14:10:42 +00:00
2025-01-13 09:56:01 +00:00
if ( ! isset ( $this -> readyNodes [ $class = $function -> getNodeClass ()])) {
$this -> readyNodes [ $class ] = ( bool ) ( new \ReflectionClass ( $class )) -> getConstructor () -> getAttributes ( FirstClassTwigCallableReady :: class );
}
2024-06-20 14:10:42 +00:00
2025-01-13 09:56:01 +00:00
if ( ! $ready = $this -> readyNodes [ $class ]) {
trigger_deprecation ( 'twig/twig' , '3.12' , 'Twig node "%s" is not marked as ready for passing a "TwigFunction" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.' , $class );
2024-06-20 14:10:42 +00:00
}
2025-01-13 09:56:01 +00:00
return new $class ( $ready ? $function : $function -> getName (), $args , $line );
2024-06-20 14:10:42 +00:00
}
public function parseSubscriptExpression ( $node )
{
2025-01-13 09:56:01 +00:00
if ( '.' === $this -> parser -> getStream () -> next () -> getValue ()) {
return $this -> parseSubscriptExpressionDot ( $node );
2024-06-20 14:10:42 +00:00
}
2025-01-13 09:56:01 +00:00
return $this -> parseSubscriptExpressionArray ( $node );
2024-06-20 14:10:42 +00:00
}
public function parseFilterExpression ( $node )
{
$this -> parser -> getStream () -> next ();
return $this -> parseFilterExpressionRaw ( $node );
}
2025-01-13 09:56:01 +00:00
public function parseFilterExpressionRaw ( $node )
2024-06-20 14:10:42 +00:00
{
2025-01-13 09:56:01 +00:00
if ( \func_num_args () > 1 ) {
trigger_deprecation ( 'twig/twig' , '3.12' , 'Passing a second argument to "%s()" is deprecated.' , __METHOD__ );
}
2024-06-20 14:10:42 +00:00
while ( true ) {
2025-01-13 09:56:01 +00:00
$token = $this -> parser -> getStream () -> expect ( Token :: NAME_TYPE );
2024-06-20 14:10:42 +00:00
2025-01-13 09:56:01 +00:00
if ( ! $this -> parser -> getStream () -> test ( Token :: PUNCTUATION_TYPE , '(' )) {
$arguments = new EmptyNode ();
2024-06-20 14:10:42 +00:00
} else {
2025-01-13 09:56:01 +00:00
$arguments = $this -> parseOnlyArguments ();
2024-06-20 14:10:42 +00:00
}
2025-01-13 09:56:01 +00:00
$filter = $this -> getFilter ( $token -> getValue (), $token -> getLine ());
2024-06-20 14:10:42 +00:00
2025-01-13 09:56:01 +00:00
$ready = true ;
if ( ! isset ( $this -> readyNodes [ $class = $filter -> getNodeClass ()])) {
$this -> readyNodes [ $class ] = ( bool ) ( new \ReflectionClass ( $class )) -> getConstructor () -> getAttributes ( FirstClassTwigCallableReady :: class );
}
if ( ! $ready = $this -> readyNodes [ $class ]) {
trigger_deprecation ( 'twig/twig' , '3.12' , 'Twig node "%s" is not marked as ready for passing a "TwigFilter" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.' , $class );
}
2024-06-20 14:10:42 +00:00
2025-01-13 09:56:01 +00:00
$node = new $class ( $node , $ready ? $filter : new ConstantExpression ( $filter -> getName (), $token -> getLine ()), $arguments , $token -> getLine ());
if ( ! $this -> parser -> getStream () -> test ( Token :: PUNCTUATION_TYPE , '|' )) {
2024-06-20 14:10:42 +00:00
break ;
}
$this -> parser -> getStream () -> next ();
}
return $node ;
}
/**
* Parses arguments .
*
* @ return Node
*
* @ throws SyntaxError
*/
2025-01-13 09:56:01 +00:00
public function parseArguments ()
2024-06-20 14:10:42 +00:00
{
2025-01-13 09:56:01 +00:00
$namedArguments = false ;
$definition = false ;
if ( \func_num_args () > 1 ) {
$definition = func_get_arg ( 1 );
}
if ( \func_num_args () > 0 ) {
trigger_deprecation ( 'twig/twig' , '3.15' , 'Passing arguments to "%s()" is deprecated.' , __METHOD__ );
$namedArguments = func_get_arg ( 0 );
}
2024-06-20 14:10:42 +00:00
$args = [];
$stream = $this -> parser -> getStream ();
2025-01-13 09:56:01 +00:00
$stream -> expect ( Token :: PUNCTUATION_TYPE , '(' , 'A list of arguments must begin with an opening parenthesis' );
$hasSpread = false ;
while ( ! $stream -> test ( Token :: PUNCTUATION_TYPE , ')' )) {
if ( $args ) {
$stream -> expect ( Token :: PUNCTUATION_TYPE , ',' , 'Arguments must be separated by a comma' );
2024-06-20 14:10:42 +00:00
// if the comma above was a trailing comma, early exit the argument parse loop
2025-01-13 09:56:01 +00:00
if ( $stream -> test ( Token :: PUNCTUATION_TYPE , ')' )) {
2024-06-20 14:10:42 +00:00
break ;
}
}
if ( $definition ) {
2025-01-13 09:56:01 +00:00
$token = $stream -> expect ( Token :: NAME_TYPE , null , 'An argument must be a name' );
$value = new ContextVariable ( $token -> getValue (), $this -> parser -> getCurrentToken () -> getLine ());
2024-06-20 14:10:42 +00:00
} else {
2025-01-13 09:56:01 +00:00
if ( $stream -> nextIf ( Token :: SPREAD_TYPE )) {
$hasSpread = true ;
$value = new SpreadUnary ( $this -> parseExpression (), $stream -> getCurrent () -> getLine ());
} elseif ( $hasSpread ) {
throw new SyntaxError ( 'Normal arguments must be placed before argument unpacking.' , $stream -> getCurrent () -> getLine (), $stream -> getSourceContext ());
} else {
$value = $this -> parseExpression ();
}
2024-06-20 14:10:42 +00:00
}
$name = null ;
2025-01-13 09:56:01 +00:00
if ( $namedArguments && (( $token = $stream -> nextIf ( Token :: OPERATOR_TYPE , '=' )) || ( ! $definition && $token = $stream -> nextIf ( Token :: PUNCTUATION_TYPE , ':' )))) {
2024-06-20 14:10:42 +00:00
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 ) {
2025-01-13 09:56:01 +00:00
$value = $this -> getPrimary ();
2024-06-20 14:10:42 +00:00
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 {
2025-01-13 09:56:01 +00:00
$value = $this -> parseExpression ();
2024-06-20 14:10:42 +00:00
}
}
if ( $definition ) {
if ( null === $name ) {
$name = $value -> getAttribute ( 'name' );
$value = new ConstantExpression ( null , $this -> parser -> getCurrentToken () -> getLine ());
2025-01-13 09:56:01 +00:00
$value -> setAttribute ( 'is_implicit' , true );
2024-06-20 14:10:42 +00:00
}
$args [ $name ] = $value ;
} else {
if ( null === $name ) {
$args [] = $value ;
} else {
$args [ $name ] = $value ;
}
}
}
2025-01-13 09:56:01 +00:00
$stream -> expect ( Token :: PUNCTUATION_TYPE , ')' , 'A list of arguments must be closed by a parenthesis' );
2024-06-20 14:10:42 +00:00
2025-01-13 09:56:01 +00:00
return new Nodes ( $args );
2024-06-20 14:10:42 +00:00
}
public function parseAssignmentExpression ()
{
$stream = $this -> parser -> getStream ();
$targets = [];
while ( true ) {
$token = $this -> parser -> getCurrentToken ();
2025-01-13 09:56:01 +00:00
if ( $stream -> test ( Token :: OPERATOR_TYPE ) && preg_match ( Lexer :: REGEX_NAME , $token -> getValue ())) {
2024-06-20 14:10:42 +00:00
// in this context, string operators are variable names
$this -> parser -> getStream () -> next ();
} else {
2025-01-13 09:56:01 +00:00
$stream -> expect ( Token :: NAME_TYPE , null , 'Only variables can be assigned to' );
2024-06-20 14:10:42 +00:00
}
2025-01-13 09:56:01 +00:00
$targets [] = new AssignContextVariable ( $token -> getValue (), $token -> getLine ());
2024-06-20 14:10:42 +00:00
2025-01-13 09:56:01 +00:00
if ( ! $stream -> nextIf ( Token :: PUNCTUATION_TYPE , ',' )) {
2024-06-20 14:10:42 +00:00
break ;
}
}
2025-01-13 09:56:01 +00:00
return new Nodes ( $targets );
2024-06-20 14:10:42 +00:00
}
public function parseMultitargetExpression ()
{
$targets = [];
while ( true ) {
$targets [] = $this -> parseExpression ();
2025-01-13 09:56:01 +00:00
if ( ! $this -> parser -> getStream () -> nextIf ( Token :: PUNCTUATION_TYPE , ',' )) {
2024-06-20 14:10:42 +00:00
break ;
}
}
2025-01-13 09:56:01 +00:00
return new Nodes ( $targets );
2024-06-20 14:10:42 +00:00
}
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 ();
2025-01-13 09:56:01 +00:00
$test = $this -> getTest ( $node -> getTemplateLine ());
2024-06-20 14:10:42 +00:00
$arguments = null ;
2025-01-13 09:56:01 +00:00
if ( $stream -> test ( Token :: PUNCTUATION_TYPE , '(' )) {
$arguments = $this -> parseOnlyArguments ();
2024-06-20 14:10:42 +00:00
} elseif ( $test -> hasOneMandatoryArgument ()) {
2025-01-13 09:56:01 +00:00
$arguments = new Nodes ([ 0 => $this -> getPrimary ()]);
}
if ( 'defined' === $test -> getName () && $node instanceof NameExpression && null !== $alias = $this -> parser -> getImportedSymbol ( 'function' , $node -> getAttribute ( 'name' ))) {
$node = new MacroReferenceExpression ( $alias [ 'node' ] -> getNode ( 'var' ), $alias [ 'name' ], new ArrayExpression ([], $node -> getTemplateLine ()), $node -> getTemplateLine ());
2024-06-20 14:10:42 +00:00
}
2025-01-13 09:56:01 +00:00
$ready = $test instanceof TwigTest ;
if ( ! isset ( $this -> readyNodes [ $class = $test -> getNodeClass ()])) {
$this -> readyNodes [ $class ] = ( bool ) ( new \ReflectionClass ( $class )) -> getConstructor () -> getAttributes ( FirstClassTwigCallableReady :: class );
2024-06-20 14:10:42 +00:00
}
2025-01-13 09:56:01 +00:00
if ( ! $ready = $this -> readyNodes [ $class ]) {
trigger_deprecation ( 'twig/twig' , '3.12' , 'Twig node "%s" is not marked as ready for passing a "TwigTest" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.' , $class );
}
return new $class ( $node , $ready ? $test : $test -> getName (), $arguments , $this -> parser -> getCurrentToken () -> getLine ());
2024-06-20 14:10:42 +00:00
}
2025-01-13 09:56:01 +00:00
private function getTest ( int $line ) : TwigTest
2024-06-20 14:10:42 +00:00
{
$stream = $this -> parser -> getStream ();
2025-01-13 09:56:01 +00:00
$name = $stream -> expect ( Token :: NAME_TYPE ) -> getValue ();
2024-06-20 14:10:42 +00:00
2025-01-13 09:56:01 +00:00
if ( $stream -> test ( Token :: NAME_TYPE )) {
2024-06-20 14:10:42 +00:00
// try 2-words tests
$name = $name . ' ' . $this -> parser -> getCurrentToken () -> getValue ();
if ( $test = $this -> env -> getTest ( $name )) {
$stream -> next ();
}
2025-01-13 09:56:01 +00:00
} else {
$test = $this -> env -> getTest ( $name );
2024-06-20 14:10:42 +00:00
}
2025-01-13 09:56:01 +00:00
if ( ! $test ) {
if ( $this -> parser -> shouldIgnoreUnknownTwigCallables ()) {
return new TwigTest ( $name , fn () => '' );
}
$e = new SyntaxError ( \sprintf ( 'Unknown "%s" test.' , $name ), $line , $stream -> getSourceContext ());
$e -> addSuggestions ( $name , array_keys ( $this -> env -> getTests ()));
2024-06-20 14:10:42 +00:00
2025-01-13 09:56:01 +00:00
throw $e ;
}
2024-06-20 14:10:42 +00:00
if ( $test -> isDeprecated ()) {
$stream = $this -> parser -> getStream ();
$src = $stream -> getSourceContext ();
2025-01-13 09:56:01 +00:00
$test -> triggerDeprecation ( $src -> getPath () ? : $src -> getName (), $stream -> getCurrent () -> getLine ());
2024-06-20 14:10:42 +00:00
}
2025-01-13 09:56:01 +00:00
return $test ;
2024-06-20 14:10:42 +00:00
}
2025-01-13 09:56:01 +00:00
private function getFunction ( string $name , int $line ) : TwigFunction
2024-06-20 14:10:42 +00:00
{
if ( ! $function = $this -> env -> getFunction ( $name )) {
2025-01-13 09:56:01 +00:00
if ( $this -> parser -> shouldIgnoreUnknownTwigCallables ()) {
return new TwigFunction ( $name , fn () => '' );
}
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 ()) {
$src = $this -> parser -> getStream () -> getSourceContext ();
2025-01-13 09:56:01 +00:00
$function -> triggerDeprecation ( $src -> getPath () ? : $src -> getName (), $line );
2024-06-20 14:10:42 +00:00
}
2025-01-13 09:56:01 +00:00
return $function ;
2024-06-20 14:10:42 +00:00
}
2025-01-13 09:56:01 +00:00
private function getFilter ( string $name , int $line ) : TwigFilter
2024-06-20 14:10:42 +00:00
{
if ( ! $filter = $this -> env -> getFilter ( $name )) {
2025-01-13 09:56:01 +00:00
if ( $this -> parser -> shouldIgnoreUnknownTwigCallables ()) {
return new TwigFilter ( $name , fn () => '' );
}
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 ()) {
$src = $this -> parser -> getStream () -> getSourceContext ();
2025-01-13 09:56:01 +00:00
$filter -> triggerDeprecation ( $src -> getPath () ? : $src -> getName (), $line );
2024-06-20 14:10:42 +00:00
}
2025-01-13 09:56:01 +00:00
return $filter ;
2024-06-20 14:10:42 +00:00
}
// checks that the node only contains "constant" elements
2025-01-13 09:56:01 +00:00
// to be removed in 4.0
2024-06-20 14:10:42 +00:00
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 ;
}
2025-01-13 09:56:01 +00:00
private function setDeprecationCheck ( bool $deprecationCheck ) : bool
{
$current = $this -> deprecationCheck ;
$this -> deprecationCheck = $deprecationCheck ;
return $current ;
}
private function createArguments ( int $line ) : ArrayExpression
{
$arguments = new ArrayExpression ([], $line );
foreach ( $this -> parseOnlyArguments () as $k => $n ) {
$arguments -> addElement ( $n , new LocalVariable ( $k , $line ));
}
return $arguments ;
}
public function parseOnlyArguments ()
{
$args = [];
$stream = $this -> parser -> getStream ();
$stream -> expect ( Token :: PUNCTUATION_TYPE , '(' , 'A list of arguments must begin with an opening parenthesis' );
$hasSpread = false ;
while ( ! $stream -> test ( Token :: PUNCTUATION_TYPE , ')' )) {
if ( $args ) {
$stream -> expect ( Token :: PUNCTUATION_TYPE , ',' , '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 , ')' )) {
break ;
}
}
if ( $stream -> nextIf ( Token :: SPREAD_TYPE )) {
$hasSpread = true ;
$value = new SpreadUnary ( $this -> parseExpression (), $stream -> getCurrent () -> getLine ());
} elseif ( $hasSpread ) {
throw new SyntaxError ( 'Normal arguments must be placed before argument unpacking.' , $stream -> getCurrent () -> getLine (), $stream -> getSourceContext ());
} else {
$value = $this -> parseExpression ();
}
$name = null ;
if (( $token = $stream -> nextIf ( Token :: OPERATOR_TYPE , '=' )) || ( $token = $stream -> nextIf ( Token :: PUNCTUATION_TYPE , ':' ))) {
if ( ! $value instanceof NameExpression ) {
throw new SyntaxError ( \sprintf ( 'A parameter name must be a string, "%s" given.' , \get_class ( $value )), $token -> getLine (), $stream -> getSourceContext ());
}
$name = $value -> getAttribute ( 'name' );
$value = $this -> parseExpression ();
}
if ( null === $name ) {
$args [] = $value ;
} else {
$args [ $name ] = $value ;
}
}
$stream -> expect ( Token :: PUNCTUATION_TYPE , ')' , 'A list of arguments must be closed by a parenthesis' );
return new Nodes ( $args );
}
private function parseSubscriptExpressionDot ( Node $node ) : AbstractExpression
{
$stream = $this -> parser -> getStream ();
$token = $stream -> getCurrent ();
$lineno = $token -> getLine ();
$arguments = new ArrayExpression ([], $lineno );
$type = Template :: ANY_CALL ;
if ( $stream -> nextIf ( Token :: PUNCTUATION_TYPE , '(' )) {
$attribute = $this -> parseExpression ();
$stream -> expect ( Token :: PUNCTUATION_TYPE , ')' );
} else {
$token = $stream -> next ();
if (
Token :: NAME_TYPE == $token -> getType ()
|| Token :: NUMBER_TYPE == $token -> getType ()
|| ( Token :: OPERATOR_TYPE == $token -> getType () && preg_match ( Lexer :: REGEX_NAME , $token -> getValue ()))
) {
$attribute = new ConstantExpression ( $token -> getValue (), $token -> getLine ());
} else {
throw new SyntaxError ( \sprintf ( 'Expected name or number, got value "%s" of type %s.' , $token -> getValue (), Token :: typeToEnglish ( $token -> getType ())), $token -> getLine (), $stream -> getSourceContext ());
}
}
if ( $stream -> test ( Token :: PUNCTUATION_TYPE , '(' )) {
$type = Template :: METHOD_CALL ;
$arguments = $this -> createArguments ( $token -> getLine ());
}
if (
$node instanceof NameExpression
&& (
null !== $this -> parser -> getImportedSymbol ( 'template' , $node -> getAttribute ( 'name' ))
|| '_self' === $node -> getAttribute ( 'name' ) && $attribute instanceof ConstantExpression
)
) {
return new MacroReferenceExpression ( new TemplateVariable ( $node -> getAttribute ( 'name' ), $node -> getTemplateLine ()), 'macro_' . $attribute -> getAttribute ( 'value' ), $arguments , $node -> getTemplateLine ());
}
return new GetAttrExpression ( $node , $attribute , $arguments , $type , $lineno );
}
private function parseSubscriptExpressionArray ( Node $node ) : AbstractExpression
{
$stream = $this -> parser -> getStream ();
$token = $stream -> getCurrent ();
$lineno = $token -> getLine ();
$arguments = new ArrayExpression ([], $lineno );
// slice?
$slice = false ;
if ( $stream -> test ( Token :: PUNCTUATION_TYPE , ':' )) {
$slice = true ;
$attribute = new ConstantExpression ( 0 , $token -> getLine ());
} else {
$attribute = $this -> parseExpression ();
}
if ( $stream -> nextIf ( Token :: PUNCTUATION_TYPE , ':' )) {
$slice = true ;
}
if ( $slice ) {
if ( $stream -> test ( Token :: PUNCTUATION_TYPE , ']' )) {
$length = new ConstantExpression ( null , $token -> getLine ());
} else {
$length = $this -> parseExpression ();
}
$filter = $this -> getFilter ( 'slice' , $token -> getLine ());
$arguments = new Nodes ([ $attribute , $length ]);
$filter = new ( $filter -> getNodeClass ())( $node , $filter , $arguments , $token -> getLine ());
$stream -> expect ( Token :: PUNCTUATION_TYPE , ']' );
return $filter ;
}
$stream -> expect ( Token :: PUNCTUATION_TYPE , ']' );
return new GetAttrExpression ( $node , $attribute , $arguments , Template :: ARRAY_CALL , $lineno );
}
2024-06-20 14:10:42 +00:00
}