Update Twig to v2.15.4

This commit is contained in:
slawkens 2023-02-02 10:37:45 +01:00
parent e552bcfe82
commit 130f7ba405
309 changed files with 3802 additions and 4005 deletions

View File

@ -18,7 +18,7 @@ namespace Twig\Cache;
*/ */
class FilesystemCache implements CacheInterface class FilesystemCache implements CacheInterface
{ {
const FORCE_BYTECODE_INVALIDATION = 1; public const FORCE_BYTECODE_INVALIDATION = 1;
private $directory; private $directory;
private $options; private $options;
@ -35,7 +35,7 @@ class FilesystemCache implements CacheInterface
public function generateKey($name, $className) public function generateKey($name, $className)
{ {
$hash = hash('sha256', $className); $hash = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $className);
return $this->directory.$hash[0].$hash[1].'/'.$hash.'.php'; return $this->directory.$hash[0].$hash[1].'/'.$hash.'.php';
} }
@ -67,7 +67,7 @@ class FilesystemCache implements CacheInterface
if (self::FORCE_BYTECODE_INVALIDATION == ($this->options & self::FORCE_BYTECODE_INVALIDATION)) { if (self::FORCE_BYTECODE_INVALIDATION == ($this->options & self::FORCE_BYTECODE_INVALIDATION)) {
// Compile cached file into bytecode cache // Compile cached file into bytecode cache
if (\function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), FILTER_VALIDATE_BOOLEAN)) { if (\function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) {
@opcache_invalidate($key, true); @opcache_invalidate($key, true);
} elseif (\function_exists('apc_compile_file')) { } elseif (\function_exists('apc_compile_file')) {
apc_compile_file($key); apc_compile_file($key);

View File

@ -14,11 +14,9 @@ namespace Twig\Cache;
/** /**
* Implements a no-cache strategy. * Implements a no-cache strategy.
* *
* @final
*
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*/ */
class NullCache implements CacheInterface final class NullCache implements CacheInterface
{ {
public function generateKey($name, $className) public function generateKey($name, $className)
{ {

View File

@ -12,23 +12,22 @@
namespace Twig; namespace Twig;
use Twig\Node\ModuleNode; use Twig\Node\Node;
/** /**
* Compiles a node to PHP code. * Compiles a node to PHP code.
* *
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*/ */
class Compiler implements \Twig_CompilerInterface class Compiler
{ {
protected $lastLine; private $lastLine;
protected $source; private $source;
protected $indentation; private $indentation;
protected $env; private $env;
protected $debugInfo = []; private $debugInfo = [];
protected $sourceOffset; private $sourceOffset;
protected $sourceLine; private $sourceLine;
protected $filename;
private $varNameSalt = 0; private $varNameSalt = 0;
public function __construct(Environment $env) public function __construct(Environment $env)
@ -36,16 +35,6 @@ class Compiler implements \Twig_CompilerInterface
$this->env = $env; $this->env = $env;
} }
/**
* @deprecated since 1.25 (to be removed in 2.0)
*/
public function getFilename()
{
@trigger_error(sprintf('The %s() method is deprecated since version 1.25 and will be removed in 2.0.', __FUNCTION__), E_USER_DEPRECATED);
return $this->filename;
}
/** /**
* Returns the environment instance related to this compiler. * Returns the environment instance related to this compiler.
* *
@ -73,7 +62,7 @@ class Compiler implements \Twig_CompilerInterface
* *
* @return $this * @return $this
*/ */
public function compile(\Twig_NodeInterface $node, $indentation = 0) public function compile(Node $node, $indentation = 0)
{ {
$this->lastLine = null; $this->lastLine = null;
$this->source = ''; $this->source = '';
@ -84,17 +73,12 @@ class Compiler implements \Twig_CompilerInterface
$this->indentation = $indentation; $this->indentation = $indentation;
$this->varNameSalt = 0; $this->varNameSalt = 0;
if ($node instanceof ModuleNode) {
// to be removed in 2.0
$this->filename = $node->getTemplateName();
}
$node->compile($this); $node->compile($this);
return $this; return $this;
} }
public function subcompile(\Twig_NodeInterface $node, $raw = true) public function subcompile(Node $node, $raw = true)
{ {
if (false === $raw) { if (false === $raw) {
$this->source .= str_repeat(' ', $this->indentation * 4); $this->source .= str_repeat(' ', $this->indentation * 4);
@ -124,9 +108,8 @@ class Compiler implements \Twig_CompilerInterface
* *
* @return $this * @return $this
*/ */
public function write() public function write(...$strings)
{ {
$strings = \func_get_args();
foreach ($strings as $string) { foreach ($strings as $string) {
$this->source .= str_repeat(' ', $this->indentation * 4).$string; $this->source .= str_repeat(' ', $this->indentation * 4).$string;
} }
@ -134,22 +117,6 @@ class Compiler implements \Twig_CompilerInterface
return $this; return $this;
} }
/**
* Appends an indentation to the current PHP code after compilation.
*
* @return $this
*
* @deprecated since 1.27 (to be removed in 2.0).
*/
public function addIndentation()
{
@trigger_error('The '.__METHOD__.' method is deprecated since version 1.27 and will be removed in 2.0. Use write(\'\') instead.', E_USER_DEPRECATED);
$this->source .= str_repeat(' ', $this->indentation * 4);
return $this;
}
/** /**
* Adds a quoted string to the compiled code. * Adds a quoted string to the compiled code.
* *
@ -174,21 +141,21 @@ class Compiler implements \Twig_CompilerInterface
public function repr($value) public function repr($value)
{ {
if (\is_int($value) || \is_float($value)) { if (\is_int($value) || \is_float($value)) {
if (false !== $locale = setlocale(LC_NUMERIC, '0')) { if (false !== $locale = setlocale(\LC_NUMERIC, '0')) {
setlocale(LC_NUMERIC, 'C'); setlocale(\LC_NUMERIC, 'C');
} }
$this->raw(var_export($value, true)); $this->raw(var_export($value, true));
if (false !== $locale) { if (false !== $locale) {
setlocale(LC_NUMERIC, $locale); setlocale(\LC_NUMERIC, $locale);
} }
} elseif (null === $value) { } elseif (null === $value) {
$this->raw('null'); $this->raw('null');
} elseif (\is_bool($value)) { } elseif (\is_bool($value)) {
$this->raw($value ? 'true' : 'false'); $this->raw($value ? 'true' : 'false');
} elseif (\is_array($value)) { } elseif (\is_array($value)) {
$this->raw('['); $this->raw('array(');
$first = true; $first = true;
foreach ($value as $key => $v) { foreach ($value as $key => $v) {
if (!$first) { if (!$first) {
@ -199,7 +166,7 @@ class Compiler implements \Twig_CompilerInterface
$this->raw(' => '); $this->raw(' => ');
$this->repr($v); $this->repr($v);
} }
$this->raw(']'); $this->raw(')');
} else { } else {
$this->string($value); $this->string($value);
} }
@ -212,22 +179,12 @@ class Compiler implements \Twig_CompilerInterface
* *
* @return $this * @return $this
*/ */
public function addDebugInfo(\Twig_NodeInterface $node) public function addDebugInfo(Node $node)
{ {
if ($node->getTemplateLine() != $this->lastLine) { if ($node->getTemplateLine() != $this->lastLine) {
$this->write(sprintf("// line %d\n", $node->getTemplateLine())); $this->write(sprintf("// line %d\n", $node->getTemplateLine()));
// when mbstring.func_overload is set to 2
// mb_substr_count() replaces substr_count()
// but they have different signatures!
if (((int) ini_get('mbstring.func_overload')) & 2) {
@trigger_error('Support for having "mbstring.func_overload" different from 0 is deprecated version 1.29 and will be removed in 2.0.', E_USER_DEPRECATED);
// this is much slower than the "right" version
$this->sourceLine += mb_substr_count(mb_substr($this->source, $this->sourceOffset), "\n");
} else {
$this->sourceLine += substr_count($this->source, "\n", $this->sourceOffset); $this->sourceLine += substr_count($this->source, "\n", $this->sourceOffset);
}
$this->sourceOffset = \strlen($this->source); $this->sourceOffset = \strlen($this->source);
$this->debugInfo[$this->sourceLine] = $node->getTemplateLine(); $this->debugInfo[$this->sourceLine] = $node->getTemplateLine();
@ -281,7 +238,7 @@ class Compiler implements \Twig_CompilerInterface
public function getVarName() public function getVarName()
{ {
return sprintf('__internal_%s', hash('sha256', __METHOD__.$this->varNameSalt++)); return sprintf('__internal_compile_%d', $this->varNameSalt++);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -38,11 +38,9 @@ use Twig\Template;
*/ */
class Error extends \Exception class Error extends \Exception
{ {
protected $lineno; private $lineno;
// to be renamed to name in 2.0 private $name;
protected $filename; private $rawMessage;
protected $rawMessage;
private $sourcePath; private $sourcePath;
private $sourceCode; private $sourceCode;
@ -57,22 +55,23 @@ class Error extends \Exception
* @param Source|string|null $source The source context where the error occurred * @param Source|string|null $source The source context where the error occurred
* @param \Exception $previous The previous exception * @param \Exception $previous The previous exception
*/ */
public function __construct($message, $lineno = -1, $source = null, \Exception $previous = null) public function __construct(string $message, int $lineno = -1, $source = null, \Exception $previous = null)
{ {
parent::__construct('', 0, $previous);
if (null === $source) { if (null === $source) {
$name = null; $name = null;
} elseif (!$source instanceof Source) { } elseif (!$source instanceof Source && !$source instanceof \Twig_Source) {
// for compat with the Twig C ext., passing the template name as string is accepted @trigger_error(sprintf('Passing a string as a source to %s is deprecated since Twig 2.6.1; pass a Twig\Source instance instead.', __CLASS__), \E_USER_DEPRECATED);
$name = $source; $name = $source;
} else { } else {
$name = $source->getName(); $name = $source->getName();
$this->sourceCode = $source->getCode(); $this->sourceCode = $source->getCode();
$this->sourcePath = $source->getPath(); $this->sourcePath = $source->getPath();
} }
parent::__construct('', 0, $previous);
$this->lineno = $lineno; $this->lineno = $lineno;
$this->filename = $name; $this->name = $name;
$this->rawMessage = $message; $this->rawMessage = $message;
$this->updateRepr(); $this->updateRepr();
} }
@ -87,67 +86,6 @@ class Error extends \Exception
return $this->rawMessage; return $this->rawMessage;
} }
/**
* Gets the logical name where the error occurred.
*
* @return string The name
*
* @deprecated since 1.27 (to be removed in 2.0). Use getSourceContext() instead.
*/
public function getTemplateFile()
{
@trigger_error(sprintf('The "%s" method is deprecated since version 1.27 and will be removed in 2.0. Use getSourceContext() instead.', __METHOD__), E_USER_DEPRECATED);
return $this->filename;
}
/**
* Sets the logical name where the error occurred.
*
* @param string $name The name
*
* @deprecated since 1.27 (to be removed in 2.0). Use setSourceContext() instead.
*/
public function setTemplateFile($name)
{
@trigger_error(sprintf('The "%s" method is deprecated since version 1.27 and will be removed in 2.0. Use setSourceContext() instead.', __METHOD__), E_USER_DEPRECATED);
$this->filename = $name;
$this->updateRepr();
}
/**
* Gets the logical name where the error occurred.
*
* @return string The name
*
* @deprecated since 1.29 (to be removed in 2.0). Use getSourceContext() instead.
*/
public function getTemplateName()
{
@trigger_error(sprintf('The "%s" method is deprecated since version 1.29 and will be removed in 2.0. Use getSourceContext() instead.', __METHOD__), E_USER_DEPRECATED);
return $this->filename;
}
/**
* Sets the logical name where the error occurred.
*
* @param string $name The name
*
* @deprecated since 1.29 (to be removed in 2.0). Use setSourceContext() instead.
*/
public function setTemplateName($name)
{
@trigger_error(sprintf('The "%s" method is deprecated since version 1.29 and will be removed in 2.0. Use setSourceContext() instead.', __METHOD__), E_USER_DEPRECATED);
$this->filename = $name;
$this->sourceCode = $this->sourcePath = null;
$this->updateRepr();
}
/** /**
* Gets the template line where the error occurred. * Gets the template line where the error occurred.
* *
@ -177,7 +115,7 @@ class Error extends \Exception
*/ */
public function getSourceContext() public function getSourceContext()
{ {
return $this->filename ? new Source($this->sourceCode, $this->filename, $this->sourcePath) : null; return $this->name ? new Source($this->sourceCode, $this->name, $this->sourcePath) : null;
} }
/** /**
@ -186,10 +124,10 @@ class Error extends \Exception
public function setSourceContext(Source $source = null) public function setSourceContext(Source $source = null)
{ {
if (null === $source) { if (null === $source) {
$this->sourceCode = $this->filename = $this->sourcePath = null; $this->sourceCode = $this->name = $this->sourcePath = null;
} else { } else {
$this->sourceCode = $source->getCode(); $this->sourceCode = $source->getCode();
$this->filename = $source->getName(); $this->name = $source->getName();
$this->sourcePath = $source->getPath(); $this->sourcePath = $source->getPath();
} }
@ -208,10 +146,7 @@ class Error extends \Exception
$this->updateRepr(); $this->updateRepr();
} }
/** private function updateRepr()
* @internal
*/
protected function updateRepr()
{ {
$this->message = $this->rawMessage; $this->message = $this->rawMessage;
@ -234,11 +169,11 @@ class Error extends \Exception
$questionMark = true; $questionMark = true;
} }
if ($this->filename) { if ($this->name) {
if (\is_string($this->filename) || (\is_object($this->filename) && method_exists($this->filename, '__toString'))) { if (\is_string($this->name) || (\is_object($this->name) && method_exists($this->name, '__toString'))) {
$name = sprintf('"%s"', $this->filename); $name = sprintf('"%s"', $this->name);
} else { } else {
$name = json_encode($this->filename); $name = json_encode($this->name);
} }
$this->message .= sprintf(' in %s', $name); $this->message .= sprintf(' in %s', $name);
} }
@ -256,20 +191,17 @@ class Error extends \Exception
} }
} }
/** private function guessTemplateInfo()
* @internal
*/
protected function guessTemplateInfo()
{ {
$template = null; $template = null;
$templateClass = null; $templateClass = null;
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT); $backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT);
foreach ($backtrace as $trace) { foreach ($backtrace as $trace) {
if (isset($trace['object']) && $trace['object'] instanceof Template && 'Twig_Template' !== \get_class($trace['object'])) { if (isset($trace['object']) && $trace['object'] instanceof Template && 'Twig\Template' !== \get_class($trace['object'])) {
$currentClass = \get_class($trace['object']); $currentClass = \get_class($trace['object']);
$isEmbedContainer = 0 === strpos($templateClass, $currentClass); $isEmbedContainer = null === $templateClass ? false : 0 === strpos($templateClass, $currentClass);
if (null === $this->filename || ($this->filename == $trace['object']->getTemplateName() && !$isEmbedContainer)) { if (null === $this->name || ($this->name == $trace['object']->getTemplateName() && !$isEmbedContainer)) {
$template = $trace['object']; $template = $trace['object'];
$templateClass = \get_class($trace['object']); $templateClass = \get_class($trace['object']);
} }
@ -277,8 +209,8 @@ class Error extends \Exception
} }
// update template name // update template name
if (null !== $template && null === $this->filename) { if (null !== $template && null === $this->name) {
$this->filename = $template->getTemplateName(); $this->name = $template->getTemplateName();
} }
// update template path if any // update template path if any
@ -296,7 +228,7 @@ class Error extends \Exception
$file = $r->getFileName(); $file = $r->getFileName();
$exceptions = [$e = $this]; $exceptions = [$e = $this];
while ($e instanceof self && $e = $e->getPrevious()) { while ($e = $e->getPrevious()) {
$exceptions[] = $e; $exceptions[] = $e;
} }

View File

@ -26,20 +26,6 @@ class SyntaxError extends Error
* @param array $items An array of possible items * @param array $items An array of possible items
*/ */
public function addSuggestions($name, array $items) public function addSuggestions($name, array $items)
{
if (!$alternatives = self::computeAlternatives($name, $items)) {
return;
}
$this->appendMessage(sprintf(' Did you mean "%s"?', implode('", "', $alternatives)));
}
/**
* @internal
*
* To be merged with the addSuggestions() method in 2.0.
*/
public static function computeAlternatives($name, $items)
{ {
$alternatives = []; $alternatives = [];
foreach ($items as $item) { foreach ($items as $item) {
@ -48,9 +34,14 @@ class SyntaxError extends Error
$alternatives[$item] = $lev; $alternatives[$item] = $lev;
} }
} }
if (!$alternatives) {
return;
}
asort($alternatives); asort($alternatives);
return array_keys($alternatives); $this->appendMessage(sprintf(' Did you mean "%s"?', implode('", "', array_keys($alternatives))));
} }
} }

View File

@ -13,6 +13,7 @@
namespace Twig; namespace Twig;
use Twig\Error\SyntaxError; use Twig\Error\SyntaxError;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\ArrowFunctionExpression; use Twig\Node\Expression\ArrowFunctionExpression;
use Twig\Node\Expression\AssignNameExpression; use Twig\Node\Expression\AssignNameExpression;
@ -24,6 +25,7 @@ use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\MethodCallExpression; use Twig\Node\Expression\MethodCallExpression;
use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\NameExpression;
use Twig\Node\Expression\ParentExpression; use Twig\Node\Expression\ParentExpression;
use Twig\Node\Expression\TestExpression;
use Twig\Node\Expression\Unary\NegUnary; use Twig\Node\Expression\Unary\NegUnary;
use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Expression\Unary\NotUnary;
use Twig\Node\Expression\Unary\PosUnary; use Twig\Node\Expression\Unary\PosUnary;
@ -43,30 +45,20 @@ use Twig\Node\Node;
*/ */
class ExpressionParser class ExpressionParser
{ {
const OPERATOR_LEFT = 1; public const OPERATOR_LEFT = 1;
const OPERATOR_RIGHT = 2; public const OPERATOR_RIGHT = 2;
protected $parser;
protected $unaryOperators;
protected $binaryOperators;
private $parser;
private $env; private $env;
private $unaryOperators;
private $binaryOperators;
public function __construct(Parser $parser, $env = null) public function __construct(Parser $parser, Environment $env)
{ {
$this->parser = $parser; $this->parser = $parser;
if ($env instanceof Environment) {
$this->env = $env; $this->env = $env;
$this->unaryOperators = $env->getUnaryOperators(); $this->unaryOperators = $env->getUnaryOperators();
$this->binaryOperators = $env->getBinaryOperators(); $this->binaryOperators = $env->getBinaryOperators();
} else {
@trigger_error('Passing the operators as constructor arguments to '.__METHOD__.' is deprecated since version 1.27. Pass the environment instead.', E_USER_DEPRECATED);
$this->env = $parser->getEnvironment();
$this->unaryOperators = func_get_arg(1);
$this->binaryOperators = func_get_arg(2);
}
} }
public function parseExpression($precedence = 0, $allowArrow = false) public function parseExpression($precedence = 0, $allowArrow = false)
@ -86,7 +78,7 @@ class ExpressionParser
} elseif ('is' === $token->getValue()) { } elseif ('is' === $token->getValue()) {
$expr = $this->parseTestExpression($expr); $expr = $this->parseTestExpression($expr);
} elseif (isset($op['callable'])) { } elseif (isset($op['callable'])) {
$expr = \call_user_func($op['callable'], $this->parser, $expr); $expr = $op['callable']($this->parser, $expr);
} else { } else {
$expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']); $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']);
$class = $op['class']; $class = $op['class'];
@ -111,57 +103,57 @@ class ExpressionParser
$stream = $this->parser->getStream(); $stream = $this->parser->getStream();
// short array syntax (one argument, no parentheses)? // short array syntax (one argument, no parentheses)?
if ($stream->look(1)->test(Token::ARROW_TYPE)) { if ($stream->look(1)->test(/* Token::ARROW_TYPE */ 12)) {
$line = $stream->getCurrent()->getLine(); $line = $stream->getCurrent()->getLine();
$token = $stream->expect(Token::NAME_TYPE); $token = $stream->expect(/* Token::NAME_TYPE */ 5);
$names = [new AssignNameExpression($token->getValue(), $token->getLine())]; $names = [new AssignNameExpression($token->getValue(), $token->getLine())];
$stream->expect(Token::ARROW_TYPE); $stream->expect(/* Token::ARROW_TYPE */ 12);
return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line); return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line);
} }
// first, determine if we are parsing an arrow function by finding => (long form) // first, determine if we are parsing an arrow function by finding => (long form)
$i = 0; $i = 0;
if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, '(')) { if (!$stream->look($i)->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
return null; return null;
} }
++$i; ++$i;
while (true) { while (true) {
// variable name // variable name
++$i; ++$i;
if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ',')) { if (!$stream->look($i)->test(/* Token::PUNCTUATION_TYPE */ 9, ',')) {
break; break;
} }
++$i; ++$i;
} }
if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ')')) { if (!$stream->look($i)->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) {
return null; return null;
} }
++$i; ++$i;
if (!$stream->look($i)->test(Token::ARROW_TYPE)) { if (!$stream->look($i)->test(/* Token::ARROW_TYPE */ 12)) {
return null; return null;
} }
// yes, let's parse it properly // yes, let's parse it properly
$token = $stream->expect(Token::PUNCTUATION_TYPE, '('); $token = $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '(');
$line = $token->getLine(); $line = $token->getLine();
$names = []; $names = [];
while (true) { while (true) {
$token = $stream->expect(Token::NAME_TYPE); $token = $stream->expect(/* Token::NAME_TYPE */ 5);
$names[] = new AssignNameExpression($token->getValue(), $token->getLine()); $names[] = new AssignNameExpression($token->getValue(), $token->getLine());
if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) {
break; break;
} }
} }
$stream->expect(Token::PUNCTUATION_TYPE, ')'); $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ')');
$stream->expect(Token::ARROW_TYPE); $stream->expect(/* Token::ARROW_TYPE */ 12);
return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line); return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line);
} }
protected function getPrimary() private function getPrimary(): AbstractExpression
{ {
$token = $this->parser->getCurrentToken(); $token = $this->parser->getCurrentToken();
@ -172,10 +164,10 @@ class ExpressionParser
$class = $operator['class']; $class = $operator['class'];
return $this->parsePostfixExpression(new $class($expr, $token->getLine())); return $this->parsePostfixExpression(new $class($expr, $token->getLine()));
} elseif ($token->test(Token::PUNCTUATION_TYPE, '(')) { } elseif ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
$this->parser->getStream()->next(); $this->parser->getStream()->next();
$expr = $this->parseExpression(); $expr = $this->parseExpression();
$this->parser->getStream()->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed'); $this->parser->getStream()->expect(/* Token::PUNCTUATION_TYPE */ 9, ')', 'An opened parenthesis is not properly closed');
return $this->parsePostfixExpression($expr); return $this->parsePostfixExpression($expr);
} }
@ -183,12 +175,12 @@ class ExpressionParser
return $this->parsePrimaryExpression(); return $this->parsePrimaryExpression();
} }
protected function parseConditionalExpression($expr) private function parseConditionalExpression($expr): AbstractExpression
{ {
while ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, '?')) { while ($this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, '?')) {
if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) { if (!$this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) {
$expr2 = $this->parseExpression(); $expr2 = $this->parseExpression();
if ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) { if ($this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) {
$expr3 = $this->parseExpression(); $expr3 = $this->parseExpression();
} else { } else {
$expr3 = new ConstantExpression('', $this->parser->getCurrentToken()->getLine()); $expr3 = new ConstantExpression('', $this->parser->getCurrentToken()->getLine());
@ -204,21 +196,21 @@ class ExpressionParser
return $expr; return $expr;
} }
protected function isUnary(Token $token) private function isUnary(Token $token): bool
{ {
return $token->test(Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->getValue()]); return $token->test(/* Token::OPERATOR_TYPE */ 8) && isset($this->unaryOperators[$token->getValue()]);
} }
protected function isBinary(Token $token) private function isBinary(Token $token): bool
{ {
return $token->test(Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->getValue()]); return $token->test(/* Token::OPERATOR_TYPE */ 8) && isset($this->binaryOperators[$token->getValue()]);
} }
public function parsePrimaryExpression() public function parsePrimaryExpression()
{ {
$token = $this->parser->getCurrentToken(); $token = $this->parser->getCurrentToken();
switch ($token->getType()) { switch ($token->getType()) {
case Token::NAME_TYPE: case /* Token::NAME_TYPE */ 5:
$this->parser->getStream()->next(); $this->parser->getStream()->next();
switch ($token->getValue()) { switch ($token->getValue()) {
case 'true': case 'true':
@ -247,17 +239,17 @@ class ExpressionParser
} }
break; break;
case Token::NUMBER_TYPE: case /* Token::NUMBER_TYPE */ 6:
$this->parser->getStream()->next(); $this->parser->getStream()->next();
$node = new ConstantExpression($token->getValue(), $token->getLine()); $node = new ConstantExpression($token->getValue(), $token->getLine());
break; break;
case Token::STRING_TYPE: case /* Token::STRING_TYPE */ 7:
case Token::INTERPOLATION_START_TYPE: case /* Token::INTERPOLATION_START_TYPE */ 10:
$node = $this->parseStringExpression(); $node = $this->parseStringExpression();
break; break;
case Token::OPERATOR_TYPE: case /* Token::OPERATOR_TYPE */ 8:
if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) { if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) {
// in this context, string operators are variable names // in this context, string operators are variable names
$this->parser->getStream()->next(); $this->parser->getStream()->next();
@ -267,10 +259,8 @@ class ExpressionParser
$class = $this->unaryOperators[$token->getValue()]['class']; $class = $this->unaryOperators[$token->getValue()]['class'];
$ref = new \ReflectionClass($class); $ref = new \ReflectionClass($class);
$negClass = 'Twig\Node\Expression\Unary\NegUnary'; if (!(\in_array($ref->getName(), [NegUnary::class, PosUnary::class, 'Twig_Node_Expression_Unary_Neg', 'Twig_Node_Expression_Unary_Pos'])
$posClass = 'Twig\Node\Expression\Unary\PosUnary'; || $ref->isSubclassOf(NegUnary::class) || $ref->isSubclassOf(PosUnary::class)
if (!(\in_array($ref->getName(), [$negClass, $posClass, 'Twig_Node_Expression_Unary_Neg', 'Twig_Node_Expression_Unary_Pos'])
|| $ref->isSubclassOf($negClass) || $ref->isSubclassOf($posClass)
|| $ref->isSubclassOf('Twig_Node_Expression_Unary_Neg') || $ref->isSubclassOf('Twig_Node_Expression_Unary_Pos')) || $ref->isSubclassOf('Twig_Node_Expression_Unary_Neg') || $ref->isSubclassOf('Twig_Node_Expression_Unary_Pos'))
) { ) {
throw new SyntaxError(sprintf('Unexpected unary operator "%s".', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); throw new SyntaxError(sprintf('Unexpected unary operator "%s".', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
@ -285,11 +275,11 @@ class ExpressionParser
// no break // no break
default: default:
if ($token->test(Token::PUNCTUATION_TYPE, '[')) { if ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '[')) {
$node = $this->parseArrayExpression(); $node = $this->parseArrayExpression();
} elseif ($token->test(Token::PUNCTUATION_TYPE, '{')) { } elseif ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '{')) {
$node = $this->parseHashExpression(); $node = $this->parseHashExpression();
} elseif ($token->test(Token::OPERATOR_TYPE, '=') && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) { } elseif ($token->test(/* Token::OPERATOR_TYPE */ 8, '=') && ('==' === $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()); 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());
} else { } else {
throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
@ -307,12 +297,12 @@ class ExpressionParser
// a string cannot be followed by another string in a single expression // a string cannot be followed by another string in a single expression
$nextCanBeString = true; $nextCanBeString = true;
while (true) { while (true) {
if ($nextCanBeString && $token = $stream->nextIf(Token::STRING_TYPE)) { if ($nextCanBeString && $token = $stream->nextIf(/* Token::STRING_TYPE */ 7)) {
$nodes[] = new ConstantExpression($token->getValue(), $token->getLine()); $nodes[] = new ConstantExpression($token->getValue(), $token->getLine());
$nextCanBeString = false; $nextCanBeString = false;
} elseif ($stream->nextIf(Token::INTERPOLATION_START_TYPE)) { } elseif ($stream->nextIf(/* Token::INTERPOLATION_START_TYPE */ 10)) {
$nodes[] = $this->parseExpression(); $nodes[] = $this->parseExpression();
$stream->expect(Token::INTERPOLATION_END_TYPE); $stream->expect(/* Token::INTERPOLATION_END_TYPE */ 11);
$nextCanBeString = true; $nextCanBeString = true;
} else { } else {
break; break;
@ -330,16 +320,16 @@ class ExpressionParser
public function parseArrayExpression() public function parseArrayExpression()
{ {
$stream = $this->parser->getStream(); $stream = $this->parser->getStream();
$stream->expect(Token::PUNCTUATION_TYPE, '[', 'An array element was expected'); $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '[', 'An array element was expected');
$node = new ArrayExpression([], $stream->getCurrent()->getLine()); $node = new ArrayExpression([], $stream->getCurrent()->getLine());
$first = true; $first = true;
while (!$stream->test(Token::PUNCTUATION_TYPE, ']')) { while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) {
if (!$first) { if (!$first) {
$stream->expect(Token::PUNCTUATION_TYPE, ',', 'An array element must be followed by a comma'); $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'An array element must be followed by a comma');
// trailing ,? // trailing ,?
if ($stream->test(Token::PUNCTUATION_TYPE, ']')) { if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) {
break; break;
} }
} }
@ -347,7 +337,7 @@ class ExpressionParser
$node->addElement($this->parseExpression()); $node->addElement($this->parseExpression());
} }
$stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened array is not properly closed'); $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']', 'An opened array is not properly closed');
return $node; return $node;
} }
@ -355,16 +345,16 @@ class ExpressionParser
public function parseHashExpression() public function parseHashExpression()
{ {
$stream = $this->parser->getStream(); $stream = $this->parser->getStream();
$stream->expect(Token::PUNCTUATION_TYPE, '{', 'A hash element was expected'); $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '{', 'A hash element was expected');
$node = new ArrayExpression([], $stream->getCurrent()->getLine()); $node = new ArrayExpression([], $stream->getCurrent()->getLine());
$first = true; $first = true;
while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) { while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, '}')) {
if (!$first) { if (!$first) {
$stream->expect(Token::PUNCTUATION_TYPE, ',', 'A hash value must be followed by a comma'); $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'A hash value must be followed by a comma');
// trailing ,? // trailing ,?
if ($stream->test(Token::PUNCTUATION_TYPE, '}')) { if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '}')) {
break; break;
} }
} }
@ -376,9 +366,18 @@ class ExpressionParser
// * a string -- 'a' // * a string -- 'a'
// * a name, which is equivalent to a string -- a // * a name, which is equivalent to a string -- a
// * an expression, which must be enclosed in parentheses -- (1 + 2) // * an expression, which must be enclosed in parentheses -- (1 + 2)
if (($token = $stream->nextIf(Token::STRING_TYPE)) || ($token = $stream->nextIf(Token::NAME_TYPE)) || $token = $stream->nextIf(Token::NUMBER_TYPE)) { if ($token = $stream->nextIf(/* Token::NAME_TYPE */ 5)) {
$key = new ConstantExpression($token->getValue(), $token->getLine()); $key = new ConstantExpression($token->getValue(), $token->getLine());
} elseif ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
// {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(); $key = $this->parseExpression();
} else { } else {
$current = $stream->getCurrent(); $current = $stream->getCurrent();
@ -386,12 +385,12 @@ class ExpressionParser
throw new SyntaxError(sprintf('A hash 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()); throw new SyntaxError(sprintf('A hash 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());
} }
$stream->expect(Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)'); $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ':', 'A hash key must be followed by a colon (:)');
$value = $this->parseExpression(); $value = $this->parseExpression();
$node->addElement($value, $key); $node->addElement($value, $key);
} }
$stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened hash is not properly closed'); $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '}', 'An opened hash is not properly closed');
return $node; return $node;
} }
@ -400,7 +399,7 @@ class ExpressionParser
{ {
while (true) { while (true) {
$token = $this->parser->getCurrentToken(); $token = $this->parser->getCurrentToken();
if (Token::PUNCTUATION_TYPE == $token->getType()) { if (/* Token::PUNCTUATION_TYPE */ 9 == $token->getType()) {
if ('.' == $token->getValue() || '[' == $token->getValue()) { if ('.' == $token->getValue() || '[' == $token->getValue()) {
$node = $this->parseSubscriptExpression($node); $node = $this->parseSubscriptExpression($node);
} elseif ('|' == $token->getValue()) { } elseif ('|' == $token->getValue()) {
@ -474,22 +473,22 @@ class ExpressionParser
if ('.' == $token->getValue()) { if ('.' == $token->getValue()) {
$token = $stream->next(); $token = $stream->next();
if ( if (
Token::NAME_TYPE == $token->getType() /* Token::NAME_TYPE */ 5 == $token->getType()
|| ||
Token::NUMBER_TYPE == $token->getType() /* Token::NUMBER_TYPE */ 6 == $token->getType()
|| ||
(Token::OPERATOR_TYPE == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue())) (/* Token::OPERATOR_TYPE */ 8 == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue()))
) { ) {
$arg = new ConstantExpression($token->getValue(), $lineno); $arg = new ConstantExpression($token->getValue(), $lineno);
if ($stream->test(Token::PUNCTUATION_TYPE, '(')) { if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
$type = Template::METHOD_CALL; $type = Template::METHOD_CALL;
foreach ($this->parseArguments() as $n) { foreach ($this->parseArguments() as $n) {
$arguments->addElement($n); $arguments->addElement($n);
} }
} }
} else { } else {
throw new SyntaxError('Expected name or number.', $lineno, $stream->getSourceContext()); throw new SyntaxError(sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), Token::typeToEnglish($token->getType())), $lineno, $stream->getSourceContext());
} }
if ($node instanceof NameExpression && null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) { if ($node instanceof NameExpression && null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) {
@ -499,11 +498,7 @@ class ExpressionParser
$name = $arg->getAttribute('value'); $name = $arg->getAttribute('value');
if ($this->parser->isReservedMacroName($name)) { $node = new MethodCallExpression($node, 'macro_'.$name, $arguments, $lineno);
throw new SyntaxError(sprintf('"%s" cannot be called as macro as it is a reserved keyword.', $name), $token->getLine(), $stream->getSourceContext());
}
$node = new MethodCallExpression($node, 'get'.$name, $arguments, $lineno);
$node->setAttribute('safe', true); $node->setAttribute('safe', true);
return $node; return $node;
@ -513,19 +508,19 @@ class ExpressionParser
// slice? // slice?
$slice = false; $slice = false;
if ($stream->test(Token::PUNCTUATION_TYPE, ':')) { if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ':')) {
$slice = true; $slice = true;
$arg = new ConstantExpression(0, $token->getLine()); $arg = new ConstantExpression(0, $token->getLine());
} else { } else {
$arg = $this->parseExpression(); $arg = $this->parseExpression();
} }
if ($stream->nextIf(Token::PUNCTUATION_TYPE, ':')) { if ($stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) {
$slice = true; $slice = true;
} }
if ($slice) { if ($slice) {
if ($stream->test(Token::PUNCTUATION_TYPE, ']')) { if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) {
$length = new ConstantExpression(null, $token->getLine()); $length = new ConstantExpression(null, $token->getLine());
} else { } else {
$length = $this->parseExpression(); $length = $this->parseExpression();
@ -535,12 +530,12 @@ class ExpressionParser
$arguments = new Node([$arg, $length]); $arguments = new Node([$arg, $length]);
$filter = new $class($node, new ConstantExpression('slice', $token->getLine()), $arguments, $token->getLine()); $filter = new $class($node, new ConstantExpression('slice', $token->getLine()), $arguments, $token->getLine());
$stream->expect(Token::PUNCTUATION_TYPE, ']'); $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']');
return $filter; return $filter;
} }
$stream->expect(Token::PUNCTUATION_TYPE, ']'); $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']');
} }
return new GetAttrExpression($node, $arg, $arguments, $type, $lineno); return new GetAttrExpression($node, $arg, $arguments, $type, $lineno);
@ -556,10 +551,10 @@ class ExpressionParser
public function parseFilterExpressionRaw($node, $tag = null) public function parseFilterExpressionRaw($node, $tag = null)
{ {
while (true) { while (true) {
$token = $this->parser->getStream()->expect(Token::NAME_TYPE); $token = $this->parser->getStream()->expect(/* Token::NAME_TYPE */ 5);
$name = new ConstantExpression($token->getValue(), $token->getLine()); $name = new ConstantExpression($token->getValue(), $token->getLine());
if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) { if (!$this->parser->getStream()->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
$arguments = new Node(); $arguments = new Node();
} else { } else {
$arguments = $this->parseArguments(true, false, true); $arguments = $this->parseArguments(true, false, true);
@ -569,7 +564,7 @@ class ExpressionParser
$node = new $class($node, $name, $arguments, $token->getLine(), $tag); $node = new $class($node, $name, $arguments, $token->getLine(), $tag);
if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '|')) { if (!$this->parser->getStream()->test(/* Token::PUNCTUATION_TYPE */ 9, '|')) {
break; break;
} }
@ -594,21 +589,26 @@ class ExpressionParser
$args = []; $args = [];
$stream = $this->parser->getStream(); $stream = $this->parser->getStream();
$stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '(', 'A list of arguments must begin with an opening parenthesis');
while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) {
if (!empty($args)) { if (!empty($args)) {
$stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); $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) { if ($definition) {
$token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name'); $token = $stream->expect(/* Token::NAME_TYPE */ 5, null, 'An argument must be a name');
$value = new NameExpression($token->getValue(), $this->parser->getCurrentToken()->getLine()); $value = new NameExpression($token->getValue(), $this->parser->getCurrentToken()->getLine());
} else { } else {
$value = $this->parseExpression(0, $allowArrow); $value = $this->parseExpression(0, $allowArrow);
} }
$name = null; $name = null;
if ($namedArguments && $token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) { if ($namedArguments && $token = $stream->nextIf(/* Token::OPERATOR_TYPE */ 8, '=')) {
if (!$value instanceof NameExpression) { if (!$value instanceof NameExpression) {
throw new SyntaxError(sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext()); throw new SyntaxError(sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext());
} }
@ -618,7 +618,7 @@ class ExpressionParser
$value = $this->parsePrimaryExpression(); $value = $this->parsePrimaryExpression();
if (!$this->checkConstantExpression($value)) { if (!$this->checkConstantExpression($value)) {
throw new SyntaxError(sprintf('A default value for an argument must be a constant (a boolean, a string, a number, or an array).'), $token->getLine(), $stream->getSourceContext()); throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, or an array).', $token->getLine(), $stream->getSourceContext());
} }
} else { } else {
$value = $this->parseExpression(0, $allowArrow); $value = $this->parseExpression(0, $allowArrow);
@ -639,7 +639,7 @@ class ExpressionParser
} }
} }
} }
$stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ')', 'A list of arguments must be closed by a parenthesis');
return new Node($args); return new Node($args);
} }
@ -650,19 +650,19 @@ class ExpressionParser
$targets = []; $targets = [];
while (true) { while (true) {
$token = $this->parser->getCurrentToken(); $token = $this->parser->getCurrentToken();
if ($stream->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) { if ($stream->test(/* Token::OPERATOR_TYPE */ 8) && preg_match(Lexer::REGEX_NAME, $token->getValue())) {
// in this context, string operators are variable names // in this context, string operators are variable names
$this->parser->getStream()->next(); $this->parser->getStream()->next();
} else { } else {
$stream->expect(Token::NAME_TYPE, null, 'Only variables can be assigned to'); $stream->expect(/* Token::NAME_TYPE */ 5, null, 'Only variables can be assigned to');
} }
$value = $token->getValue(); $value = $token->getValue();
if (\in_array(strtolower($value), ['true', 'false', 'none', 'null'])) { if (\in_array(strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), ['true', 'false', 'none', 'null'])) {
throw new SyntaxError(sprintf('You cannot assign a value to "%s".', $value), $token->getLine(), $stream->getSourceContext()); throw new SyntaxError(sprintf('You cannot assign a value to "%s".', $value), $token->getLine(), $stream->getSourceContext());
} }
$targets[] = new AssignNameExpression($value, $token->getLine()); $targets[] = new AssignNameExpression($value, $token->getLine());
if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) {
break; break;
} }
} }
@ -675,7 +675,7 @@ class ExpressionParser
$targets = []; $targets = [];
while (true) { while (true) {
$targets[] = $this->parseExpression(); $targets[] = $this->parseExpression();
if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ',')) { if (!$this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) {
break; break;
} }
} }
@ -683,35 +683,42 @@ class ExpressionParser
return new Node($targets); return new Node($targets);
} }
private function parseNotTestExpression(\Twig_NodeInterface $node) private function parseNotTestExpression(Node $node): NotUnary
{ {
return new NotUnary($this->parseTestExpression($node), $this->parser->getCurrentToken()->getLine()); return new NotUnary($this->parseTestExpression($node), $this->parser->getCurrentToken()->getLine());
} }
private function parseTestExpression(\Twig_NodeInterface $node) private function parseTestExpression(Node $node): TestExpression
{ {
$stream = $this->parser->getStream(); $stream = $this->parser->getStream();
list($name, $test) = $this->getTest($node->getTemplateLine()); list($name, $test) = $this->getTest($node->getTemplateLine());
$class = $this->getTestNodeClass($test); $class = $this->getTestNodeClass($test);
$arguments = null; $arguments = null;
if ($stream->test(Token::PUNCTUATION_TYPE, '(')) { if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
$arguments = $this->parseArguments(true); $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()); return new $class($node, $name, $arguments, $this->parser->getCurrentToken()->getLine());
} }
private function getTest($line) private function getTest(int $line): array
{ {
$stream = $this->parser->getStream(); $stream = $this->parser->getStream();
$name = $stream->expect(Token::NAME_TYPE)->getValue(); $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue();
if ($test = $this->env->getTest($name)) { if ($test = $this->env->getTest($name)) {
return [$name, $test]; return [$name, $test];
} }
if ($stream->test(Token::NAME_TYPE)) { if ($stream->test(/* Token::NAME_TYPE */ 5)) {
// try 2-words tests // try 2-words tests
$name = $name.' '.$this->parser->getCurrentToken()->getValue(); $name = $name.' '.$this->parser->getCurrentToken()->getValue();
@ -728,11 +735,12 @@ class ExpressionParser
throw $e; throw $e;
} }
private function getTestNodeClass($test) private function getTestNodeClass(TwigTest $test): string
{ {
if ($test instanceof TwigTest && $test->isDeprecated()) { if ($test->isDeprecated()) {
$stream = $this->parser->getStream(); $stream = $this->parser->getStream();
$message = sprintf('Twig Test "%s" is deprecated', $test->getName()); $message = sprintf('Twig Test "%s" is deprecated', $test->getName());
if (!\is_bool($test->getDeprecatedVersion())) { if (!\is_bool($test->getDeprecatedVersion())) {
$message .= sprintf(' since version %s', $test->getDeprecatedVersion()); $message .= sprintf(' since version %s', $test->getDeprecatedVersion());
} }
@ -740,19 +748,15 @@ class ExpressionParser
$message .= sprintf('. Use "%s" instead', $test->getAlternative()); $message .= sprintf('. Use "%s" instead', $test->getAlternative());
} }
$src = $stream->getSourceContext(); $src = $stream->getSourceContext();
$message .= sprintf(' in %s at line %d.', $src->getPath() ? $src->getPath() : $src->getName(), $stream->getCurrent()->getLine()); $message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine());
@trigger_error($message, E_USER_DEPRECATED); @trigger_error($message, \E_USER_DEPRECATED);
} }
if ($test instanceof TwigTest) {
return $test->getNodeClass(); return $test->getNodeClass();
} }
return $test instanceof \Twig_Test_Node ? $test->getClass() : 'Twig\Node\Expression\TestExpression'; private function getFunctionNodeClass(string $name, int $line): string
}
protected function getFunctionNodeClass($name, $line)
{ {
if (false === $function = $this->env->getFunction($name)) { if (false === $function = $this->env->getFunction($name)) {
$e = new SyntaxError(sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext()); $e = new SyntaxError(sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext());
@ -761,7 +765,7 @@ class ExpressionParser
throw $e; throw $e;
} }
if ($function instanceof TwigFunction && $function->isDeprecated()) { if ($function->isDeprecated()) {
$message = sprintf('Twig Function "%s" is deprecated', $function->getName()); $message = sprintf('Twig Function "%s" is deprecated', $function->getName());
if (!\is_bool($function->getDeprecatedVersion())) { if (!\is_bool($function->getDeprecatedVersion())) {
$message .= sprintf(' since version %s', $function->getDeprecatedVersion()); $message .= sprintf(' since version %s', $function->getDeprecatedVersion());
@ -770,19 +774,15 @@ class ExpressionParser
$message .= sprintf('. Use "%s" instead', $function->getAlternative()); $message .= sprintf('. Use "%s" instead', $function->getAlternative());
} }
$src = $this->parser->getStream()->getSourceContext(); $src = $this->parser->getStream()->getSourceContext();
$message .= sprintf(' in %s at line %d.', $src->getPath() ? $src->getPath() : $src->getName(), $line); $message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line);
@trigger_error($message, E_USER_DEPRECATED); @trigger_error($message, \E_USER_DEPRECATED);
} }
if ($function instanceof TwigFunction) {
return $function->getNodeClass(); return $function->getNodeClass();
} }
return $function instanceof \Twig_Function_Node ? $function->getClass() : 'Twig\Node\Expression\FunctionExpression'; private function getFilterNodeClass(string $name, int $line): string
}
protected function getFilterNodeClass($name, $line)
{ {
if (false === $filter = $this->env->getFilter($name)) { if (false === $filter = $this->env->getFilter($name)) {
$e = new SyntaxError(sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext()); $e = new SyntaxError(sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext());
@ -791,7 +791,7 @@ class ExpressionParser
throw $e; throw $e;
} }
if ($filter instanceof TwigFilter && $filter->isDeprecated()) { if ($filter->isDeprecated()) {
$message = sprintf('Twig Filter "%s" is deprecated', $filter->getName()); $message = sprintf('Twig Filter "%s" is deprecated', $filter->getName());
if (!\is_bool($filter->getDeprecatedVersion())) { if (!\is_bool($filter->getDeprecatedVersion())) {
$message .= sprintf(' since version %s', $filter->getDeprecatedVersion()); $message .= sprintf(' since version %s', $filter->getDeprecatedVersion());
@ -800,20 +800,16 @@ class ExpressionParser
$message .= sprintf('. Use "%s" instead', $filter->getAlternative()); $message .= sprintf('. Use "%s" instead', $filter->getAlternative());
} }
$src = $this->parser->getStream()->getSourceContext(); $src = $this->parser->getStream()->getSourceContext();
$message .= sprintf(' in %s at line %d.', $src->getPath() ? $src->getPath() : $src->getName(), $line); $message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line);
@trigger_error($message, E_USER_DEPRECATED); @trigger_error($message, \E_USER_DEPRECATED);
} }
if ($filter instanceof TwigFilter) {
return $filter->getNodeClass(); return $filter->getNodeClass();
} }
return $filter instanceof \Twig_Filter_Node ? $filter->getClass() : 'Twig\Node\Expression\FilterExpression';
}
// checks that the node only contains "constant" elements // checks that the node only contains "constant" elements
protected function checkConstantExpression(\Twig_NodeInterface $node) private function checkConstantExpression(Node $node): bool
{ {
if (!($node instanceof ConstantExpression || $node instanceof ArrayExpression if (!($node instanceof ConstantExpression || $node instanceof ArrayExpression
|| $node instanceof NegUnary || $node instanceof PosUnary || $node instanceof NegUnary || $node instanceof PosUnary

View File

@ -11,17 +11,8 @@
namespace Twig\Extension; namespace Twig\Extension;
use Twig\Environment;
abstract class AbstractExtension implements ExtensionInterface abstract class AbstractExtension implements ExtensionInterface
{ {
/**
* @deprecated since 1.23 (to be removed in 2.0), implement \Twig_Extension_InitRuntimeInterface instead
*/
public function initRuntime(Environment $environment)
{
}
public function getTokenParsers() public function getTokenParsers()
{ {
return []; return [];
@ -51,22 +42,6 @@ abstract class AbstractExtension implements ExtensionInterface
{ {
return []; return [];
} }
/**
* @deprecated since 1.23 (to be removed in 2.0), implement \Twig_Extension_GlobalsInterface instead
*/
public function getGlobals()
{
return [];
}
/**
* @deprecated since 1.26 (to be removed in 2.0), not used anymore internally
*/
public function getName()
{
return \get_class($this);
}
} }
class_alias('Twig\Extension\AbstractExtension', 'Twig_Extension'); class_alias('Twig\Extension\AbstractExtension', 'Twig_Extension');

File diff suppressed because it is too large Load Diff

View File

@ -12,10 +12,7 @@
namespace Twig\Extension { namespace Twig\Extension {
use Twig\TwigFunction; use Twig\TwigFunction;
/** final class DebugExtension extends AbstractExtension
* @final
*/
class DebugExtension extends AbstractExtension
{ {
public function getFunctions() public function getFunctions()
{ {
@ -33,11 +30,6 @@ class DebugExtension extends AbstractExtension
new TwigFunction('dump', 'twig_var_dump', ['is_safe' => $isDumpOutputHtmlSafe ? ['html'] : [], 'needs_context' => true, 'needs_environment' => true, 'is_variadic' => true]), new TwigFunction('dump', 'twig_var_dump', ['is_safe' => $isDumpOutputHtmlSafe ? ['html'] : [], 'needs_context' => true, 'needs_environment' => true, 'is_variadic' => true]),
]; ];
} }
public function getName()
{
return 'debug';
}
} }
class_alias('Twig\Extension\DebugExtension', 'Twig_Extension_Debug'); class_alias('Twig\Extension\DebugExtension', 'Twig_Extension_Debug');
@ -48,7 +40,7 @@ use Twig\Environment;
use Twig\Template; use Twig\Template;
use Twig\TemplateWrapper; use Twig\TemplateWrapper;
function twig_var_dump(Environment $env, $context, array $vars = []) function twig_var_dump(Environment $env, $context, ...$vars)
{ {
if (!$env->isDebug()) { if (!$env->isDebug()) {
return; return;
@ -66,9 +58,7 @@ function twig_var_dump(Environment $env, $context, array $vars = [])
var_dump($vars); var_dump($vars);
} else { } else {
foreach ($vars as $var) { var_dump(...$vars);
var_dump($var);
}
} }
return ob_get_clean(); return ob_get_clean();

View File

@ -10,16 +10,21 @@
*/ */
namespace Twig\Extension { namespace Twig\Extension {
use Twig\FileExtensionEscapingStrategy;
use Twig\NodeVisitor\EscaperNodeVisitor; use Twig\NodeVisitor\EscaperNodeVisitor;
use Twig\TokenParser\AutoEscapeTokenParser; use Twig\TokenParser\AutoEscapeTokenParser;
use Twig\TwigFilter; use Twig\TwigFilter;
/** final class EscaperExtension extends AbstractExtension
* @final
*/
class EscaperExtension extends AbstractExtension
{ {
protected $defaultStrategy; private $defaultStrategy;
private $escapers = [];
/** @internal */
public $safeClasses = [];
/** @internal */
public $safeLookup = [];
/** /**
* @param string|false|callable $defaultStrategy An escaping strategy * @param string|false|callable $defaultStrategy An escaping strategy
@ -44,6 +49,8 @@ class EscaperExtension extends AbstractExtension
public function getFilters() public function getFilters()
{ {
return [ return [
new TwigFilter('escape', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']),
new TwigFilter('e', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']),
new TwigFilter('raw', 'twig_raw_filter', ['is_safe' => ['all']]), new TwigFilter('raw', 'twig_raw_filter', ['is_safe' => ['all']]),
]; ];
} }
@ -58,21 +65,8 @@ class EscaperExtension extends AbstractExtension
*/ */
public function setDefaultStrategy($defaultStrategy) public function setDefaultStrategy($defaultStrategy)
{ {
// for BC
if (true === $defaultStrategy) {
@trigger_error('Using "true" as the default strategy is deprecated since version 1.21. Use "html" instead.', E_USER_DEPRECATED);
$defaultStrategy = 'html';
}
if ('filename' === $defaultStrategy) {
@trigger_error('Using "filename" as the default strategy is deprecated since version 1.27. Use "name" instead.', E_USER_DEPRECATED);
$defaultStrategy = 'name';
}
if ('name' === $defaultStrategy) { if ('name' === $defaultStrategy) {
$defaultStrategy = ['\Twig\FileExtensionEscapingStrategy', 'guess']; $defaultStrategy = [FileExtensionEscapingStrategy::class, 'guess'];
} }
$this->defaultStrategy = $defaultStrategy; $this->defaultStrategy = $defaultStrategy;
@ -96,9 +90,47 @@ class EscaperExtension extends AbstractExtension
return $this->defaultStrategy; return $this->defaultStrategy;
} }
public function getName() /**
* Defines a new escaper to be used via the escape filter.
*
* @param string $strategy The strategy name that should be used as a strategy in the escape call
* @param callable $callable A valid PHP callable
*/
public function setEscaper($strategy, callable $callable)
{ {
return 'escaper'; $this->escapers[$strategy] = $callable;
}
/**
* Gets all defined escapers.
*
* @return callable[] An array of escapers
*/
public function getEscapers()
{
return $this->escapers;
}
public function setSafeClasses(array $safeClasses = [])
{
$this->safeClasses = [];
$this->safeLookup = [];
foreach ($safeClasses as $class => $strategies) {
$this->addSafeClass($class, $strategies);
}
}
public function addSafeClass(string $class, array $strategies)
{
$class = ltrim($class, '\\');
if (!isset($this->safeClasses[$class])) {
$this->safeClasses[$class] = [];
}
$this->safeClasses[$class] = array_merge($this->safeClasses[$class], $strategies);
foreach ($strategies as $strategy) {
$this->safeLookup[$strategy][$class] = true;
}
} }
} }
@ -106,6 +138,14 @@ class_alias('Twig\Extension\EscaperExtension', 'Twig_Extension_Escaper');
} }
namespace { namespace {
use Twig\Environment;
use Twig\Error\RuntimeError;
use Twig\Extension\CoreExtension;
use Twig\Extension\EscaperExtension;
use Twig\Markup;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Node;
/** /**
* Marks a variable as being safe. * Marks a variable as being safe.
* *
@ -117,4 +157,272 @@ function twig_raw_filter($string)
{ {
return $string; return $string;
} }
/**
* Escapes a string.
*
* @param mixed $string The value to be escaped
* @param string $strategy The escaping strategy
* @param string $charset The charset
* @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false)
*
* @return string
*/
function twig_escape_filter(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false)
{
if ($autoescape && $string instanceof Markup) {
return $string;
}
if (!\is_string($string)) {
if (\is_object($string) && method_exists($string, '__toString')) {
if ($autoescape) {
$c = \get_class($string);
$ext = $env->getExtension(EscaperExtension::class);
if (!isset($ext->safeClasses[$c])) {
$ext->safeClasses[$c] = [];
foreach (class_parents($string) + class_implements($string) as $class) {
if (isset($ext->safeClasses[$class])) {
$ext->safeClasses[$c] = array_unique(array_merge($ext->safeClasses[$c], $ext->safeClasses[$class]));
foreach ($ext->safeClasses[$class] as $s) {
$ext->safeLookup[$s][$c] = true;
}
}
}
}
if (isset($ext->safeLookup[$strategy][$c]) || isset($ext->safeLookup['all'][$c])) {
return (string) $string;
}
}
$string = (string) $string;
} elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) {
return $string;
}
}
if ('' === $string) {
return '';
}
if (null === $charset) {
$charset = $env->getCharset();
}
switch ($strategy) {
case 'html':
// see https://www.php.net/htmlspecialchars
// Using a static variable to avoid initializing the array
// each time the function is called. Moving the declaration on the
// top of the function slow downs other escaping strategies.
static $htmlspecialcharsCharsets = [
'ISO-8859-1' => true, 'ISO8859-1' => true,
'ISO-8859-15' => true, 'ISO8859-15' => true,
'utf-8' => true, 'UTF-8' => true,
'CP866' => true, 'IBM866' => true, '866' => true,
'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true,
'1251' => true,
'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true,
'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true,
'BIG5' => true, '950' => true,
'GB2312' => true, '936' => true,
'BIG5-HKSCS' => true,
'SHIFT_JIS' => true, 'SJIS' => true, '932' => true,
'EUC-JP' => true, 'EUCJP' => true,
'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true,
];
if (isset($htmlspecialcharsCharsets[$charset])) {
return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset);
}
if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) {
// cache the lowercase variant for future iterations
$htmlspecialcharsCharsets[$charset] = true;
return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset);
}
$string = twig_convert_encoding($string, 'UTF-8', $charset);
$string = htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');
return iconv('UTF-8', $charset, $string);
case 'js':
// escape all non-alphanumeric characters
// into their \x or \uHHHH representations
if ('UTF-8' !== $charset) {
$string = twig_convert_encoding($string, 'UTF-8', $charset);
}
if (!preg_match('//u', $string)) {
throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
}
$string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) {
$char = $matches[0];
/*
* A few characters have short escape sequences in JSON and JavaScript.
* Escape sequences supported only by JavaScript, not JSON, are omitted.
* \" is also supported but omitted, because the resulting string is not HTML safe.
*/
static $shortMap = [
'\\' => '\\\\',
'/' => '\\/',
"\x08" => '\b',
"\x0C" => '\f',
"\x0A" => '\n',
"\x0D" => '\r',
"\x09" => '\t',
];
if (isset($shortMap[$char])) {
return $shortMap[$char];
}
$codepoint = mb_ord($char, 'UTF-8');
if (0x10000 > $codepoint) {
return sprintf('\u%04X', $codepoint);
}
// Split characters outside the BMP into surrogate pairs
// https://tools.ietf.org/html/rfc2781.html#section-2.1
$u = $codepoint - 0x10000;
$high = 0xD800 | ($u >> 10);
$low = 0xDC00 | ($u & 0x3FF);
return sprintf('\u%04X\u%04X', $high, $low);
}, $string);
if ('UTF-8' !== $charset) {
$string = iconv('UTF-8', $charset, $string);
}
return $string;
case 'css':
if ('UTF-8' !== $charset) {
$string = twig_convert_encoding($string, 'UTF-8', $charset);
}
if (!preg_match('//u', $string)) {
throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
}
$string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) {
$char = $matches[0];
return sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8'));
}, $string);
if ('UTF-8' !== $charset) {
$string = iconv('UTF-8', $charset, $string);
}
return $string;
case 'html_attr':
if ('UTF-8' !== $charset) {
$string = twig_convert_encoding($string, 'UTF-8', $charset);
}
if (!preg_match('//u', $string)) {
throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
}
$string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) {
/**
* This function is adapted from code coming from Zend Framework.
*
* @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://framework.zend.com/license/new-bsd New BSD License
*/
$chr = $matches[0];
$ord = \ord($chr);
/*
* The following replaces characters undefined in HTML with the
* hex entity for the Unicode replacement character.
*/
if (($ord <= 0x1f && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7f && $ord <= 0x9f)) {
return '&#xFFFD;';
}
/*
* Check if the current character to escape has a name entity we should
* replace it with while grabbing the hex value of the character.
*/
if (1 === \strlen($chr)) {
/*
* While HTML supports far more named entities, the lowest common denominator
* has become HTML5's XML Serialisation which is restricted to the those named
* entities that XML supports. Using HTML entities would result in this error:
* XML Parsing Error: undefined entity
*/
static $entityMap = [
34 => '&quot;', /* quotation mark */
38 => '&amp;', /* ampersand */
60 => '&lt;', /* less-than sign */
62 => '&gt;', /* greater-than sign */
];
if (isset($entityMap[$ord])) {
return $entityMap[$ord];
}
return sprintf('&#x%02X;', $ord);
}
/*
* Per OWASP recommendations, we'll use hex entities for any other
* characters where a named entity does not exist.
*/
return sprintf('&#x%04X;', mb_ord($chr, 'UTF-8'));
}, $string);
if ('UTF-8' !== $charset) {
$string = iconv('UTF-8', $charset, $string);
}
return $string;
case 'url':
return rawurlencode($string);
default:
// check the ones set on CoreExtension for BC (to be removed in 3.0)
$legacyEscapers = $env->getExtension(CoreExtension::class)->getEscapers(false);
if (array_key_exists($strategy, $legacyEscapers)) {
return $legacyEscapers[$strategy]($env, $string, $charset);
}
$escapers = $env->getExtension(EscaperExtension::class)->getEscapers();
if (array_key_exists($strategy, $escapers)) {
return $escapers[$strategy]($env, $string, $charset);
}
$escapers = array_merge($legacyEscapers, $escapers);
$validStrategies = implode(', ', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers)));
throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies));
}
}
/**
* @internal
*/
function twig_escape_filter_is_safe(Node $filterArgs)
{
foreach ($filterArgs as $arg) {
if ($arg instanceof ConstantExpression) {
return [$arg->getAttribute('value')];
}
return [];
}
return ['html'];
}
} }

View File

@ -11,7 +11,6 @@
namespace Twig\Extension; namespace Twig\Extension;
use Twig\Environment;
use Twig\NodeVisitor\NodeVisitorInterface; use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\TokenParser\TokenParserInterface; use Twig\TokenParser\TokenParserInterface;
use Twig\TwigFilter; use Twig\TwigFilter;
@ -25,15 +24,6 @@ use Twig\TwigTest;
*/ */
interface ExtensionInterface interface ExtensionInterface
{ {
/**
* Initializes the runtime environment.
*
* This is where you can load some file that contains filter functions for instance.
*
* @deprecated since 1.23 (to be removed in 2.0), implement \Twig_Extension_InitRuntimeInterface instead
*/
public function initRuntime(Environment $environment);
/** /**
* Returns the token parser instances to add to the existing list. * Returns the token parser instances to add to the existing list.
* *
@ -75,24 +65,6 @@ interface ExtensionInterface
* @return array<array> First array of unary operators, second array of binary operators * @return array<array> First array of unary operators, second array of binary operators
*/ */
public function getOperators(); public function getOperators();
/**
* Returns a list of global variables to add to the existing list.
*
* @return array An array of global variables
*
* @deprecated since 1.23 (to be removed in 2.0), implement \Twig_Extension_GlobalsInterface instead
*/
public function getGlobals();
/**
* Returns the name of the extension.
*
* @return string The extension name
*
* @deprecated since 1.26 (to be removed in 2.0), not used anymore internally
*/
public function getName();
} }
class_alias('Twig\Extension\ExtensionInterface', 'Twig_ExtensionInterface'); class_alias('Twig\Extension\ExtensionInterface', 'Twig_ExtensionInterface');

View File

@ -21,6 +21,12 @@ namespace Twig\Extension;
*/ */
interface GlobalsInterface interface GlobalsInterface
{ {
/**
* Returns a list of global variables to add to the existing list.
*
* @return array An array of global variables
*/
public function getGlobals();
} }
class_alias('Twig\Extension\GlobalsInterface', 'Twig_Extension_GlobalsInterface'); class_alias('Twig\Extension\GlobalsInterface', 'Twig_Extension_GlobalsInterface');

View File

@ -11,6 +11,8 @@
namespace Twig\Extension; namespace Twig\Extension;
use Twig\Environment;
/** /**
* Enables usage of the deprecated Twig\Extension\AbstractExtension::initRuntime() method. * Enables usage of the deprecated Twig\Extension\AbstractExtension::initRuntime() method.
* *
@ -18,9 +20,17 @@ namespace Twig\Extension;
* deprecated initRuntime() method in your extensions. * deprecated initRuntime() method in your extensions.
* *
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*
* @deprecated since Twig 2.7, to be removed in 3.0
*/ */
interface InitRuntimeInterface interface InitRuntimeInterface
{ {
/**
* Initializes the runtime environment.
*
* This is where you can load some file that contains filter functions for instance.
*/
public function initRuntime(Environment $environment);
} }
class_alias('Twig\Extension\InitRuntimeInterface', 'Twig_Extension_InitRuntimeInterface'); class_alias('Twig\Extension\InitRuntimeInterface', 'Twig_Extension_InitRuntimeInterface');

View File

@ -13,12 +13,9 @@ namespace Twig\Extension;
use Twig\NodeVisitor\OptimizerNodeVisitor; use Twig\NodeVisitor\OptimizerNodeVisitor;
/** final class OptimizerExtension extends AbstractExtension
* @final
*/
class OptimizerExtension extends AbstractExtension
{ {
protected $optimizers; private $optimizers;
public function __construct($optimizers = -1) public function __construct($optimizers = -1)
{ {
@ -29,11 +26,6 @@ class OptimizerExtension extends AbstractExtension
{ {
return [new OptimizerNodeVisitor($this->optimizers)]; return [new OptimizerNodeVisitor($this->optimizers)];
} }
public function getName()
{
return 'optimizer';
}
} }
class_alias('Twig\Extension\OptimizerExtension', 'Twig_Extension_Optimizer'); class_alias('Twig\Extension\OptimizerExtension', 'Twig_Extension_Optimizer');

View File

@ -41,12 +41,7 @@ class ProfilerExtension extends AbstractExtension
public function getNodeVisitors() public function getNodeVisitors()
{ {
return [new ProfilerNodeVisitor(\get_class($this))]; return [new ProfilerNodeVisitor(static::class)];
}
public function getName()
{
return 'profiler';
} }
} }

View File

@ -12,17 +12,17 @@
namespace Twig\Extension; namespace Twig\Extension;
use Twig\NodeVisitor\SandboxNodeVisitor; use Twig\NodeVisitor\SandboxNodeVisitor;
use Twig\Sandbox\SecurityNotAllowedMethodError;
use Twig\Sandbox\SecurityNotAllowedPropertyError;
use Twig\Sandbox\SecurityPolicyInterface; use Twig\Sandbox\SecurityPolicyInterface;
use Twig\Source;
use Twig\TokenParser\SandboxTokenParser; use Twig\TokenParser\SandboxTokenParser;
/** final class SandboxExtension extends AbstractExtension
* @final
*/
class SandboxExtension extends AbstractExtension
{ {
protected $sandboxedGlobally; private $sandboxedGlobally;
protected $sandboxed; private $sandboxed;
protected $policy; private $policy;
public function __construct(SecurityPolicyInterface $policy, $sandboxed = false) public function __construct(SecurityPolicyInterface $policy, $sandboxed = false)
{ {
@ -77,33 +77,49 @@ class SandboxExtension extends AbstractExtension
} }
} }
public function checkMethodAllowed($obj, $method) public function checkMethodAllowed($obj, $method, int $lineno = -1, Source $source = null)
{ {
if ($this->isSandboxed()) { if ($this->isSandboxed()) {
try {
$this->policy->checkMethodAllowed($obj, $method); $this->policy->checkMethodAllowed($obj, $method);
} catch (SecurityNotAllowedMethodError $e) {
$e->setSourceContext($source);
$e->setTemplateLine($lineno);
throw $e;
}
} }
} }
public function checkPropertyAllowed($obj, $method) public function checkPropertyAllowed($obj, $property, int $lineno = -1, Source $source = null)
{ {
if ($this->isSandboxed()) { if ($this->isSandboxed()) {
$this->policy->checkPropertyAllowed($obj, $method); try {
$this->policy->checkPropertyAllowed($obj, $property);
} catch (SecurityNotAllowedPropertyError $e) {
$e->setSourceContext($source);
$e->setTemplateLine($lineno);
throw $e;
}
} }
} }
public function ensureToStringAllowed($obj) public function ensureToStringAllowed($obj, int $lineno = -1, Source $source = null)
{ {
if ($this->isSandboxed() && \is_object($obj) && method_exists($obj, '__toString')) { if ($this->isSandboxed() && \is_object($obj) && method_exists($obj, '__toString')) {
try {
$this->policy->checkMethodAllowed($obj, '__toString'); $this->policy->checkMethodAllowed($obj, '__toString');
} catch (SecurityNotAllowedMethodError $e) {
$e->setSourceContext($source);
$e->setTemplateLine($lineno);
throw $e;
}
} }
return $obj; return $obj;
} }
public function getName()
{
return 'sandbox';
}
} }
class_alias('Twig\Extension\SandboxExtension', 'Twig_Extension_Sandbox'); class_alias('Twig\Extension\SandboxExtension', 'Twig_Extension_Sandbox');

View File

@ -13,32 +13,32 @@ namespace Twig\Extension;
use Twig\NodeVisitor\NodeVisitorInterface; use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\TokenParser\TokenParserInterface; use Twig\TokenParser\TokenParserInterface;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\TwigTest;
/** /**
* Internal class. * Used by \Twig\Environment as a staging area.
*
* This class is used by \Twig\Environment as a staging area and must not be used directly.
* *
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
* *
* @internal * @internal
*/ */
class StagingExtension extends AbstractExtension final class StagingExtension extends AbstractExtension
{ {
protected $functions = []; private $functions = [];
protected $filters = []; private $filters = [];
protected $visitors = []; private $visitors = [];
protected $tokenParsers = []; private $tokenParsers = [];
protected $globals = []; private $tests = [];
protected $tests = [];
public function addFunction($name, $function) public function addFunction(TwigFunction $function)
{ {
if (isset($this->functions[$name])) { if (isset($this->functions[$function->getName()])) {
@trigger_error(sprintf('Overriding function "%s" that is already registered is deprecated since version 1.30 and won\'t be possible anymore in 2.0.', $name), E_USER_DEPRECATED); throw new \LogicException(sprintf('Function "%s" is already registered.', $function->getName()));
} }
$this->functions[$name] = $function; $this->functions[$function->getName()] = $function;
} }
public function getFunctions() public function getFunctions()
@ -46,13 +46,13 @@ class StagingExtension extends AbstractExtension
return $this->functions; return $this->functions;
} }
public function addFilter($name, $filter) public function addFilter(TwigFilter $filter)
{ {
if (isset($this->filters[$name])) { if (isset($this->filters[$filter->getName()])) {
@trigger_error(sprintf('Overriding filter "%s" that is already registered is deprecated since version 1.30 and won\'t be possible anymore in 2.0.', $name), E_USER_DEPRECATED); throw new \LogicException(sprintf('Filter "%s" is already registered.', $filter->getName()));
} }
$this->filters[$name] = $filter; $this->filters[$filter->getName()] = $filter;
} }
public function getFilters() public function getFilters()
@ -73,7 +73,7 @@ class StagingExtension extends AbstractExtension
public function addTokenParser(TokenParserInterface $parser) public function addTokenParser(TokenParserInterface $parser)
{ {
if (isset($this->tokenParsers[$parser->getTag()])) { if (isset($this->tokenParsers[$parser->getTag()])) {
@trigger_error(sprintf('Overriding tag "%s" that is already registered is deprecated since version 1.30 and won\'t be possible anymore in 2.0.', $parser->getTag()), E_USER_DEPRECATED); throw new \LogicException(sprintf('Tag "%s" is already registered.', $parser->getTag()));
} }
$this->tokenParsers[$parser->getTag()] = $parser; $this->tokenParsers[$parser->getTag()] = $parser;
@ -84,34 +84,19 @@ class StagingExtension extends AbstractExtension
return $this->tokenParsers; return $this->tokenParsers;
} }
public function addGlobal($name, $value) public function addTest(TwigTest $test)
{ {
$this->globals[$name] = $value; if (isset($this->tests[$test->getName()])) {
throw new \LogicException(sprintf('Test "%s" is already registered.', $test->getName()));
} }
public function getGlobals() $this->tests[$test->getName()] = $test;
{
return $this->globals;
}
public function addTest($name, $test)
{
if (isset($this->tests[$name])) {
@trigger_error(sprintf('Overriding test "%s" that is already registered is deprecated since version 1.30 and won\'t be possible anymore in 2.0.', $name), E_USER_DEPRECATED);
}
$this->tests[$name] = $test;
} }
public function getTests() public function getTests()
{ {
return $this->tests; return $this->tests;
} }
public function getName()
{
return 'staging';
}
} }
class_alias('Twig\Extension\StagingExtension', 'Twig_Extension_Staging'); class_alias('Twig\Extension\StagingExtension', 'Twig_Extension_Staging');

View File

@ -12,10 +12,7 @@
namespace Twig\Extension { namespace Twig\Extension {
use Twig\TwigFunction; use Twig\TwigFunction;
/** final class StringLoaderExtension extends AbstractExtension
* @final
*/
class StringLoaderExtension extends AbstractExtension
{ {
public function getFunctions() public function getFunctions()
{ {
@ -23,11 +20,6 @@ class StringLoaderExtension extends AbstractExtension
new TwigFunction('template_from_string', 'twig_template_from_string', ['needs_environment' => true]), new TwigFunction('template_from_string', 'twig_template_from_string', ['needs_environment' => true]),
]; ];
} }
public function getName()
{
return 'string_loader';
}
} }
class_alias('Twig\Extension\StringLoaderExtension', 'Twig_Extension_StringLoader'); class_alias('Twig\Extension\StringLoaderExtension', 'Twig_Extension_StringLoader');
@ -47,7 +39,7 @@ use Twig\TemplateWrapper;
* *
* @return TemplateWrapper * @return TemplateWrapper
*/ */
function twig_template_from_string(Environment $env, $template, $name = null) function twig_template_from_string(Environment $env, $template, string $name = null)
{ {
return $env->createTemplate((string) $template, $name); return $env->createTemplate((string) $template, $name);
} }

View File

@ -0,0 +1,475 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig;
use Twig\Error\RuntimeError;
use Twig\Extension\ExtensionInterface;
use Twig\Extension\GlobalsInterface;
use Twig\Extension\InitRuntimeInterface;
use Twig\Extension\StagingExtension;
use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\TokenParser\TokenParserInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
final class ExtensionSet
{
private $extensions;
private $initialized = false;
private $runtimeInitialized = false;
private $staging;
private $parsers;
private $visitors;
private $filters;
private $tests;
private $functions;
private $unaryOperators;
private $binaryOperators;
private $globals;
private $functionCallbacks = [];
private $filterCallbacks = [];
private $lastModified = 0;
public function __construct()
{
$this->staging = new StagingExtension();
}
/**
* Initializes the runtime environment.
*
* @deprecated since Twig 2.7
*/
public function initRuntime(Environment $env)
{
if ($this->runtimeInitialized) {
return;
}
$this->runtimeInitialized = true;
foreach ($this->extensions as $extension) {
if ($extension instanceof InitRuntimeInterface) {
$extension->initRuntime($env);
}
}
}
public function hasExtension(string $class): bool
{
$class = ltrim($class, '\\');
if (!isset($this->extensions[$class]) && class_exists($class, false)) {
// For BC/FC with namespaced aliases
$class = (new \ReflectionClass($class))->name;
}
return isset($this->extensions[$class]);
}
public function getExtension(string $class): ExtensionInterface
{
$class = ltrim($class, '\\');
if (!isset($this->extensions[$class]) && class_exists($class, false)) {
// For BC/FC with namespaced aliases
$class = (new \ReflectionClass($class))->name;
}
if (!isset($this->extensions[$class])) {
throw new RuntimeError(sprintf('The "%s" extension is not enabled.', $class));
}
return $this->extensions[$class];
}
/**
* @param ExtensionInterface[] $extensions
*/
public function setExtensions(array $extensions)
{
foreach ($extensions as $extension) {
$this->addExtension($extension);
}
}
/**
* @return ExtensionInterface[]
*/
public function getExtensions(): array
{
return $this->extensions;
}
public function getSignature(): string
{
return json_encode(array_keys($this->extensions));
}
public function isInitialized(): bool
{
return $this->initialized || $this->runtimeInitialized;
}
public function getLastModified(): int
{
if (0 !== $this->lastModified) {
return $this->lastModified;
}
foreach ($this->extensions as $extension) {
$r = new \ReflectionObject($extension);
if (file_exists($r->getFileName()) && ($extensionTime = filemtime($r->getFileName())) > $this->lastModified) {
$this->lastModified = $extensionTime;
}
}
return $this->lastModified;
}
public function addExtension(ExtensionInterface $extension)
{
$class = \get_class($extension);
if ($this->initialized) {
throw new \LogicException(sprintf('Unable to register extension "%s" as extensions have already been initialized.', $class));
}
if (isset($this->extensions[$class])) {
throw new \LogicException(sprintf('Unable to register extension "%s" as it is already registered.', $class));
}
$this->extensions[$class] = $extension;
}
public function addFunction(TwigFunction $function)
{
if ($this->initialized) {
throw new \LogicException(sprintf('Unable to add function "%s" as extensions have already been initialized.', $function->getName()));
}
$this->staging->addFunction($function);
}
/**
* @return TwigFunction[]
*/
public function getFunctions(): array
{
if (!$this->initialized) {
$this->initExtensions();
}
return $this->functions;
}
/**
* @return TwigFunction|false
*/
public function getFunction(string $name)
{
if (!$this->initialized) {
$this->initExtensions();
}
if (isset($this->functions[$name])) {
return $this->functions[$name];
}
foreach ($this->functions as $pattern => $function) {
$pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
if ($count && preg_match('#^'.$pattern.'$#', $name, $matches)) {
array_shift($matches);
$function->setArguments($matches);
return $function;
}
}
foreach ($this->functionCallbacks as $callback) {
if (false !== $function = $callback($name)) {
return $function;
}
}
return false;
}
public function registerUndefinedFunctionCallback(callable $callable)
{
$this->functionCallbacks[] = $callable;
}
public function addFilter(TwigFilter $filter)
{
if ($this->initialized) {
throw new \LogicException(sprintf('Unable to add filter "%s" as extensions have already been initialized.', $filter->getName()));
}
$this->staging->addFilter($filter);
}
/**
* @return TwigFilter[]
*/
public function getFilters(): array
{
if (!$this->initialized) {
$this->initExtensions();
}
return $this->filters;
}
/**
* @return TwigFilter|false
*/
public function getFilter(string $name)
{
if (!$this->initialized) {
$this->initExtensions();
}
if (isset($this->filters[$name])) {
return $this->filters[$name];
}
foreach ($this->filters as $pattern => $filter) {
$pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
if ($count && preg_match('#^'.$pattern.'$#', $name, $matches)) {
array_shift($matches);
$filter->setArguments($matches);
return $filter;
}
}
foreach ($this->filterCallbacks as $callback) {
if (false !== $filter = $callback($name)) {
return $filter;
}
}
return false;
}
public function registerUndefinedFilterCallback(callable $callable)
{
$this->filterCallbacks[] = $callable;
}
public function addNodeVisitor(NodeVisitorInterface $visitor)
{
if ($this->initialized) {
throw new \LogicException('Unable to add a node visitor as extensions have already been initialized.');
}
$this->staging->addNodeVisitor($visitor);
}
/**
* @return NodeVisitorInterface[]
*/
public function getNodeVisitors(): array
{
if (!$this->initialized) {
$this->initExtensions();
}
return $this->visitors;
}
public function addTokenParser(TokenParserInterface $parser)
{
if ($this->initialized) {
throw new \LogicException('Unable to add a token parser as extensions have already been initialized.');
}
$this->staging->addTokenParser($parser);
}
/**
* @return TokenParserInterface[]
*/
public function getTokenParsers(): array
{
if (!$this->initialized) {
$this->initExtensions();
}
return $this->parsers;
}
public function getGlobals(): array
{
if (null !== $this->globals) {
return $this->globals;
}
$globals = [];
foreach ($this->extensions as $extension) {
if (!$extension instanceof GlobalsInterface) {
continue;
}
$extGlobals = $extension->getGlobals();
if (!\is_array($extGlobals)) {
throw new \UnexpectedValueException(sprintf('"%s::getGlobals()" must return an array of globals.', \get_class($extension)));
}
$globals = array_merge($globals, $extGlobals);
}
if ($this->initialized) {
$this->globals = $globals;
}
return $globals;
}
public function addTest(TwigTest $test)
{
if ($this->initialized) {
throw new \LogicException(sprintf('Unable to add test "%s" as extensions have already been initialized.', $test->getName()));
}
$this->staging->addTest($test);
}
/**
* @return TwigTest[]
*/
public function getTests(): array
{
if (!$this->initialized) {
$this->initExtensions();
}
return $this->tests;
}
/**
* @return TwigTest|false
*/
public function getTest(string $name)
{
if (!$this->initialized) {
$this->initExtensions();
}
if (isset($this->tests[$name])) {
return $this->tests[$name];
}
foreach ($this->tests as $pattern => $test) {
$pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
if ($count) {
if (preg_match('#^'.$pattern.'$#', $name, $matches)) {
array_shift($matches);
$test->setArguments($matches);
return $test;
}
}
}
return false;
}
public function getUnaryOperators(): array
{
if (!$this->initialized) {
$this->initExtensions();
}
return $this->unaryOperators;
}
public function getBinaryOperators(): array
{
if (!$this->initialized) {
$this->initExtensions();
}
return $this->binaryOperators;
}
private function initExtensions()
{
$this->parsers = [];
$this->filters = [];
$this->functions = [];
$this->tests = [];
$this->visitors = [];
$this->unaryOperators = [];
$this->binaryOperators = [];
foreach ($this->extensions as $extension) {
$this->initExtension($extension);
}
$this->initExtension($this->staging);
// Done at the end only, so that an exception during initialization does not mark the environment as initialized when catching the exception
$this->initialized = true;
}
private function initExtension(ExtensionInterface $extension)
{
// filters
foreach ($extension->getFilters() as $filter) {
$this->filters[$filter->getName()] = $filter;
}
// functions
foreach ($extension->getFunctions() as $function) {
$this->functions[$function->getName()] = $function;
}
// tests
foreach ($extension->getTests() as $test) {
$this->tests[$test->getName()] = $test;
}
// token parsers
foreach ($extension->getTokenParsers() as $parser) {
if (!$parser instanceof TokenParserInterface) {
throw new \LogicException('getTokenParsers() must return an array of \Twig\TokenParser\TokenParserInterface.');
}
$this->parsers[] = $parser;
}
// node visitors
foreach ($extension->getNodeVisitors() as $visitor) {
$this->visitors[] = $visitor;
}
// operators
if ($operators = $extension->getOperators()) {
if (!\is_array($operators)) {
throw new \InvalidArgumentException(sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), \is_object($operators) ? \get_class($operators) : \gettype($operators).(\is_resource($operators) ? '' : '#'.$operators)));
}
if (2 !== \count($operators)) {
throw new \InvalidArgumentException(sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators)));
}
$this->unaryOperators = array_merge($this->unaryOperators, $operators[0]);
$this->binaryOperators = array_merge($this->binaryOperators, $operators[1]);
}
}
}
class_alias('Twig\ExtensionSet', 'Twig_ExtensionSet');

View File

@ -41,7 +41,7 @@ class FileExtensionEscapingStrategy
$name = substr($name, 0, -5); $name = substr($name, 0, -5);
} }
$extension = pathinfo($name, PATHINFO_EXTENSION); $extension = pathinfo($name, \PATHINFO_EXTENSION);
switch ($extension) { switch ($extension) {
case 'js': case 'js':

View File

@ -19,39 +19,36 @@ use Twig\Error\SyntaxError;
* *
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*/ */
class Lexer implements \Twig_LexerInterface class Lexer
{ {
protected $tokens; private $tokens;
protected $code; private $code;
protected $cursor; private $cursor;
protected $lineno; private $lineno;
protected $end; private $end;
protected $state; private $state;
protected $states; private $states;
protected $brackets; private $brackets;
protected $env; private $env;
// to be renamed to $name in 2.0 (where it is private)
protected $filename;
protected $options;
protected $regexes;
protected $position;
protected $positions;
protected $currentVarBlockLine;
private $source; private $source;
private $options;
private $regexes;
private $position;
private $positions;
private $currentVarBlockLine;
const STATE_DATA = 0; public const STATE_DATA = 0;
const STATE_BLOCK = 1; public const STATE_BLOCK = 1;
const STATE_VAR = 2; public const STATE_VAR = 2;
const STATE_STRING = 3; public const STATE_STRING = 3;
const STATE_INTERPOLATION = 4; public const STATE_INTERPOLATION = 4;
const REGEX_NAME = '/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A'; public const REGEX_NAME = '/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A';
const REGEX_NUMBER = '/[0-9]+(?:\.[0-9]+)?([Ee][\+\-][0-9]+)?/A'; public const REGEX_NUMBER = '/[0-9]+(?:\.[0-9]+)?([Ee][\+\-][0-9]+)?/A';
const REGEX_STRING = '/"([^#"\\\\]*(?:\\\\.[^#"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As'; public const REGEX_STRING = '/"([^#"\\\\]*(?:\\\\.[^#"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As';
const REGEX_DQ_STRING_DELIM = '/"/A'; public const REGEX_DQ_STRING_DELIM = '/"/A';
const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As'; public const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As';
const PUNCTUATION = '()[]{}?:.,|'; public const PUNCTUATION = '()[]{}?:.,|';
public function __construct(Environment $env, array $options = []) public function __construct(Environment $env, array $options = [])
{ {
@ -100,9 +97,7 @@ class Lexer implements \Twig_LexerInterface
$this->options['whitespace_trim']. // - $this->options['whitespace_trim']. // -
'|'. '|'.
$this->options['whitespace_line_trim']. // ~ $this->options['whitespace_line_trim']. // ~
')?\s*'. ')?\s*endverbatim\s*'.
'(?:end%s)'. // endraw or endverbatim
'\s*'.
'(?:'. '(?:'.
preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '#').'\s*'. // -%} preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '#').'\s*'. // -%}
'|'. '|'.
@ -117,7 +112,7 @@ class Lexer implements \Twig_LexerInterface
// #} // #}
'lex_comment' => '{ 'lex_comment' => '{
(?:'. (?:'.
preg_quote($this->options['whitespace_trim']).preg_quote($this->options['tag_comment'][1], '#').'\s*\n?'. // -#}\s*\n? preg_quote($this->options['whitespace_trim'].$this->options['tag_comment'][1], '#').'\s*\n?'. // -#}\s*\n?
'|'. '|'.
preg_quote($this->options['whitespace_line_trim'].$this->options['tag_comment'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~#}[ \t\0\x0B]* preg_quote($this->options['whitespace_line_trim'].$this->options['tag_comment'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~#}[ \t\0\x0B]*
'|'. '|'.
@ -127,9 +122,7 @@ class Lexer implements \Twig_LexerInterface
// verbatim %} // verbatim %}
'lex_block_raw' => '{ 'lex_block_raw' => '{
\s* \s*verbatim\s*
(raw|verbatim)
\s*
(?:'. (?:'.
preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '#').'\s*'. // -%}\s* preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '#').'\s*'. // -%}\s*
'|'. '|'.
@ -160,28 +153,10 @@ class Lexer implements \Twig_LexerInterface
]; ];
} }
public function tokenize($code, $name = null) public function tokenize(Source $source)
{ {
if (!$code instanceof Source) { $this->source = $source;
@trigger_error(sprintf('Passing a string as the $code argument of %s() is deprecated since version 1.27 and will be removed in 2.0. Pass a \Twig\Source instance instead.', __METHOD__), E_USER_DEPRECATED); $this->code = str_replace(["\r\n", "\r"], "\n", $source->getCode());
$this->source = new Source($code, $name);
} else {
$this->source = $code;
}
if (((int) ini_get('mbstring.func_overload')) & 2) {
@trigger_error('Support for having "mbstring.func_overload" different from 0 is deprecated version 1.29 and will be removed in 2.0.', E_USER_DEPRECATED);
}
if (\function_exists('mb_internal_encoding') && ((int) ini_get('mbstring.func_overload')) & 2) {
$mbEncoding = mb_internal_encoding();
mb_internal_encoding('ASCII');
} else {
$mbEncoding = null;
}
$this->code = str_replace(["\r\n", "\r"], "\n", $this->source->getCode());
$this->filename = $this->source->getName();
$this->cursor = 0; $this->cursor = 0;
$this->lineno = 1; $this->lineno = 1;
$this->end = \strlen($this->code); $this->end = \strlen($this->code);
@ -192,7 +167,7 @@ class Lexer implements \Twig_LexerInterface
$this->position = -1; $this->position = -1;
// find all token starts in one go // find all token starts in one go
preg_match_all($this->regexes['lex_tokens_start'], $this->code, $matches, PREG_OFFSET_CAPTURE); preg_match_all($this->regexes['lex_tokens_start'], $this->code, $matches, \PREG_OFFSET_CAPTURE);
$this->positions = $matches; $this->positions = $matches;
while ($this->cursor < $this->end) { while ($this->cursor < $this->end) {
@ -221,25 +196,21 @@ class Lexer implements \Twig_LexerInterface
} }
} }
$this->pushToken(Token::EOF_TYPE); $this->pushToken(/* Token::EOF_TYPE */ -1);
if (!empty($this->brackets)) { if (!empty($this->brackets)) {
list($expect, $lineno) = array_pop($this->brackets); list($expect, $lineno) = array_pop($this->brackets);
throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source); throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
} }
if ($mbEncoding) {
mb_internal_encoding($mbEncoding);
}
return new TokenStream($this->tokens, $this->source); return new TokenStream($this->tokens, $this->source);
} }
protected function lexData() private function lexData()
{ {
// if no matches are left we return the rest of the template as simple text token // if no matches are left we return the rest of the template as simple text token
if ($this->position == \count($this->positions[0]) - 1) { if ($this->position == \count($this->positions[0]) - 1) {
$this->pushToken(Token::TEXT_TYPE, substr($this->code, $this->cursor)); $this->pushToken(/* Token::TEXT_TYPE */ 0, substr($this->code, $this->cursor));
$this->cursor = $this->end; $this->cursor = $this->end;
return; return;
@ -268,7 +239,7 @@ class Lexer implements \Twig_LexerInterface
$text = rtrim($text, " \t\0\x0B"); $text = rtrim($text, " \t\0\x0B");
} }
} }
$this->pushToken(Token::TEXT_TYPE, $text); $this->pushToken(/* Token::TEXT_TYPE */ 0, $text);
$this->moveCursor($textContent.$position[0]); $this->moveCursor($textContent.$position[0]);
switch ($this->positions[1][$this->position][0]) { switch ($this->positions[1][$this->position][0]) {
@ -280,30 +251,30 @@ class Lexer implements \Twig_LexerInterface
// raw data? // raw data?
if (preg_match($this->regexes['lex_block_raw'], $this->code, $match, 0, $this->cursor)) { if (preg_match($this->regexes['lex_block_raw'], $this->code, $match, 0, $this->cursor)) {
$this->moveCursor($match[0]); $this->moveCursor($match[0]);
$this->lexRawData($match[1]); $this->lexRawData();
// {% line \d+ %} // {% line \d+ %}
} elseif (preg_match($this->regexes['lex_block_line'], $this->code, $match, 0, $this->cursor)) { } elseif (preg_match($this->regexes['lex_block_line'], $this->code, $match, 0, $this->cursor)) {
$this->moveCursor($match[0]); $this->moveCursor($match[0]);
$this->lineno = (int) $match[1]; $this->lineno = (int) $match[1];
} else { } else {
$this->pushToken(Token::BLOCK_START_TYPE); $this->pushToken(/* Token::BLOCK_START_TYPE */ 1);
$this->pushState(self::STATE_BLOCK); $this->pushState(self::STATE_BLOCK);
$this->currentVarBlockLine = $this->lineno; $this->currentVarBlockLine = $this->lineno;
} }
break; break;
case $this->options['tag_variable'][0]: case $this->options['tag_variable'][0]:
$this->pushToken(Token::VAR_START_TYPE); $this->pushToken(/* Token::VAR_START_TYPE */ 2);
$this->pushState(self::STATE_VAR); $this->pushState(self::STATE_VAR);
$this->currentVarBlockLine = $this->lineno; $this->currentVarBlockLine = $this->lineno;
break; break;
} }
} }
protected function lexBlock() private function lexBlock()
{ {
if (empty($this->brackets) && preg_match($this->regexes['lex_block'], $this->code, $match, 0, $this->cursor)) { if (empty($this->brackets) && preg_match($this->regexes['lex_block'], $this->code, $match, 0, $this->cursor)) {
$this->pushToken(Token::BLOCK_END_TYPE); $this->pushToken(/* Token::BLOCK_END_TYPE */ 3);
$this->moveCursor($match[0]); $this->moveCursor($match[0]);
$this->popState(); $this->popState();
} else { } else {
@ -311,10 +282,10 @@ class Lexer implements \Twig_LexerInterface
} }
} }
protected function lexVar() private function lexVar()
{ {
if (empty($this->brackets) && preg_match($this->regexes['lex_var'], $this->code, $match, 0, $this->cursor)) { if (empty($this->brackets) && preg_match($this->regexes['lex_var'], $this->code, $match, 0, $this->cursor)) {
$this->pushToken(Token::VAR_END_TYPE); $this->pushToken(/* Token::VAR_END_TYPE */ 4);
$this->moveCursor($match[0]); $this->moveCursor($match[0]);
$this->popState(); $this->popState();
} else { } else {
@ -322,7 +293,7 @@ class Lexer implements \Twig_LexerInterface
} }
} }
protected function lexExpression() private function lexExpression()
{ {
// whitespace // whitespace
if (preg_match('/\s+/A', $this->code, $match, 0, $this->cursor)) { if (preg_match('/\s+/A', $this->code, $match, 0, $this->cursor)) {
@ -340,21 +311,21 @@ class Lexer implements \Twig_LexerInterface
} }
// operators // operators
elseif (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) { elseif (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) {
$this->pushToken(Token::OPERATOR_TYPE, preg_replace('/\s+/', ' ', $match[0])); $this->pushToken(/* Token::OPERATOR_TYPE */ 8, preg_replace('/\s+/', ' ', $match[0]));
$this->moveCursor($match[0]); $this->moveCursor($match[0]);
} }
// names // names
elseif (preg_match(self::REGEX_NAME, $this->code, $match, 0, $this->cursor)) { elseif (preg_match(self::REGEX_NAME, $this->code, $match, 0, $this->cursor)) {
$this->pushToken(Token::NAME_TYPE, $match[0]); $this->pushToken(/* Token::NAME_TYPE */ 5, $match[0]);
$this->moveCursor($match[0]); $this->moveCursor($match[0]);
} }
// numbers // numbers
elseif (preg_match(self::REGEX_NUMBER, $this->code, $match, 0, $this->cursor)) { elseif (preg_match(self::REGEX_NUMBER, $this->code, $match, 0, $this->cursor)) {
$number = (float) $match[0]; // floats $number = (float) $match[0]; // floats
if (ctype_digit($match[0]) && $number <= PHP_INT_MAX) { if (ctype_digit($match[0]) && $number <= \PHP_INT_MAX) {
$number = (int) $match[0]; // integers lower than the maximum $number = (int) $match[0]; // integers lower than the maximum
} }
$this->pushToken(Token::NUMBER_TYPE, $number); $this->pushToken(/* Token::NUMBER_TYPE */ 6, $number);
$this->moveCursor($match[0]); $this->moveCursor($match[0]);
} }
// punctuation // punctuation
@ -375,12 +346,12 @@ class Lexer implements \Twig_LexerInterface
} }
} }
$this->pushToken(Token::PUNCTUATION_TYPE, $this->code[$this->cursor]); $this->pushToken(/* Token::PUNCTUATION_TYPE */ 9, $this->code[$this->cursor]);
++$this->cursor; ++$this->cursor;
} }
// strings // strings
elseif (preg_match(self::REGEX_STRING, $this->code, $match, 0, $this->cursor)) { elseif (preg_match(self::REGEX_STRING, $this->code, $match, 0, $this->cursor)) {
$this->pushToken(Token::STRING_TYPE, stripcslashes(substr($match[0], 1, -1))); $this->pushToken(/* Token::STRING_TYPE */ 7, stripcslashes(substr($match[0], 1, -1)));
$this->moveCursor($match[0]); $this->moveCursor($match[0]);
} }
// opening double quoted string // opening double quoted string
@ -395,14 +366,10 @@ class Lexer implements \Twig_LexerInterface
} }
} }
protected function lexRawData($tag) private function lexRawData()
{ {
if ('raw' === $tag) { if (!preg_match($this->regexes['lex_raw_data'], $this->code, $match, \PREG_OFFSET_CAPTURE, $this->cursor)) {
@trigger_error(sprintf('Twig Tag "raw" is deprecated since version 1.21. Use "verbatim" instead in %s at line %d.', $this->filename, $this->lineno), E_USER_DEPRECATED); throw new SyntaxError('Unexpected end of file: Unclosed "verbatim" block.', $this->lineno, $this->source);
}
if (!preg_match(str_replace('%s', $tag, $this->regexes['lex_raw_data']), $this->code, $match, PREG_OFFSET_CAPTURE, $this->cursor)) {
throw new SyntaxError(sprintf('Unexpected end of file: Unclosed "%s" block.', $tag), $this->lineno, $this->source);
} }
$text = substr($this->code, $this->cursor, $match[0][1] - $this->cursor); $text = substr($this->code, $this->cursor, $match[0][1] - $this->cursor);
@ -420,27 +387,27 @@ class Lexer implements \Twig_LexerInterface
} }
} }
$this->pushToken(Token::TEXT_TYPE, $text); $this->pushToken(/* Token::TEXT_TYPE */ 0, $text);
} }
protected function lexComment() private function lexComment()
{ {
if (!preg_match($this->regexes['lex_comment'], $this->code, $match, PREG_OFFSET_CAPTURE, $this->cursor)) { if (!preg_match($this->regexes['lex_comment'], $this->code, $match, \PREG_OFFSET_CAPTURE, $this->cursor)) {
throw new SyntaxError('Unclosed comment.', $this->lineno, $this->source); throw new SyntaxError('Unclosed comment.', $this->lineno, $this->source);
} }
$this->moveCursor(substr($this->code, $this->cursor, $match[0][1] - $this->cursor).$match[0][0]); $this->moveCursor(substr($this->code, $this->cursor, $match[0][1] - $this->cursor).$match[0][0]);
} }
protected function lexString() private function lexString()
{ {
if (preg_match($this->regexes['interpolation_start'], $this->code, $match, 0, $this->cursor)) { if (preg_match($this->regexes['interpolation_start'], $this->code, $match, 0, $this->cursor)) {
$this->brackets[] = [$this->options['interpolation'][0], $this->lineno]; $this->brackets[] = [$this->options['interpolation'][0], $this->lineno];
$this->pushToken(Token::INTERPOLATION_START_TYPE); $this->pushToken(/* Token::INTERPOLATION_START_TYPE */ 10);
$this->moveCursor($match[0]); $this->moveCursor($match[0]);
$this->pushState(self::STATE_INTERPOLATION); $this->pushState(self::STATE_INTERPOLATION);
} elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, 0, $this->cursor) && \strlen($match[0]) > 0) { } elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, 0, $this->cursor) && \strlen($match[0]) > 0) {
$this->pushToken(Token::STRING_TYPE, stripcslashes($match[0])); $this->pushToken(/* Token::STRING_TYPE */ 7, stripcslashes($match[0]));
$this->moveCursor($match[0]); $this->moveCursor($match[0]);
} elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) { } elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) {
list($expect, $lineno) = array_pop($this->brackets); list($expect, $lineno) = array_pop($this->brackets);
@ -456,12 +423,12 @@ class Lexer implements \Twig_LexerInterface
} }
} }
protected function lexInterpolation() private function lexInterpolation()
{ {
$bracket = end($this->brackets); $bracket = end($this->brackets);
if ($this->options['interpolation'][0] === $bracket[0] && preg_match($this->regexes['interpolation_end'], $this->code, $match, 0, $this->cursor)) { if ($this->options['interpolation'][0] === $bracket[0] && preg_match($this->regexes['interpolation_end'], $this->code, $match, 0, $this->cursor)) {
array_pop($this->brackets); array_pop($this->brackets);
$this->pushToken(Token::INTERPOLATION_END_TYPE); $this->pushToken(/* Token::INTERPOLATION_END_TYPE */ 11);
$this->moveCursor($match[0]); $this->moveCursor($match[0]);
$this->popState(); $this->popState();
} else { } else {
@ -469,23 +436,23 @@ class Lexer implements \Twig_LexerInterface
} }
} }
protected function pushToken($type, $value = '') private function pushToken($type, $value = '')
{ {
// do not push empty text tokens // do not push empty text tokens
if (Token::TEXT_TYPE === $type && '' === $value) { if (/* Token::TEXT_TYPE */ 0 === $type && '' === $value) {
return; return;
} }
$this->tokens[] = new Token($type, $value, $this->lineno); $this->tokens[] = new Token($type, $value, $this->lineno);
} }
protected function moveCursor($text) private function moveCursor($text)
{ {
$this->cursor += \strlen($text); $this->cursor += \strlen($text);
$this->lineno += substr_count($text, "\n"); $this->lineno += substr_count($text, "\n");
} }
protected function getOperatorRegex() private function getOperatorRegex()
{ {
$operators = array_merge( $operators = array_merge(
['='], ['='],
@ -499,11 +466,15 @@ class Lexer implements \Twig_LexerInterface
$regex = []; $regex = [];
foreach ($operators as $operator => $length) { foreach ($operators as $operator => $length) {
// an operator that ends with a character must be followed by // an operator that ends with a character must be followed by
// a whitespace or a parenthesis // a whitespace, a parenthesis, an opening map [ or sequence {
if (ctype_alpha($operator[$length - 1])) {
$r = preg_quote($operator, '/').'(?=[\s()])';
} else {
$r = preg_quote($operator, '/'); $r = preg_quote($operator, '/');
if (ctype_alpha($operator[$length - 1])) {
$r .= '(?=[\s()\[{])';
}
// an operator that begins with a character must not have a dot or pipe before
if (ctype_alpha($operator[0])) {
$r = '(?<![\.\|])'.$r;
} }
// an operator with a space can be any amount of whitespaces // an operator with a space can be any amount of whitespaces
@ -515,13 +486,13 @@ class Lexer implements \Twig_LexerInterface
return '/'.implode('|', $regex).'/A'; return '/'.implode('|', $regex).'/A';
} }
protected function pushState($state) private function pushState($state)
{ {
$this->states[] = $this->state; $this->states[] = $this->state;
$this->state = $state; $this->state = $state;
} }
protected function popState() private function popState()
{ {
if (0 === \count($this->states)) { if (0 === \count($this->states)) {
throw new \LogicException('Cannot pop state without a previous state.'); throw new \LogicException('Cannot pop state without a previous state.');

View File

@ -24,13 +24,11 @@ use Twig\Source;
* *
* This loader should only be used for unit testing. * This loader should only be used for unit testing.
* *
* @final
*
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*/ */
class ArrayLoader implements LoaderInterface, ExistsLoaderInterface, SourceContextLoaderInterface final class ArrayLoader implements LoaderInterface, ExistsLoaderInterface, SourceContextLoaderInterface
{ {
protected $templates = []; private $templates = [];
/** /**
* @param array $templates An array of templates (keys are the names, and values are the source code) * @param array $templates An array of templates (keys are the names, and values are the source code)
@ -48,19 +46,7 @@ class ArrayLoader implements LoaderInterface, ExistsLoaderInterface, SourceConte
*/ */
public function setTemplate($name, $template) public function setTemplate($name, $template)
{ {
$this->templates[(string) $name] = $template; $this->templates[$name] = $template;
}
public function getSource($name)
{
@trigger_error(sprintf('Calling "getSource" on "%s" is deprecated since 1.27. Use getSourceContext() instead.', \get_class($this)), E_USER_DEPRECATED);
$name = (string) $name;
if (!isset($this->templates[$name])) {
throw new LoaderError(sprintf('Template "%s" is not defined.', $name));
}
return $this->templates[$name];
} }
public function getSourceContext($name) public function getSourceContext($name)
@ -75,12 +61,11 @@ class ArrayLoader implements LoaderInterface, ExistsLoaderInterface, SourceConte
public function exists($name) public function exists($name)
{ {
return isset($this->templates[(string) $name]); return isset($this->templates[$name]);
} }
public function getCacheKey($name) public function getCacheKey($name)
{ {
$name = (string) $name;
if (!isset($this->templates[$name])) { if (!isset($this->templates[$name])) {
throw new LoaderError(sprintf('Template "%s" is not defined.', $name)); throw new LoaderError(sprintf('Template "%s" is not defined.', $name));
} }
@ -90,7 +75,6 @@ class ArrayLoader implements LoaderInterface, ExistsLoaderInterface, SourceConte
public function isFresh($name, $time) public function isFresh($name, $time)
{ {
$name = (string) $name;
if (!isset($this->templates[$name])) { if (!isset($this->templates[$name])) {
throw new LoaderError(sprintf('Template "%s" is not defined.', $name)); throw new LoaderError(sprintf('Template "%s" is not defined.', $name));
} }

View File

@ -12,19 +12,16 @@
namespace Twig\Loader; namespace Twig\Loader;
use Twig\Error\LoaderError; use Twig\Error\LoaderError;
use Twig\Source;
/** /**
* Loads templates from other loaders. * Loads templates from other loaders.
* *
* @final
*
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*/ */
class ChainLoader implements LoaderInterface, ExistsLoaderInterface, SourceContextLoaderInterface final class ChainLoader implements LoaderInterface, ExistsLoaderInterface, SourceContextLoaderInterface
{ {
private $hasSourceCache = []; private $hasSourceCache = [];
protected $loaders = []; private $loaders = [];
/** /**
* @param LoaderInterface[] $loaders * @param LoaderInterface[] $loaders
@ -50,40 +47,16 @@ class ChainLoader implements LoaderInterface, ExistsLoaderInterface, SourceConte
return $this->loaders; return $this->loaders;
} }
public function getSource($name)
{
@trigger_error(sprintf('Calling "getSource" on "%s" is deprecated since 1.27. Use getSourceContext() instead.', \get_class($this)), E_USER_DEPRECATED);
$exceptions = [];
foreach ($this->loaders as $loader) {
if ($loader instanceof ExistsLoaderInterface && !$loader->exists($name)) {
continue;
}
try {
return $loader->getSource($name);
} catch (LoaderError $e) {
$exceptions[] = $e->getMessage();
}
}
throw new LoaderError(sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : ''));
}
public function getSourceContext($name) public function getSourceContext($name)
{ {
$exceptions = []; $exceptions = [];
foreach ($this->loaders as $loader) { foreach ($this->loaders as $loader) {
if ($loader instanceof ExistsLoaderInterface && !$loader->exists($name)) { if (!$loader->exists($name)) {
continue; continue;
} }
try { try {
if ($loader instanceof SourceContextLoaderInterface) {
return $loader->getSourceContext($name); return $loader->getSourceContext($name);
}
return new Source($loader->getSource($name), $name);
} catch (LoaderError $e) { } catch (LoaderError $e) {
$exceptions[] = $e->getMessage(); $exceptions[] = $e->getMessage();
} }
@ -94,31 +67,14 @@ class ChainLoader implements LoaderInterface, ExistsLoaderInterface, SourceConte
public function exists($name) public function exists($name)
{ {
$name = (string) $name;
if (isset($this->hasSourceCache[$name])) { if (isset($this->hasSourceCache[$name])) {
return $this->hasSourceCache[$name]; return $this->hasSourceCache[$name];
} }
foreach ($this->loaders as $loader) { foreach ($this->loaders as $loader) {
if ($loader instanceof ExistsLoaderInterface) {
if ($loader->exists($name)) { if ($loader->exists($name)) {
return $this->hasSourceCache[$name] = true; return $this->hasSourceCache[$name] = true;
} }
continue;
}
try {
if ($loader instanceof SourceContextLoaderInterface) {
$loader->getSourceContext($name);
} else {
$loader->getSource($name);
}
return $this->hasSourceCache[$name] = true;
} catch (LoaderError $e) {
}
} }
return $this->hasSourceCache[$name] = false; return $this->hasSourceCache[$name] = false;
@ -128,7 +84,7 @@ class ChainLoader implements LoaderInterface, ExistsLoaderInterface, SourceConte
{ {
$exceptions = []; $exceptions = [];
foreach ($this->loaders as $loader) { foreach ($this->loaders as $loader) {
if ($loader instanceof ExistsLoaderInterface && !$loader->exists($name)) { if (!$loader->exists($name)) {
continue; continue;
} }
@ -146,7 +102,7 @@ class ChainLoader implements LoaderInterface, ExistsLoaderInterface, SourceConte
{ {
$exceptions = []; $exceptions = [];
foreach ($this->loaders as $loader) { foreach ($this->loaders as $loader) {
if ($loader instanceof ExistsLoaderInterface && !$loader->exists($name)) { if (!$loader->exists($name)) {
continue; continue;
} }

View File

@ -12,22 +12,12 @@
namespace Twig\Loader; namespace Twig\Loader;
/** /**
* Adds an exists() method for loaders. * Empty interface for Twig 1.x compatibility.
* *
* @author Florin Patan <florinpatan@gmail.com> * @deprecated since Twig 2.7, to be removed in 3.0
*
* @deprecated since 1.12 (to be removed in 3.0)
*/ */
interface ExistsLoaderInterface interface ExistsLoaderInterface extends LoaderInterface
{ {
/**
* Check if we have the source code of a template, given its name.
*
* @param string $name The name of the template to check if we can load
*
* @return bool If the template source code is handled by this loader or not
*/
public function exists($name);
} }
class_alias('Twig\Loader\ExistsLoaderInterface', 'Twig_ExistsLoaderInterface'); class_alias('Twig\Loader\ExistsLoaderInterface', 'Twig_ExistsLoaderInterface');

View File

@ -22,7 +22,7 @@ use Twig\Source;
class FilesystemLoader implements LoaderInterface, ExistsLoaderInterface, SourceContextLoaderInterface class FilesystemLoader implements LoaderInterface, ExistsLoaderInterface, SourceContextLoaderInterface
{ {
/** Identifier of the main namespace. */ /** Identifier of the main namespace. */
const MAIN_NAMESPACE = '__main__'; public const MAIN_NAMESPACE = '__main__';
protected $paths = []; protected $paths = [];
protected $cache = []; protected $cache = [];
@ -34,10 +34,10 @@ class FilesystemLoader implements LoaderInterface, ExistsLoaderInterface, Source
* @param string|array $paths A path or an array of paths where to look for templates * @param string|array $paths A path or an array of paths where to look for templates
* @param string|null $rootPath The root path common to all relative paths (null for getcwd()) * @param string|null $rootPath The root path common to all relative paths (null for getcwd())
*/ */
public function __construct($paths = [], $rootPath = null) public function __construct($paths = [], string $rootPath = null)
{ {
$this->rootPath = (null === $rootPath ? getcwd() : $rootPath).\DIRECTORY_SEPARATOR; $this->rootPath = (null === $rootPath ? getcwd() : $rootPath).\DIRECTORY_SEPARATOR;
if (false !== $realPath = realpath($rootPath)) { if (null !== $rootPath && false !== ($realPath = realpath($rootPath))) {
$this->rootPath = $realPath.\DIRECTORY_SEPARATOR; $this->rootPath = $realPath.\DIRECTORY_SEPARATOR;
} }
@ -136,17 +136,6 @@ class FilesystemLoader implements LoaderInterface, ExistsLoaderInterface, Source
} }
} }
public function getSource($name)
{
@trigger_error(sprintf('Calling "getSource" on "%s" is deprecated since 1.27. Use getSourceContext() instead.', \get_class($this)), E_USER_DEPRECATED);
if (null === ($path = $this->findTemplate($name)) || false === $path) {
return '';
}
return file_get_contents($path);
}
public function getSourceContext($name) public function getSourceContext($name)
{ {
if (null === ($path = $this->findTemplate($name)) || false === $path) { if (null === ($path = $this->findTemplate($name)) || false === $path) {
@ -177,13 +166,7 @@ class FilesystemLoader implements LoaderInterface, ExistsLoaderInterface, Source
return true; return true;
} }
try {
return null !== ($path = $this->findTemplate($name, false)) && false !== $path; return null !== ($path = $this->findTemplate($name, false)) && false !== $path;
} catch (LoaderError $e) {
@trigger_error(sprintf('In %s::findTemplate(), you must accept a second argument that when set to "false" returns "false" instead of throwing an exception. Not supporting this argument is deprecated since version 1.27.', \get_class($this)), E_USER_DEPRECATED);
return false;
}
} }
public function isFresh($name, $time) public function isFresh($name, $time)
@ -199,13 +182,15 @@ class FilesystemLoader implements LoaderInterface, ExistsLoaderInterface, Source
/** /**
* Checks if the template can be found. * Checks if the template can be found.
* *
* In Twig 3.0, findTemplate must return a string or null (returning false won't work anymore).
*
* @param string $name The template name * @param string $name The template name
* @param bool $throw Whether to throw an exception when an error occurs
* *
* @return string|false|null The template name or false/null * @return string|false|null The template name or false/null
*/ */
protected function findTemplate($name) protected function findTemplate($name, $throw = true)
{ {
$throw = \func_num_args() > 1 ? func_get_arg(1) : true;
$name = $this->normalizeName($name); $name = $this->normalizeName($name);
if (isset($this->cache[$name])) { if (isset($this->cache[$name])) {
@ -221,9 +206,9 @@ class FilesystemLoader implements LoaderInterface, ExistsLoaderInterface, Source
} }
try { try {
$this->validateName($name);
list($namespace, $shortname) = $this->parseName($name); list($namespace, $shortname) = $this->parseName($name);
$this->validateName($shortname);
} catch (LoaderError $e) { } catch (LoaderError $e) {
if (!$throw) { if (!$throw) {
return false; return false;
@ -265,7 +250,12 @@ class FilesystemLoader implements LoaderInterface, ExistsLoaderInterface, Source
throw new LoaderError($this->errorCache[$name]); throw new LoaderError($this->errorCache[$name]);
} }
protected function parseName($name, $default = self::MAIN_NAMESPACE) private function normalizeName($name)
{
return preg_replace('#/{2,}#', '/', str_replace('\\', '/', (string) $name));
}
private function parseName($name, $default = self::MAIN_NAMESPACE)
{ {
if (isset($name[0]) && '@' == $name[0]) { if (isset($name[0]) && '@' == $name[0]) {
if (false === $pos = strpos($name, '/')) { if (false === $pos = strpos($name, '/')) {
@ -281,12 +271,7 @@ class FilesystemLoader implements LoaderInterface, ExistsLoaderInterface, Source
return [$default, $name]; return [$default, $name];
} }
protected function normalizeName($name) private function validateName($name)
{
return preg_replace('#/{2,}#', '/', str_replace('\\', '/', (string) $name));
}
protected function validateName($name)
{ {
if (false !== strpos($name, "\0")) { if (false !== strpos($name, "\0")) {
throw new LoaderError('A template name cannot contain NUL bytes.'); throw new LoaderError('A template name cannot contain NUL bytes.');
@ -312,10 +297,10 @@ class FilesystemLoader implements LoaderInterface, ExistsLoaderInterface, Source
{ {
return strspn($file, '/\\', 0, 1) return strspn($file, '/\\', 0, 1)
|| (\strlen($file) > 3 && ctype_alpha($file[0]) || (\strlen($file) > 3 && ctype_alpha($file[0])
&& ':' === substr($file, 1, 1) && ':' === $file[1]
&& strspn($file, '/\\', 2, 1) && strspn($file, '/\\', 2, 1)
) )
|| null !== parse_url($file, PHP_URL_SCHEME) || null !== parse_url($file, \PHP_URL_SCHEME)
; ;
} }
} }

View File

@ -12,6 +12,7 @@
namespace Twig\Loader; namespace Twig\Loader;
use Twig\Error\LoaderError; use Twig\Error\LoaderError;
use Twig\Source;
/** /**
* Interface all loaders must implement. * Interface all loaders must implement.
@ -21,17 +22,15 @@ use Twig\Error\LoaderError;
interface LoaderInterface interface LoaderInterface
{ {
/** /**
* Gets the source code of a template, given its name. * Returns the source context for a given template logical name.
* *
* @param string $name The name of the template to load * @param string $name The template logical name
* *
* @return string The template source code * @return Source
* *
* @throws LoaderError When $name is not found * @throws LoaderError When $name is not found
*
* @deprecated since 1.27 (to be removed in 2.0), implement Twig\Loader\SourceContextLoaderInterface
*/ */
public function getSource($name); public function getSourceContext($name);
/** /**
* Gets the cache key to use for the cache for a given template name. * Gets the cache key to use for the cache for a given template name.
@ -56,6 +55,15 @@ interface LoaderInterface
* @throws LoaderError When $name is not found * @throws LoaderError When $name is not found
*/ */
public function isFresh($name, $time); public function isFresh($name, $time);
/**
* Check if we have the source code of a template, given its name.
*
* @param string $name The name of the template to check if we can load
*
* @return bool If the template source code is handled by this loader or not
*/
public function exists($name);
} }
class_alias('Twig\Loader\LoaderInterface', 'Twig_LoaderInterface'); class_alias('Twig\Loader\LoaderInterface', 'Twig_LoaderInterface');

View File

@ -11,28 +11,11 @@
namespace Twig\Loader; namespace Twig\Loader;
use Twig\Error\LoaderError;
use Twig\Source;
/** /**
* Adds a getSourceContext() method for loaders. * Empty interface for Twig 1.x compatibility.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @deprecated since 1.27 (to be removed in 3.0)
*/ */
interface SourceContextLoaderInterface interface SourceContextLoaderInterface extends LoaderInterface
{ {
/**
* Returns the source context for a given template logical name.
*
* @param string $name The template logical name
*
* @return Source
*
* @throws LoaderError When $name is not found
*/
public function getSourceContext($name);
} }
class_alias('Twig\Loader\SourceContextLoaderInterface', 'Twig_SourceContextLoaderInterface'); class_alias('Twig\Loader\SourceContextLoaderInterface', 'Twig_SourceContextLoaderInterface');

View File

@ -16,10 +16,10 @@ namespace Twig;
* *
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*/ */
class Markup implements \Countable class Markup implements \Countable, \JsonSerializable
{ {
protected $content; private $content;
protected $charset; private $charset;
public function __construct($content, $charset) public function __construct($content, $charset)
{ {
@ -32,9 +32,22 @@ class Markup implements \Countable
return $this->content; return $this->content;
} }
/**
* @return int
*/
#[\ReturnTypeWillChange]
public function count() public function count()
{ {
return \function_exists('mb_get_info') ? mb_strlen($this->content, $this->charset) : \strlen($this->content); return mb_strlen($this->content, $this->charset);
}
/**
* @return mixed
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->content;
} }
} }

View File

@ -26,7 +26,7 @@ use Twig\Compiler;
*/ */
class AutoEscapeNode extends Node class AutoEscapeNode extends Node
{ {
public function __construct($value, \Twig_NodeInterface $body, $lineno, $tag = 'autoescape') public function __construct($value, Node $body, int $lineno, string $tag = 'autoescape')
{ {
parent::__construct(['body' => $body], ['value' => $value], $lineno, $tag); parent::__construct(['body' => $body], ['value' => $value], $lineno, $tag);
} }

View File

@ -21,7 +21,7 @@ use Twig\Compiler;
*/ */
class BlockNode extends Node class BlockNode extends Node
{ {
public function __construct($name, \Twig_NodeInterface $body, $lineno, $tag = null) public function __construct(string $name, Node $body, int $lineno, string $tag = null)
{ {
parent::__construct(['body' => $body], ['name' => $name], $lineno, $tag); parent::__construct(['body' => $body], ['name' => $name], $lineno, $tag);
} }
@ -32,6 +32,7 @@ class BlockNode extends Node
->addDebugInfo($this) ->addDebugInfo($this)
->write(sprintf("public function block_%s(\$context, array \$blocks = [])\n", $this->getAttribute('name')), "{\n") ->write(sprintf("public function block_%s(\$context, array \$blocks = [])\n", $this->getAttribute('name')), "{\n")
->indent() ->indent()
->write("\$macros = \$this->macros;\n")
; ;
$compiler $compiler

View File

@ -21,7 +21,7 @@ use Twig\Compiler;
*/ */
class BlockReferenceNode extends Node implements NodeOutputInterface class BlockReferenceNode extends Node implements NodeOutputInterface
{ {
public function __construct($name, $lineno, $tag = null) public function __construct(string $name, int $lineno, string $tag = null)
{ {
parent::__construct([], ['name' => $name], $lineno, $tag); parent::__construct([], ['name' => $name], $lineno, $tag);
} }

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Node;
use Twig\Compiler;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class CheckSecurityCallNode extends Node
{
public function compile(Compiler $compiler)
{
$compiler
->write("\$this->sandbox = \$this->env->getExtension('\Twig\Extension\SandboxExtension');\n")
->write("\$this->checkSecurity();\n")
;
}
}

View File

@ -18,9 +18,9 @@ use Twig\Compiler;
*/ */
class CheckSecurityNode extends Node class CheckSecurityNode extends Node
{ {
protected $usedFilters; private $usedFilters;
protected $usedTags; private $usedTags;
protected $usedFunctions; private $usedFunctions;
public function __construct(array $usedFilters, array $usedTags, array $usedFunctions) public function __construct(array $usedFilters, array $usedTags, array $usedFunctions)
{ {
@ -45,10 +45,13 @@ class CheckSecurityNode extends Node
} }
$compiler $compiler
->write("\$this->sandbox = \$this->env->getExtension('\Twig\Extension\SandboxExtension');\n") ->write("\n")
->write('$tags = ')->repr(array_filter($tags))->raw(";\n") ->write("public function checkSecurity()\n")
->write('$filters = ')->repr(array_filter($filters))->raw(";\n") ->write("{\n")
->write('$functions = ')->repr(array_filter($functions))->raw(";\n\n") ->indent()
->write('static $tags = ')->repr(array_filter($tags))->raw(";\n")
->write('static $filters = ')->repr(array_filter($filters))->raw(";\n")
->write('static $functions = ')->repr(array_filter($functions))->raw(";\n\n")
->write("try {\n") ->write("try {\n")
->indent() ->indent()
->write("\$this->sandbox->checkSecurity(\n") ->write("\$this->sandbox->checkSecurity(\n")
@ -61,7 +64,7 @@ class CheckSecurityNode extends Node
->outdent() ->outdent()
->write("} catch (SecurityError \$e) {\n") ->write("} catch (SecurityError \$e) {\n")
->indent() ->indent()
->write("\$e->setSourceContext(\$this->getSourceContext());\n\n") ->write("\$e->setSourceContext(\$this->source);\n\n")
->write("if (\$e instanceof SecurityNotAllowedTagError && isset(\$tags[\$e->getTagName()])) {\n") ->write("if (\$e instanceof SecurityNotAllowedTagError && isset(\$tags[\$e->getTagName()])) {\n")
->indent() ->indent()
->write("\$e->setTemplateLine(\$tags[\$e->getTagName()]);\n") ->write("\$e->setTemplateLine(\$tags[\$e->getTagName()]);\n")
@ -78,6 +81,8 @@ class CheckSecurityNode extends Node
->write("throw \$e;\n") ->write("throw \$e;\n")
->outdent() ->outdent()
->write("}\n\n") ->write("}\n\n")
->outdent()
->write("}\n")
; ;
} }
} }

View File

@ -33,10 +33,13 @@ class CheckToStringNode extends AbstractExpression
public function compile(Compiler $compiler) public function compile(Compiler $compiler)
{ {
$expr = $this->getNode('expr');
$compiler $compiler
->raw('$this->sandbox->ensureToStringAllowed(') ->raw('$this->sandbox->ensureToStringAllowed(')
->subcompile($this->getNode('expr')) ->subcompile($expr)
->raw(')') ->raw(', ')
->repr($expr->getTemplateLine())
->raw(', $this->source)')
; ;
} }
} }

View File

@ -22,7 +22,7 @@ use Twig\Node\Expression\ConstantExpression;
*/ */
class DeprecatedNode extends Node class DeprecatedNode extends Node
{ {
public function __construct(AbstractExpression $expr, $lineno, $tag = null) public function __construct(AbstractExpression $expr, int $lineno, string $tag = null)
{ {
parent::__construct(['expr' => $expr], [], $lineno, $tag); parent::__construct(['expr' => $expr], [], $lineno, $tag);
} }

View File

@ -21,7 +21,7 @@ use Twig\Node\Expression\AbstractExpression;
*/ */
class DoNode extends Node class DoNode extends Node
{ {
public function __construct(AbstractExpression $expr, $lineno, $tag = null) public function __construct(AbstractExpression $expr, int $lineno, string $tag = null)
{ {
parent::__construct(['expr' => $expr], [], $lineno, $tag); parent::__construct(['expr' => $expr], [], $lineno, $tag);
} }

View File

@ -23,13 +23,11 @@ use Twig\Node\Expression\ConstantExpression;
class EmbedNode extends IncludeNode class EmbedNode extends IncludeNode
{ {
// we don't inject the module to avoid node visitors to traverse it twice (as it will be already visited in the main module) // we don't inject the module to avoid node visitors to traverse it twice (as it will be already visited in the main module)
public function __construct($name, $index, AbstractExpression $variables = null, $only = false, $ignoreMissing = false, $lineno, $tag = null) public function __construct(string $name, int $index, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno, string $tag = null)
{ {
parent::__construct(new ConstantExpression('not_used', $lineno), $variables, $only, $ignoreMissing, $lineno, $tag); parent::__construct(new ConstantExpression('not_used', $lineno), $variables, $only, $ignoreMissing, $lineno, $tag);
$this->setAttribute('name', $name); $this->setAttribute('name', $name);
// to be removed in 2.0, used name instead
$this->setAttribute('filename', $name);
$this->setAttribute('index', $index); $this->setAttribute('index', $index);
} }

View File

@ -15,9 +15,9 @@ use Twig\Compiler;
class ArrayExpression extends AbstractExpression class ArrayExpression extends AbstractExpression
{ {
protected $index; private $index;
public function __construct(array $elements, $lineno) public function __construct(array $elements, int $lineno)
{ {
parent::__construct($elements, [], $lineno); parent::__construct($elements, [], $lineno);

View File

@ -44,7 +44,7 @@ class ArrowFunctionExpression extends AbstractExpression
; ;
} }
$compiler $compiler
->raw(') use ($context) { ') ->raw(') use ($context, $macros) { ')
; ;
foreach ($this->getNode('names') as $name) { foreach ($this->getNode('names') as $name) {
$compiler $compiler

View File

@ -14,10 +14,11 @@ namespace Twig\Node\Expression\Binary;
use Twig\Compiler; use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Node;
abstract class AbstractBinary extends AbstractExpression abstract class AbstractBinary extends AbstractExpression
{ {
public function __construct(\Twig_NodeInterface $left, \Twig_NodeInterface $right, $lineno) public function __construct(Node $left, Node $right, int $lineno)
{ {
parent::__construct(['left' => $left, 'right' => $right], [], $lineno); parent::__construct(['left' => $left, 'right' => $right], [], $lineno);
} }

View File

@ -15,21 +15,6 @@ use Twig\Compiler;
class PowerBinary extends AbstractBinary class PowerBinary extends AbstractBinary
{ {
public function compile(Compiler $compiler)
{
if (\PHP_VERSION_ID >= 50600) {
return parent::compile($compiler);
}
$compiler
->raw('pow(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))
->raw(')')
;
}
public function operator(Compiler $compiler) public function operator(Compiler $compiler)
{ {
return $compiler->raw('**'); return $compiler->raw('**');

View File

@ -0,0 +1,22 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
class SpaceshipBinary extends AbstractBinary
{
public function operator(Compiler $compiler)
{
return $compiler->raw('<=>');
}
}

View File

@ -22,17 +22,8 @@ use Twig\Node\Node;
*/ */
class BlockReferenceExpression extends AbstractExpression class BlockReferenceExpression extends AbstractExpression
{ {
/** public function __construct(Node $name, ?Node $template, int $lineno, string $tag = null)
* @param Node|null $template
*/
public function __construct(\Twig_NodeInterface $name, $template = null, $lineno, $tag = null)
{ {
if (\is_bool($template)) {
@trigger_error(sprintf('The %s method "$asString" argument is deprecated since version 1.28 and will be removed in 2.0.', __METHOD__), E_USER_DEPRECATED);
$template = null;
}
$nodes = ['name' => $name]; $nodes = ['name' => $name];
if (null !== $template) { if (null !== $template) {
$nodes['template'] = $template; $nodes['template'] = $template;
@ -58,7 +49,7 @@ class BlockReferenceExpression extends AbstractExpression
} }
} }
private function compileTemplateCall(Compiler $compiler, $method) private function compileTemplateCall(Compiler $compiler, string $method): Compiler
{ {
if (!$this->hasNode('template')) { if (!$this->hasNode('template')) {
$compiler->write('$this'); $compiler->write('$this');
@ -75,12 +66,11 @@ class BlockReferenceExpression extends AbstractExpression
} }
$compiler->raw(sprintf('->%s', $method)); $compiler->raw(sprintf('->%s', $method));
$this->compileBlockArguments($compiler);
return $compiler; return $this->compileBlockArguments($compiler);
} }
private function compileBlockArguments(Compiler $compiler) private function compileBlockArguments(Compiler $compiler): Compiler
{ {
$compiler $compiler
->raw('(') ->raw('(')

View File

@ -22,39 +22,39 @@ abstract class CallExpression extends AbstractExpression
protected function compileCallable(Compiler $compiler) protected function compileCallable(Compiler $compiler)
{ {
$closingParenthesis = false; $callable = $this->getAttribute('callable');
$isArray = false;
if ($this->hasAttribute('callable') && $callable = $this->getAttribute('callable')) {
if (\is_string($callable) && false === strpos($callable, '::')) { if (\is_string($callable) && false === strpos($callable, '::')) {
$compiler->raw($callable); $compiler->raw($callable);
} else { } else {
list($r, $callable) = $this->reflectCallable($callable); [$r, $callable] = $this->reflectCallable($callable);
if ($r instanceof \ReflectionMethod && \is_string($callable[0])) {
if ($r->isStatic()) { if (\is_string($callable)) {
$compiler->raw($callable);
} elseif (\is_array($callable) && \is_string($callable[0])) {
if (!$r instanceof \ReflectionMethod || $r->isStatic()) {
$compiler->raw(sprintf('%s::%s', $callable[0], $callable[1])); $compiler->raw(sprintf('%s::%s', $callable[0], $callable[1]));
} else { } else {
$compiler->raw(sprintf('$this->env->getRuntime(\'%s\')->%s', $callable[0], $callable[1])); $compiler->raw(sprintf('$this->env->getRuntime(\'%s\')->%s', $callable[0], $callable[1]));
} }
} elseif ($r instanceof \ReflectionMethod && $callable[0] instanceof ExtensionInterface) { } elseif (\is_array($callable) && $callable[0] instanceof ExtensionInterface) {
$compiler->raw(sprintf('$this->env->getExtension(\'%s\')->%s', \get_class($callable[0]), $callable[1])); $class = \get_class($callable[0]);
if (!$compiler->getEnvironment()->hasExtension($class)) {
// Compile a non-optimized call to trigger a \Twig\Error\RuntimeError, which cannot be a compile-time error
$compiler->raw(sprintf('$this->env->getExtension(\'%s\')', $class));
} else { } else {
$type = ucfirst($this->getAttribute('type')); $compiler->raw(sprintf('$this->extensions[\'%s\']', ltrim($class, '\\')));
$compiler->raw(sprintf('call_user_func_array($this->env->get%s(\'%s\')->getCallable(), ', $type, $this->getAttribute('name')));
$closingParenthesis = true;
$isArray = true;
}
}
} else {
$compiler->raw($this->getAttribute('thing')->compile());
} }
$this->compileArguments($compiler, $isArray); $compiler->raw(sprintf('->%s', $callable[1]));
} else {
if ($closingParenthesis) { $compiler->raw(sprintf('$this->env->get%s(\'%s\')->getCallable()', ucfirst($this->getAttribute('type')), $this->getAttribute('name')));
$compiler->raw(')');
} }
} }
$this->compileArguments($compiler);
}
protected function compileArguments(Compiler $compiler, $isArray = false) protected function compileArguments(Compiler $compiler, $isArray = false)
{ {
$compiler->raw($isArray ? '[' : '('); $compiler->raw($isArray ? '[' : '(');
@ -93,10 +93,8 @@ abstract class CallExpression extends AbstractExpression
} }
if ($this->hasNode('arguments')) { if ($this->hasNode('arguments')) {
$callable = $this->hasAttribute('callable') ? $this->getAttribute('callable') : null; $callable = $this->getAttribute('callable');
$arguments = $this->getArguments($callable, $this->getNode('arguments')); $arguments = $this->getArguments($callable, $this->getNode('arguments'));
foreach ($arguments as $node) { foreach ($arguments as $node) {
if (!$first) { if (!$first) {
$compiler->raw(', '); $compiler->raw(', ');
@ -142,14 +140,23 @@ abstract class CallExpression extends AbstractExpression
throw new \LogicException($message); throw new \LogicException($message);
} }
$callableParameters = $this->getCallableParameters($callable, $isVariadic); list($callableParameters, $isPhpVariadic) = $this->getCallableParameters($callable, $isVariadic);
$arguments = []; $arguments = [];
$names = []; $names = [];
$missingArguments = []; $missingArguments = [];
$optionalArguments = []; $optionalArguments = [];
$pos = 0; $pos = 0;
foreach ($callableParameters as $callableParameter) { foreach ($callableParameters as $callableParameter) {
$names[] = $name = $this->normalizeName($callableParameter->name); $name = $this->normalizeName($callableParameter->name);
if (\PHP_VERSION_ID >= 80000 && 'range' === $callable) {
if ('start' === $name) {
$name = 'low';
} elseif ('end' === $name) {
$name = 'high';
}
}
$names[] = $name;
if (\array_key_exists($name, $parameters)) { if (\array_key_exists($name, $parameters)) {
if (\array_key_exists($pos, $parameters)) { if (\array_key_exists($pos, $parameters)) {
@ -187,7 +194,7 @@ abstract class CallExpression extends AbstractExpression
} }
if ($isVariadic) { if ($isVariadic) {
$arbitraryArguments = new ArrayExpression([], -1); $arbitraryArguments = $isPhpVariadic ? new VariadicExpression([], -1) : new ArrayExpression([], -1);
foreach ($parameters as $key => $value) { foreach ($parameters as $key => $value) {
if (\is_int($key)) { if (\is_int($key)) {
$arbitraryArguments->addElement($value); $arbitraryArguments->addElement($value);
@ -230,12 +237,9 @@ abstract class CallExpression extends AbstractExpression
return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $name)); return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $name));
} }
private function getCallableParameters($callable, $isVariadic) private function getCallableParameters($callable, bool $isVariadic): array
{ {
list($r) = $this->reflectCallable($callable); [$r, , $callableName] = $this->reflectCallable($callable);
if (null === $r) {
return [];
}
$parameters = $r->getParameters(); $parameters = $r->getParameters();
if ($this->hasNode('node')) { if ($this->hasNode('node')) {
@ -252,21 +256,21 @@ abstract class CallExpression extends AbstractExpression
array_shift($parameters); array_shift($parameters);
} }
} }
$isPhpVariadic = false;
if ($isVariadic) { if ($isVariadic) {
$argument = end($parameters); $argument = end($parameters);
if ($argument && $argument->isArray() && $argument->isDefaultValueAvailable() && [] === $argument->getDefaultValue()) { $isArray = $argument && $argument->hasType() && 'array' === $argument->getType()->getName();
if ($isArray && $argument->isDefaultValueAvailable() && [] === $argument->getDefaultValue()) {
array_pop($parameters); array_pop($parameters);
} elseif ($argument && $argument->isVariadic()) {
array_pop($parameters);
$isPhpVariadic = true;
} else { } else {
$callableName = $r->name;
if ($r instanceof \ReflectionMethod) {
$callableName = $r->getDeclaringClass()->name.'::'.$callableName;
}
throw new \LogicException(sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $callableName, $this->getAttribute('type'), $this->getAttribute('name'))); throw new \LogicException(sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $callableName, $this->getAttribute('type'), $this->getAttribute('name')));
} }
} }
return $parameters; return [$parameters, $isPhpVariadic];
} }
private function reflectCallable($callable) private function reflectCallable($callable)
@ -275,30 +279,44 @@ abstract class CallExpression extends AbstractExpression
return $this->reflector; return $this->reflector;
} }
if (\is_array($callable)) { if (\is_string($callable) && false !== $pos = strpos($callable, '::')) {
if (!method_exists($callable[0], $callable[1])) { $callable = [substr($callable, 0, $pos), substr($callable, 2 + $pos)];
// __call()
return [null, []];
}
$r = new \ReflectionMethod($callable[0], $callable[1]);
} elseif (\is_object($callable) && !$callable instanceof \Closure) {
$r = new \ReflectionObject($callable);
$r = $r->getMethod('__invoke');
$callable = [$callable, '__invoke'];
} elseif (\is_string($callable) && false !== $pos = strpos($callable, '::')) {
$class = substr($callable, 0, $pos);
$method = substr($callable, $pos + 2);
if (!method_exists($class, $method)) {
// __staticCall()
return [null, []];
}
$r = new \ReflectionMethod($callable);
$callable = [$class, $method];
} else {
$r = new \ReflectionFunction($callable);
} }
return $this->reflector = [$r, $callable]; if (\is_array($callable) && method_exists($callable[0], $callable[1])) {
$r = new \ReflectionMethod($callable[0], $callable[1]);
return $this->reflector = [$r, $callable, $r->class.'::'.$r->name];
}
$checkVisibility = $callable instanceof \Closure;
try {
$closure = \Closure::fromCallable($callable);
} catch (\TypeError $e) {
throw new \LogicException(sprintf('Callback for %s "%s" is not callable in the current scope.', $this->getAttribute('type'), $this->getAttribute('name')), 0, $e);
}
$r = new \ReflectionFunction($closure);
if (false !== strpos($r->name, '{closure}')) {
return $this->reflector = [$r, $callable, 'Closure'];
}
if ($object = $r->getClosureThis()) {
$callable = [$object, $r->name];
$callableName = (\function_exists('get_debug_type') ? get_debug_type($object) : \get_class($object)).'::'.$r->name;
} elseif (\PHP_VERSION_ID >= 80111 && $class = $r->getClosureCalledClass()) {
$callableName = $class->name.'::'.$r->name;
} elseif (\PHP_VERSION_ID < 80111 && $class = $r->getClosureScopeClass()) {
$callableName = (\is_array($callable) ? $callable[0] : $class->name).'::'.$r->name;
} else {
$callable = $callableName = $r->name;
}
if ($checkVisibility && \is_array($callable) && method_exists(...$callable) && !(new \ReflectionMethod(...$callable))->isPublic()) {
$callable = $r->getClosure();
}
return $this->reflector = [$r, $callable, $callableName];
} }
} }

View File

@ -16,7 +16,7 @@ use Twig\Compiler;
class ConditionalExpression extends AbstractExpression class ConditionalExpression extends AbstractExpression
{ {
public function __construct(AbstractExpression $expr1, AbstractExpression $expr2, AbstractExpression $expr3, $lineno) public function __construct(AbstractExpression $expr1, AbstractExpression $expr2, AbstractExpression $expr3, int $lineno)
{ {
parent::__construct(['expr1' => $expr1, 'expr2' => $expr2, 'expr3' => $expr3], [], $lineno); parent::__construct(['expr1' => $expr1, 'expr2' => $expr2, 'expr3' => $expr3], [], $lineno);
} }

View File

@ -16,7 +16,7 @@ use Twig\Compiler;
class ConstantExpression extends AbstractExpression class ConstantExpression extends AbstractExpression
{ {
public function __construct($value, $lineno) public function __construct($value, int $lineno)
{ {
parent::__construct([], ['value' => $value], $lineno); parent::__construct([], ['value' => $value], $lineno);
} }

View File

@ -29,7 +29,7 @@ use Twig\Node\Node;
*/ */
class DefaultFilter extends FilterExpression class DefaultFilter extends FilterExpression
{ {
public function __construct(\Twig_NodeInterface $node, ConstantExpression $filterName, \Twig_NodeInterface $arguments, $lineno, $tag = null) public function __construct(Node $node, ConstantExpression $filterName, Node $arguments, int $lineno, string $tag = null)
{ {
$default = new FilterExpression($node, new ConstantExpression('default', $node->getTemplateLine()), $arguments, $node->getTemplateLine()); $default = new FilterExpression($node, new ConstantExpression('default', $node->getTemplateLine()), $arguments, $node->getTemplateLine());

View File

@ -13,11 +13,11 @@
namespace Twig\Node\Expression; namespace Twig\Node\Expression;
use Twig\Compiler; use Twig\Compiler;
use Twig\TwigFilter; use Twig\Node\Node;
class FilterExpression extends CallExpression class FilterExpression extends CallExpression
{ {
public function __construct(\Twig_NodeInterface $node, ConstantExpression $filterName, \Twig_NodeInterface $arguments, $lineno, $tag = null) public function __construct(Node $node, ConstantExpression $filterName, Node $arguments, int $lineno, string $tag = null)
{ {
parent::__construct(['node' => $node, 'filter' => $filterName, 'arguments' => $arguments], [], $lineno, $tag); parent::__construct(['node' => $node, 'filter' => $filterName, 'arguments' => $arguments], [], $lineno, $tag);
} }
@ -29,16 +29,11 @@ class FilterExpression extends CallExpression
$this->setAttribute('name', $name); $this->setAttribute('name', $name);
$this->setAttribute('type', 'filter'); $this->setAttribute('type', 'filter');
$this->setAttribute('thing', $filter);
$this->setAttribute('needs_environment', $filter->needsEnvironment()); $this->setAttribute('needs_environment', $filter->needsEnvironment());
$this->setAttribute('needs_context', $filter->needsContext()); $this->setAttribute('needs_context', $filter->needsContext());
$this->setAttribute('arguments', $filter->getArguments()); $this->setAttribute('arguments', $filter->getArguments());
if ($filter instanceof \Twig_FilterCallableInterface || $filter instanceof TwigFilter) {
$this->setAttribute('callable', $filter->getCallable()); $this->setAttribute('callable', $filter->getCallable());
}
if ($filter instanceof TwigFilter) {
$this->setAttribute('is_variadic', $filter->isVariadic()); $this->setAttribute('is_variadic', $filter->isVariadic());
}
$this->compileCallable($compiler); $this->compileCallable($compiler);
} }

View File

@ -12,11 +12,11 @@
namespace Twig\Node\Expression; namespace Twig\Node\Expression;
use Twig\Compiler; use Twig\Compiler;
use Twig\TwigFunction; use Twig\Node\Node;
class FunctionExpression extends CallExpression class FunctionExpression extends CallExpression
{ {
public function __construct($name, \Twig_NodeInterface $arguments, $lineno) public function __construct(string $name, Node $arguments, int $lineno)
{ {
parent::__construct(['arguments' => $arguments], ['name' => $name, 'is_defined_test' => false], $lineno); parent::__construct(['arguments' => $arguments], ['name' => $name, 'is_defined_test' => false], $lineno);
} }
@ -28,21 +28,15 @@ class FunctionExpression extends CallExpression
$this->setAttribute('name', $name); $this->setAttribute('name', $name);
$this->setAttribute('type', 'function'); $this->setAttribute('type', 'function');
$this->setAttribute('thing', $function);
$this->setAttribute('needs_environment', $function->needsEnvironment()); $this->setAttribute('needs_environment', $function->needsEnvironment());
$this->setAttribute('needs_context', $function->needsContext()); $this->setAttribute('needs_context', $function->needsContext());
$this->setAttribute('arguments', $function->getArguments()); $this->setAttribute('arguments', $function->getArguments());
if ($function instanceof \Twig_FunctionCallableInterface || $function instanceof TwigFunction) {
$callable = $function->getCallable(); $callable = $function->getCallable();
if ('constant' === $name && $this->getAttribute('is_defined_test')) { if ('constant' === $name && $this->getAttribute('is_defined_test')) {
$callable = 'twig_constant_is_defined'; $callable = 'twig_constant_is_defined';
} }
$this->setAttribute('callable', $callable); $this->setAttribute('callable', $callable);
}
if ($function instanceof TwigFunction) {
$this->setAttribute('is_variadic', $function->isVariadic()); $this->setAttribute('is_variadic', $function->isVariadic());
}
$this->compileCallable($compiler); $this->compileCallable($compiler);
} }

View File

@ -13,67 +13,76 @@
namespace Twig\Node\Expression; namespace Twig\Node\Expression;
use Twig\Compiler; use Twig\Compiler;
use Twig\Extension\SandboxExtension;
use Twig\Template; use Twig\Template;
class GetAttrExpression extends AbstractExpression class GetAttrExpression extends AbstractExpression
{ {
public function __construct(AbstractExpression $node, AbstractExpression $attribute, AbstractExpression $arguments = null, $type, $lineno) public function __construct(AbstractExpression $node, AbstractExpression $attribute, ?AbstractExpression $arguments, string $type, int $lineno)
{ {
$nodes = ['node' => $node, 'attribute' => $attribute]; $nodes = ['node' => $node, 'attribute' => $attribute];
if (null !== $arguments) { if (null !== $arguments) {
$nodes['arguments'] = $arguments; $nodes['arguments'] = $arguments;
} }
parent::__construct($nodes, ['type' => $type, 'is_defined_test' => false, 'ignore_strict_check' => false, 'disable_c_ext' => false], $lineno); parent::__construct($nodes, ['type' => $type, 'is_defined_test' => false, 'ignore_strict_check' => false, 'optimizable' => true], $lineno);
} }
public function compile(Compiler $compiler) public function compile(Compiler $compiler)
{ {
if ($this->getAttribute('disable_c_ext')) { $env = $compiler->getEnvironment();
@trigger_error(sprintf('Using the "disable_c_ext" attribute on %s is deprecated since version 1.30 and will be removed in 2.0.', __CLASS__), E_USER_DEPRECATED);
// optimize array calls
if (
$this->getAttribute('optimizable')
&& (!$env->isStrictVariables() || $this->getAttribute('ignore_strict_check'))
&& !$this->getAttribute('is_defined_test')
&& Template::ARRAY_CALL === $this->getAttribute('type')
) {
$var = '$'.$compiler->getVarName();
$compiler
->raw('(('.$var.' = ')
->subcompile($this->getNode('node'))
->raw(') && is_array(')
->raw($var)
->raw(') || ')
->raw($var)
->raw(' instanceof ArrayAccess ? (')
->raw($var)
->raw('[')
->subcompile($this->getNode('attribute'))
->raw('] ?? null) : null)')
;
return;
} }
if (\function_exists('twig_template_get_attributes') && !$this->getAttribute('disable_c_ext')) { $compiler->raw('twig_get_attribute($this->env, $this->source, ');
$compiler->raw('twig_template_get_attributes($this, ');
} else {
$compiler->raw('$this->getAttribute(');
}
if ($this->getAttribute('ignore_strict_check')) { if ($this->getAttribute('ignore_strict_check')) {
$this->getNode('node')->setAttribute('ignore_strict_check', true); $this->getNode('node')->setAttribute('ignore_strict_check', true);
} }
$compiler->subcompile($this->getNode('node')); $compiler
->subcompile($this->getNode('node'))
->raw(', ')
->subcompile($this->getNode('attribute'))
;
$compiler->raw(', ')->subcompile($this->getNode('attribute'));
// only generate optional arguments when needed (to make generated code more readable)
$needFourth = $this->getAttribute('ignore_strict_check');
$needThird = $needFourth || $this->getAttribute('is_defined_test');
$needSecond = $needThird || Template::ANY_CALL !== $this->getAttribute('type');
$needFirst = $needSecond || $this->hasNode('arguments');
if ($needFirst) {
if ($this->hasNode('arguments')) { if ($this->hasNode('arguments')) {
$compiler->raw(', ')->subcompile($this->getNode('arguments')); $compiler->raw(', ')->subcompile($this->getNode('arguments'));
} else { } else {
$compiler->raw(', []'); $compiler->raw(', []');
} }
}
if ($needSecond) { $compiler->raw(', ')
$compiler->raw(', ')->repr($this->getAttribute('type')); ->repr($this->getAttribute('type'))
} ->raw(', ')->repr($this->getAttribute('is_defined_test'))
->raw(', ')->repr($this->getAttribute('ignore_strict_check'))
if ($needThird) { ->raw(', ')->repr($env->hasExtension(SandboxExtension::class))
$compiler->raw(', ')->repr($this->getAttribute('is_defined_test')); ->raw(', ')->repr($this->getNode('node')->getTemplateLine())
} ->raw(')')
;
if ($needFourth) {
$compiler->raw(', ')->repr($this->getAttribute('ignore_strict_check'));
}
$compiler->raw(')');
} }
} }

View File

@ -15,9 +15,9 @@ use Twig\Compiler;
class MethodCallExpression extends AbstractExpression class MethodCallExpression extends AbstractExpression
{ {
public function __construct(AbstractExpression $node, $method, ArrayExpression $arguments, $lineno) public function __construct(AbstractExpression $node, string $method, ArrayExpression $arguments, int $lineno)
{ {
parent::__construct(['node' => $node, 'arguments' => $arguments], ['method' => $method, 'safe' => false], $lineno); parent::__construct(['node' => $node, 'arguments' => $arguments], ['method' => $method, 'safe' => false, 'is_defined_test' => false], $lineno);
if ($node instanceof NameExpression) { if ($node instanceof NameExpression) {
$node->setAttribute('always_defined', true); $node->setAttribute('always_defined', true);
@ -26,11 +26,24 @@ class MethodCallExpression extends AbstractExpression
public function compile(Compiler $compiler) public function compile(Compiler $compiler)
{ {
if ($this->getAttribute('is_defined_test')) {
$compiler $compiler
->subcompile($this->getNode('node')) ->raw('method_exists($macros[')
->raw('->') ->repr($this->getNode('node')->getAttribute('name'))
->raw($this->getAttribute('method')) ->raw('], ')
->raw('(') ->repr($this->getAttribute('method'))
->raw(')')
;
return;
}
$compiler
->raw('twig_call_macro($macros[')
->repr($this->getNode('node')->getAttribute('name'))
->raw('], ')
->repr($this->getAttribute('method'))
->raw(', [')
; ;
$first = true; $first = true;
foreach ($this->getNode('arguments')->getKeyValuePairs() as $pair) { foreach ($this->getNode('arguments')->getKeyValuePairs() as $pair) {
@ -41,7 +54,10 @@ class MethodCallExpression extends AbstractExpression
$compiler->subcompile($pair['value']); $compiler->subcompile($pair['value']);
} }
$compiler->raw(')'); $compiler
->raw('], ')
->repr($this->getTemplateLine())
->raw(', $context, $this->getSourceContext())');
} }
} }

View File

@ -16,13 +16,13 @@ use Twig\Compiler;
class NameExpression extends AbstractExpression class NameExpression extends AbstractExpression
{ {
protected $specialVars = [ private $specialVars = [
'_self' => '$this', '_self' => '$this->getTemplateName()',
'_context' => '$context', '_context' => '$context',
'_charset' => '$this->env->getCharset()', '_charset' => '$this->env->getCharset()',
]; ];
public function __construct($name, $lineno) public function __construct(string $name, int $lineno)
{ {
parent::__construct([], ['name' => $name, 'is_defined_test' => false, 'ignore_strict_check' => false, 'always_defined' => false], $lineno); parent::__construct([], ['name' => $name, 'is_defined_test' => false, 'ignore_strict_check' => false, 'always_defined' => false], $lineno);
} }
@ -36,7 +36,7 @@ class NameExpression extends AbstractExpression
if ($this->getAttribute('is_defined_test')) { if ($this->getAttribute('is_defined_test')) {
if ($this->isSpecial()) { if ($this->isSpecial()) {
$compiler->repr(true); $compiler->repr(true);
} elseif (\PHP_VERSION_ID >= 700400) { } elseif (\PHP_VERSION_ID >= 70400) {
$compiler $compiler
->raw('array_key_exists(') ->raw('array_key_exists(')
->string($name) ->string($name)
@ -60,45 +60,25 @@ class NameExpression extends AbstractExpression
->raw(']') ->raw(']')
; ;
} else { } else {
if (\PHP_VERSION_ID >= 70000) { if ($this->getAttribute('ignore_strict_check') || !$compiler->getEnvironment()->isStrictVariables()) {
// use PHP 7 null coalescing operator
$compiler $compiler
->raw('($context[') ->raw('($context[')
->string($name) ->string($name)
->raw('] ?? ') ->raw('] ?? null)')
; ;
if ($this->getAttribute('ignore_strict_check') || !$compiler->getEnvironment()->isStrictVariables()) {
$compiler->raw('null)');
} else { } else {
$compiler->raw('$this->getContext($context, ')->string($name)->raw('))');
}
} elseif (\PHP_VERSION_ID >= 50400) {
// PHP 5.4 ternary operator performance was optimized
$compiler $compiler
->raw('(isset($context[') ->raw('(isset($context[')
->string($name) ->string($name)
->raw(']) ? $context[') ->raw(']) || array_key_exists(')
->string($name) ->string($name)
->raw('] : ') ->raw(', $context) ? $context[')
;
if ($this->getAttribute('ignore_strict_check') || !$compiler->getEnvironment()->isStrictVariables()) {
$compiler->raw('null)');
} else {
$compiler->raw('$this->getContext($context, ')->string($name)->raw('))');
}
} else {
$compiler
->raw('$this->getContext($context, ')
->string($name) ->string($name)
; ->raw('] : (function () { throw new RuntimeError(\'Variable ')
->string($name)
if ($this->getAttribute('ignore_strict_check')) { ->raw(' does not exist.\', ')
$compiler->raw(', true'); ->repr($this->lineno)
} ->raw(', $this->source); })()')
$compiler
->raw(')') ->raw(')')
; ;
} }

View File

@ -20,7 +20,7 @@ use Twig\Node\Node;
class NullCoalesceExpression extends ConditionalExpression class NullCoalesceExpression extends ConditionalExpression
{ {
public function __construct(\Twig_NodeInterface $left, \Twig_NodeInterface $right, $lineno) public function __construct(Node $left, Node $right, int $lineno)
{ {
$test = new DefinedTest(clone $left, 'defined', new Node(), $left->getTemplateLine()); $test = new DefinedTest(clone $left, 'defined', new Node(), $left->getTemplateLine());
// for "block()", we don't need the null test as the return value is always a string // for "block()", we don't need the null test as the return value is always a string
@ -44,7 +44,7 @@ class NullCoalesceExpression extends ConditionalExpression
* cases might be implemented as an optimizer node visitor, but has not been done * cases might be implemented as an optimizer node visitor, but has not been done
* as benefits are probably not worth the added complexity. * as benefits are probably not worth the added complexity.
*/ */
if (\PHP_VERSION_ID >= 70000 && $this->getNode('expr2') instanceof NameExpression) { if ($this->getNode('expr2') instanceof NameExpression) {
$this->getNode('expr2')->setAttribute('always_defined', true); $this->getNode('expr2')->setAttribute('always_defined', true);
$compiler $compiler
->raw('((') ->raw('((')

View File

@ -21,7 +21,7 @@ use Twig\Compiler;
*/ */
class ParentExpression extends AbstractExpression class ParentExpression extends AbstractExpression
{ {
public function __construct($name, $lineno, $tag = null) public function __construct(string $name, int $lineno, string $tag = null)
{ {
parent::__construct([], ['output' => false, 'name' => $name], $lineno, $tag); parent::__construct([], ['output' => false, 'name' => $name], $lineno, $tag);
} }

View File

@ -15,7 +15,7 @@ use Twig\Compiler;
class TempNameExpression extends AbstractExpression class TempNameExpression extends AbstractExpression
{ {
public function __construct($name, $lineno) public function __construct(string $name, int $lineno)
{ {
parent::__construct([], ['name' => $name], $lineno); parent::__construct([], ['name' => $name], $lineno);
} }

View File

@ -18,8 +18,10 @@ use Twig\Node\Expression\BlockReferenceExpression;
use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FunctionExpression; use Twig\Node\Expression\FunctionExpression;
use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\MethodCallExpression;
use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\NameExpression;
use Twig\Node\Expression\TestExpression; use Twig\Node\Expression\TestExpression;
use Twig\Node\Node;
/** /**
* Checks if a variable is defined in the current context. * Checks if a variable is defined in the current context.
@ -33,7 +35,7 @@ use Twig\Node\Expression\TestExpression;
*/ */
class DefinedTest extends TestExpression class DefinedTest extends TestExpression
{ {
public function __construct(\Twig_NodeInterface $node, $name, \Twig_NodeInterface $arguments = null, $lineno) public function __construct(Node $node, string $name, ?Node $arguments, int $lineno)
{ {
if ($node instanceof NameExpression) { if ($node instanceof NameExpression) {
$node->setAttribute('is_defined_test', true); $node->setAttribute('is_defined_test', true);
@ -46,6 +48,8 @@ class DefinedTest extends TestExpression
$node->setAttribute('is_defined_test', true); $node->setAttribute('is_defined_test', true);
} elseif ($node instanceof ConstantExpression || $node instanceof ArrayExpression) { } elseif ($node instanceof ConstantExpression || $node instanceof ArrayExpression) {
$node = new ConstantExpression(true, $node->getTemplateLine()); $node = new ConstantExpression(true, $node->getTemplateLine());
} elseif ($node instanceof MethodCallExpression) {
$node->setAttribute('is_defined_test', true);
} else { } else {
throw new SyntaxError('The "defined" test only works with simple variables.', $lineno); throw new SyntaxError('The "defined" test only works with simple variables.', $lineno);
} }
@ -53,8 +57,9 @@ class DefinedTest extends TestExpression
parent::__construct($node, $name, $arguments, $lineno); parent::__construct($node, $name, $arguments, $lineno);
} }
protected function changeIgnoreStrictCheck(GetAttrExpression $node) private function changeIgnoreStrictCheck(GetAttrExpression $node)
{ {
$node->setAttribute('optimizable', false);
$node->setAttribute('ignore_strict_check', true); $node->setAttribute('ignore_strict_check', true);
if ($node->getNode('node') instanceof GetAttrExpression) { if ($node->getNode('node') instanceof GetAttrExpression) {

View File

@ -28,7 +28,7 @@ class OddTest extends TestExpression
$compiler $compiler
->raw('(') ->raw('(')
->subcompile($this->getNode('node')) ->subcompile($this->getNode('node'))
->raw(' % 2 == 1') ->raw(' % 2 != 0')
->raw(')') ->raw(')')
; ;
} }

View File

@ -12,11 +12,11 @@
namespace Twig\Node\Expression; namespace Twig\Node\Expression;
use Twig\Compiler; use Twig\Compiler;
use Twig\TwigTest; use Twig\Node\Node;
class TestExpression extends CallExpression class TestExpression extends CallExpression
{ {
public function __construct(\Twig_NodeInterface $node, $name, \Twig_NodeInterface $arguments = null, $lineno) public function __construct(Node $node, string $name, ?Node $arguments, int $lineno)
{ {
$nodes = ['node' => $node]; $nodes = ['node' => $node];
if (null !== $arguments) { if (null !== $arguments) {
@ -33,16 +33,9 @@ class TestExpression extends CallExpression
$this->setAttribute('name', $name); $this->setAttribute('name', $name);
$this->setAttribute('type', 'test'); $this->setAttribute('type', 'test');
$this->setAttribute('thing', $test);
if ($test instanceof TwigTest) {
$this->setAttribute('arguments', $test->getArguments()); $this->setAttribute('arguments', $test->getArguments());
}
if ($test instanceof \Twig_TestCallableInterface || $test instanceof TwigTest) {
$this->setAttribute('callable', $test->getCallable()); $this->setAttribute('callable', $test->getCallable());
}
if ($test instanceof TwigTest) {
$this->setAttribute('is_variadic', $test->isVariadic()); $this->setAttribute('is_variadic', $test->isVariadic());
}
$this->compileCallable($compiler); $this->compileCallable($compiler);
} }

View File

@ -14,10 +14,11 @@ namespace Twig\Node\Expression\Unary;
use Twig\Compiler; use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Node;
abstract class AbstractUnary extends AbstractExpression abstract class AbstractUnary extends AbstractExpression
{ {
public function __construct(\Twig_NodeInterface $node, $lineno) public function __construct(Node $node, int $lineno)
{ {
parent::__construct(['node' => $node], [], $lineno); parent::__construct(['node' => $node], [], $lineno);
} }

View File

@ -0,0 +1,24 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Node\Expression;
use Twig\Compiler;
class VariadicExpression extends ArrayExpression
{
public function compile(Compiler $compiler)
{
$compiler->raw('...');
parent::compile($compiler);
}
}

View File

@ -20,7 +20,7 @@ use Twig\Compiler;
*/ */
class FlushNode extends Node class FlushNode extends Node
{ {
public function __construct($lineno, $tag) public function __construct(int $lineno, string $tag)
{ {
parent::__construct([], [], $lineno, $tag); parent::__construct([], [], $lineno, $tag);
} }

View File

@ -20,7 +20,7 @@ use Twig\Compiler;
*/ */
class ForLoopNode extends Node class ForLoopNode extends Node
{ {
public function __construct($lineno, $tag = null) public function __construct(int $lineno, string $tag = null)
{ {
parent::__construct([], ['with_loop' => false, 'ifexpr' => false, 'else' => false], $lineno, $tag); parent::__construct([], ['with_loop' => false, 'ifexpr' => false, 'else' => false], $lineno, $tag);
} }

View File

@ -23,9 +23,9 @@ use Twig\Node\Expression\AssignNameExpression;
*/ */
class ForNode extends Node class ForNode extends Node
{ {
protected $loop; private $loop;
public function __construct(AssignNameExpression $keyTarget, AssignNameExpression $valueTarget, AbstractExpression $seq, AbstractExpression $ifexpr = null, \Twig_NodeInterface $body, \Twig_NodeInterface $else = null, $lineno, $tag = null) public function __construct(AssignNameExpression $keyTarget, AssignNameExpression $valueTarget, AbstractExpression $seq, ?AbstractExpression $ifexpr, Node $body, ?Node $else, int $lineno, string $tag = null)
{ {
$body = new Node([$body, $this->loop = new ForLoopNode($lineno, $tag)]); $body = new Node([$body, $this->loop = new ForLoopNode($lineno, $tag)]);

View File

@ -21,7 +21,7 @@ use Twig\Compiler;
*/ */
class IfNode extends Node class IfNode extends Node
{ {
public function __construct(\Twig_NodeInterface $tests, \Twig_NodeInterface $else = null, $lineno, $tag = null) public function __construct(Node $tests, ?Node $else, int $lineno, string $tag = null)
{ {
$nodes = ['tests' => $tests]; $nodes = ['tests' => $tests];
if (null !== $else) { if (null !== $else) {
@ -50,8 +50,11 @@ class IfNode extends Node
->subcompile($this->getNode('tests')->getNode($i)) ->subcompile($this->getNode('tests')->getNode($i))
->raw(") {\n") ->raw(") {\n")
->indent() ->indent()
->subcompile($this->getNode('tests')->getNode($i + 1))
; ;
// The node might not exists if the content is empty
if ($this->getNode('tests')->hasNode($i + 1)) {
$compiler->subcompile($this->getNode('tests')->getNode($i + 1));
}
} }
if ($this->hasNode('else')) { if ($this->hasNode('else')) {

View File

@ -22,20 +22,28 @@ use Twig\Node\Expression\NameExpression;
*/ */
class ImportNode extends Node class ImportNode extends Node
{ {
public function __construct(AbstractExpression $expr, AbstractExpression $var, $lineno, $tag = null) public function __construct(AbstractExpression $expr, AbstractExpression $var, int $lineno, string $tag = null, bool $global = true)
{ {
parent::__construct(['expr' => $expr, 'var' => $var], [], $lineno, $tag); parent::__construct(['expr' => $expr, 'var' => $var], ['global' => $global], $lineno, $tag);
} }
public function compile(Compiler $compiler) public function compile(Compiler $compiler)
{ {
$compiler $compiler
->addDebugInfo($this) ->addDebugInfo($this)
->write('') ->write('$macros[')
->subcompile($this->getNode('var')) ->repr($this->getNode('var')->getAttribute('name'))
->raw(' = ') ->raw('] = ')
; ;
if ($this->getAttribute('global')) {
$compiler
->raw('$this->macros[')
->repr($this->getNode('var')->getAttribute('name'))
->raw('] = ')
;
}
if ($this->getNode('expr') instanceof NameExpression && '_self' === $this->getNode('expr')->getAttribute('name')) { if ($this->getNode('expr') instanceof NameExpression && '_self' === $this->getNode('expr')->getAttribute('name')) {
$compiler->raw('$this'); $compiler->raw('$this');
} else { } else {

View File

@ -22,7 +22,7 @@ use Twig\Node\Expression\AbstractExpression;
*/ */
class IncludeNode extends Node implements NodeOutputInterface class IncludeNode extends Node implements NodeOutputInterface
{ {
public function __construct(AbstractExpression $expr, AbstractExpression $variables = null, $only = false, $ignoreMissing = false, $lineno, $tag = null) public function __construct(AbstractExpression $expr, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno, string $tag = null)
{ {
$nodes = ['expr' => $expr]; $nodes = ['expr' => $expr];
if (null !== $variables) { if (null !== $variables) {

View File

@ -21,9 +21,9 @@ use Twig\Error\SyntaxError;
*/ */
class MacroNode extends Node class MacroNode extends Node
{ {
const VARARGS_NAME = 'varargs'; public const VARARGS_NAME = 'varargs';
public function __construct($name, \Twig_NodeInterface $body, \Twig_NodeInterface $arguments, $lineno, $tag = null) public function __construct(string $name, Node $body, Node $arguments, int $lineno, string $tag = null)
{ {
foreach ($arguments as $argumentName => $argument) { foreach ($arguments as $argumentName => $argument) {
if (self::VARARGS_NAME === $argumentName) { if (self::VARARGS_NAME === $argumentName) {
@ -38,7 +38,7 @@ class MacroNode extends Node
{ {
$compiler $compiler
->addDebugInfo($this) ->addDebugInfo($this)
->write(sprintf('public function get%s(', $this->getAttribute('name'))) ->write(sprintf('public function macro_%s(', $this->getAttribute('name')))
; ;
$count = \count($this->getNode('arguments')); $count = \count($this->getNode('arguments'));
@ -54,21 +54,16 @@ class MacroNode extends Node
} }
} }
if (\PHP_VERSION_ID >= 50600) {
if ($count) { if ($count) {
$compiler->raw(', '); $compiler->raw(', ');
} }
$compiler->raw('...$__varargs__');
}
$compiler $compiler
->raw('...$__varargs__')
->raw(")\n") ->raw(")\n")
->write("{\n") ->write("{\n")
->indent() ->indent()
; ->write("\$macros = \$this->macros;\n")
$compiler
->write("\$context = \$this->env->mergeGlobals([\n") ->write("\$context = \$this->env->mergeGlobals([\n")
->indent() ->indent()
; ;
@ -88,19 +83,8 @@ class MacroNode extends Node
->raw(' => ') ->raw(' => ')
; ;
if (\PHP_VERSION_ID >= 50600) {
$compiler->raw("\$__varargs__,\n");
} else {
$compiler
->raw('func_num_args() > ')
->repr($count)
->raw(' ? array_slice(func_get_args(), ')
->repr($count)
->raw(") : [],\n")
;
}
$compiler $compiler
->raw("\$__varargs__,\n")
->outdent() ->outdent()
->write("]);\n\n") ->write("]);\n\n")
->write("\$blocks = [];\n\n") ->write("\$blocks = [];\n\n")
@ -114,19 +98,14 @@ class MacroNode extends Node
->write("try {\n") ->write("try {\n")
->indent() ->indent()
->subcompile($this->getNode('body')) ->subcompile($this->getNode('body'))
->raw("\n")
->write("return ('' === \$tmp = ob_get_contents()) ? '' : new Markup(\$tmp, \$this->env->getCharset());\n")
->outdent() ->outdent()
->write("} catch (\Exception \$e) {\n") ->write("} finally {\n")
->indent() ->indent()
->write("ob_end_clean();\n\n") ->write("ob_end_clean();\n")
->write("throw \$e;\n")
->outdent() ->outdent()
->write("} catch (\Throwable \$e) {\n") ->write("}\n")
->indent()
->write("ob_end_clean();\n\n")
->write("throw \$e;\n")
->outdent()
->write("}\n\n")
->write("return ('' === \$tmp = ob_get_clean()) ? '' : new Markup(\$tmp, \$this->env->getCharset());\n")
->outdent() ->outdent()
->write("}\n\n") ->write("}\n\n")
; ;

View File

@ -25,16 +25,15 @@ use Twig\Source;
* display_end, constructor_start, constructor_end, and class_end. * display_end, constructor_start, constructor_end, and class_end.
* *
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*
* @final since Twig 2.4.0
*/ */
class ModuleNode extends Node class ModuleNode extends Node
{ {
public function __construct(\Twig_NodeInterface $body, AbstractExpression $parent = null, \Twig_NodeInterface $blocks, \Twig_NodeInterface $macros, \Twig_NodeInterface $traits, $embeddedTemplates, $name, $source = '') public function __construct(Node $body, ?AbstractExpression $parent, Node $blocks, Node $macros, Node $traits, $embeddedTemplates, Source $source)
{ {
if (!$name instanceof Source) { if (__CLASS__ !== static::class) {
@trigger_error(sprintf('Passing a string as the $name argument of %s() is deprecated since version 1.27. Pass a \Twig\Source instance instead.', __METHOD__), E_USER_DEPRECATED); @trigger_error('Overriding '.__CLASS__.' is deprecated since Twig 2.4.0 and the class will be final in 3.0.', \E_USER_DEPRECATED);
$source = new Source($source, $name);
} else {
$source = $name;
} }
$nodes = [ $nodes = [
@ -54,16 +53,11 @@ class ModuleNode extends Node
// embedded templates are set as attributes so that they are only visited once by the visitors // embedded templates are set as attributes so that they are only visited once by the visitors
parent::__construct($nodes, [ parent::__construct($nodes, [
// source to be remove in 2.0
'source' => $source->getCode(),
// filename to be remove in 2.0 (use getTemplateName() instead)
'filename' => $source->getName(),
'index' => null, 'index' => null,
'embedded_templates' => $embeddedTemplates, 'embedded_templates' => $embeddedTemplates,
], 1); ], 1);
// populate the template name of all node children // populate the template name of all node children
$this->setTemplateName($source->getName());
$this->setSourceContext($source); $this->setSourceContext($source);
} }
@ -89,16 +83,7 @@ class ModuleNode extends Node
$this->compileClassHeader($compiler); $this->compileClassHeader($compiler);
if (
\count($this->getNode('blocks'))
|| \count($this->getNode('traits'))
|| !$this->hasNode('parent')
|| $this->getNode('parent') instanceof ConstantExpression
|| \count($this->getNode('constructor_start'))
|| \count($this->getNode('constructor_end'))
) {
$this->compileConstructor($compiler); $this->compileConstructor($compiler);
}
$this->compileGetParent($compiler); $this->compileGetParent($compiler);
@ -114,8 +99,6 @@ class ModuleNode extends Node
$this->compileDebugInfo($compiler); $this->compileDebugInfo($compiler);
$this->compileGetSource($compiler);
$this->compileGetSourceContext($compiler); $this->compileGetSourceContext($compiler);
$this->compileClassFooter($compiler); $this->compileClassFooter($compiler);
@ -166,6 +149,7 @@ class ModuleNode extends Node
->write("use Twig\Environment;\n") ->write("use Twig\Environment;\n")
->write("use Twig\Error\LoaderError;\n") ->write("use Twig\Error\LoaderError;\n")
->write("use Twig\Error\RuntimeError;\n") ->write("use Twig\Error\RuntimeError;\n")
->write("use Twig\Extension\SandboxExtension;\n")
->write("use Twig\Markup;\n") ->write("use Twig\Markup;\n")
->write("use Twig\Sandbox\SecurityError;\n") ->write("use Twig\Sandbox\SecurityError;\n")
->write("use Twig\Sandbox\SecurityNotAllowedTagError;\n") ->write("use Twig\Sandbox\SecurityNotAllowedTagError;\n")
@ -179,9 +163,11 @@ class ModuleNode extends Node
// if the template name contains */, add a blank to avoid a PHP parse error // if the template name contains */, add a blank to avoid a PHP parse error
->write('/* '.str_replace('*/', '* /', $this->getSourceContext()->getName())." */\n") ->write('/* '.str_replace('*/', '* /', $this->getSourceContext()->getName())." */\n")
->write('class '.$compiler->getEnvironment()->getTemplateClass($this->getSourceContext()->getName(), $this->getAttribute('index'))) ->write('class '.$compiler->getEnvironment()->getTemplateClass($this->getSourceContext()->getName(), $this->getAttribute('index')))
->raw(sprintf(" extends %s\n", $compiler->getEnvironment()->getBaseTemplateClass())) ->raw(sprintf(" extends %s\n", $compiler->getEnvironment()->getBaseTemplateClass(false)))
->write("{\n") ->write("{\n")
->indent() ->indent()
->write("private \$source;\n")
->write("private \$macros = [];\n\n")
; ;
} }
@ -192,6 +178,7 @@ class ModuleNode extends Node
->indent() ->indent()
->subcompile($this->getNode('constructor_start')) ->subcompile($this->getNode('constructor_start'))
->write("parent::__construct(\$env);\n\n") ->write("parent::__construct(\$env);\n\n")
->write("\$this->source = \$this->getSourceContext();\n\n")
; ;
// parent // parent
@ -203,18 +190,24 @@ class ModuleNode extends Node
if ($countTraits) { if ($countTraits) {
// traits // traits
foreach ($this->getNode('traits') as $i => $trait) { foreach ($this->getNode('traits') as $i => $trait) {
$this->compileLoadTemplate($compiler, $trait->getNode('template'), sprintf('$_trait_%s', $i));
$node = $trait->getNode('template'); $node = $trait->getNode('template');
$compiler $compiler
->addDebugInfo($node) ->addDebugInfo($node)
->write(sprintf('$_trait_%s = $this->loadTemplate(', $i))
->subcompile($node)
->raw(', ')
->repr($node->getTemplateName())
->raw(', ')
->repr($node->getTemplateLine())
->raw(");\n")
->write(sprintf("if (!\$_trait_%s->isTraitable()) {\n", $i)) ->write(sprintf("if (!\$_trait_%s->isTraitable()) {\n", $i))
->indent() ->indent()
->write("throw new RuntimeError('Template \"'.") ->write("throw new RuntimeError('Template \"'.")
->subcompile($trait->getNode('template')) ->subcompile($trait->getNode('template'))
->raw(".'\" cannot be used as a trait.', ") ->raw(".'\" cannot be used as a trait.', ")
->repr($node->getTemplateLine()) ->repr($node->getTemplateLine())
->raw(", \$this->getSourceContext());\n") ->raw(", \$this->source);\n")
->outdent() ->outdent()
->write("}\n") ->write("}\n")
->write(sprintf("\$_trait_%s_blocks = \$_trait_%s->getBlocks();\n\n", $i, $i)) ->write(sprintf("\$_trait_%s_blocks = \$_trait_%s->getBlocks();\n\n", $i, $i))
@ -226,13 +219,13 @@ class ModuleNode extends Node
->string($key) ->string($key)
->raw("])) {\n") ->raw("])) {\n")
->indent() ->indent()
->write("throw new RuntimeError(sprintf('Block ") ->write("throw new RuntimeError('Block ")
->string($key) ->string($key)
->raw(' is not defined in trait ') ->raw(' is not defined in trait ')
->subcompile($trait->getNode('template')) ->subcompile($trait->getNode('template'))
->raw(".'), ") ->raw(".', ")
->repr($node->getTemplateLine()) ->repr($node->getTemplateLine())
->raw(", \$this->getSourceContext());\n") ->raw(", \$this->source);\n")
->outdent() ->outdent()
->write("}\n\n") ->write("}\n\n")
@ -318,6 +311,7 @@ class ModuleNode extends Node
$compiler $compiler
->write("protected function doDisplay(array \$context, array \$blocks = [])\n", "{\n") ->write("protected function doDisplay(array \$context, array \$blocks = [])\n", "{\n")
->indent() ->indent()
->write("\$macros = \$this->macros;\n")
->subcompile($this->getNode('display_start')) ->subcompile($this->getNode('display_start'))
->subcompile($this->getNode('body')) ->subcompile($this->getNode('body'))
; ;
@ -440,20 +434,6 @@ class ModuleNode extends Node
; ;
} }
protected function compileGetSource(Compiler $compiler)
{
$compiler
->write("/** @deprecated since 1.27 (to be removed in 2.0). Use getSourceContext() instead */\n")
->write("public function getSource()\n", "{\n")
->indent()
->write("@trigger_error('The '.__METHOD__.' method is deprecated since version 1.27 and will be removed in 2.0. Use getSourceContext() instead.', E_USER_DEPRECATED);\n\n")
->write('return $this->getSourceContext()->getCode();')
->raw("\n")
->outdent()
->write("}\n\n")
;
}
protected function compileGetSourceContext(Compiler $compiler) protected function compileGetSourceContext(Compiler $compiler)
{ {
$compiler $compiler

View File

@ -20,7 +20,7 @@ use Twig\Source;
* *
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*/ */
class Node implements \Twig_NodeInterface class Node implements \Countable, \IteratorAggregate
{ {
protected $nodes; protected $nodes;
protected $attributes; protected $attributes;
@ -36,11 +36,11 @@ class Node implements \Twig_NodeInterface
* @param int $lineno The line number * @param int $lineno The line number
* @param string $tag The tag name associated with the Node * @param string $tag The tag name associated with the Node
*/ */
public function __construct(array $nodes = [], array $attributes = [], $lineno = 0, $tag = null) public function __construct(array $nodes = [], array $attributes = [], int $lineno = 0, string $tag = null)
{ {
foreach ($nodes as $name => $node) { foreach ($nodes as $name => $node) {
if (!$node instanceof \Twig_NodeInterface) { if (!$node instanceof self) {
@trigger_error(sprintf('Using "%s" for the value of node "%s" of "%s" is deprecated since version 1.25 and will be removed in 2.0.', \is_object($node) ? \get_class($node) : (null === $node ? 'null' : \gettype($node)), $name, \get_class($this)), E_USER_DEPRECATED); throw new \InvalidArgumentException(sprintf('Using "%s" for the value of node "%s" of "%s" is not supported. You must pass a \Twig\Node\Node instance.', \is_object($node) ? \get_class($node) : (null === $node ? 'null' : \gettype($node)), $name, static::class));
} }
} }
$this->nodes = $nodes; $this->nodes = $nodes;
@ -56,7 +56,7 @@ class Node implements \Twig_NodeInterface
$attributes[] = sprintf('%s: %s', $name, str_replace("\n", '', var_export($value, true))); $attributes[] = sprintf('%s: %s', $name, str_replace("\n", '', var_export($value, true)));
} }
$repr = [\get_class($this).'('.implode(', ', $attributes)]; $repr = [static::class.'('.implode(', ', $attributes)];
if (\count($this->nodes)) { if (\count($this->nodes)) {
foreach ($this->nodes as $name => $node) { foreach ($this->nodes as $name => $node) {
@ -77,41 +77,6 @@ class Node implements \Twig_NodeInterface
return implode("\n", $repr); return implode("\n", $repr);
} }
/**
* @deprecated since 1.16.1 (to be removed in 2.0)
*/
public function toXml($asDom = false)
{
@trigger_error(sprintf('%s is deprecated since version 1.16.1 and will be removed in 2.0.', __METHOD__), E_USER_DEPRECATED);
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = true;
$dom->appendChild($xml = $dom->createElement('twig'));
$xml->appendChild($node = $dom->createElement('node'));
$node->setAttribute('class', \get_class($this));
foreach ($this->attributes as $name => $value) {
$node->appendChild($attribute = $dom->createElement('attribute'));
$attribute->setAttribute('name', $name);
$attribute->appendChild($dom->createTextNode($value));
}
foreach ($this->nodes as $name => $n) {
if (null === $n) {
continue;
}
$child = $n->toXml(true)->getElementsByTagName('node')->item(0);
$child = $dom->importNode($child, true);
$child->setAttribute('name', $name);
$node->appendChild($child);
}
return $asDom ? $dom : $dom->saveXML();
}
public function compile(Compiler $compiler) public function compile(Compiler $compiler)
{ {
foreach ($this->nodes as $node) { foreach ($this->nodes as $node) {
@ -124,16 +89,6 @@ class Node implements \Twig_NodeInterface
return $this->lineno; return $this->lineno;
} }
/**
* @deprecated since 1.27 (to be removed in 2.0)
*/
public function getLine()
{
@trigger_error('The '.__METHOD__.' method is deprecated since version 1.27 and will be removed in 2.0. Use getTemplateLine() instead.', E_USER_DEPRECATED);
return $this->lineno;
}
public function getNodeTag() public function getNodeTag()
{ {
return $this->tag; return $this->tag;
@ -153,7 +108,7 @@ class Node implements \Twig_NodeInterface
public function getAttribute($name) public function getAttribute($name)
{ {
if (!\array_key_exists($name, $this->attributes)) { if (!\array_key_exists($name, $this->attributes)) {
throw new \LogicException(sprintf('Attribute "%s" does not exist for Node "%s".', $name, \get_class($this))); throw new \LogicException(sprintf('Attribute "%s" does not exist for Node "%s".', $name, static::class));
} }
return $this->attributes[$name]; return $this->attributes[$name];
@ -178,7 +133,7 @@ class Node implements \Twig_NodeInterface
*/ */
public function hasNode($name) public function hasNode($name)
{ {
return \array_key_exists($name, $this->nodes); return isset($this->nodes[$name]);
} }
/** /**
@ -186,19 +141,15 @@ class Node implements \Twig_NodeInterface
*/ */
public function getNode($name) public function getNode($name)
{ {
if (!\array_key_exists($name, $this->nodes)) { if (!isset($this->nodes[$name])) {
throw new \LogicException(sprintf('Node "%s" does not exist for Node "%s".', $name, \get_class($this))); throw new \LogicException(sprintf('Node "%s" does not exist for Node "%s".', $name, static::class));
} }
return $this->nodes[$name]; return $this->nodes[$name];
} }
public function setNode($name, $node = null) public function setNode($name, self $node)
{ {
if (!$node instanceof \Twig_NodeInterface) {
@trigger_error(sprintf('Using "%s" for the value of node "%s" of "%s" is deprecated since version 1.25 and will be removed in 2.0.', \is_object($node) ? \get_class($node) : (null === $node ? 'null' : \gettype($node)), $name, \get_class($this)), E_USER_DEPRECATED);
}
$this->nodes[$name] = $node; $this->nodes[$name] = $node;
} }
@ -207,65 +158,59 @@ class Node implements \Twig_NodeInterface
unset($this->nodes[$name]); unset($this->nodes[$name]);
} }
/**
* @return int
*/
#[\ReturnTypeWillChange]
public function count() public function count()
{ {
return \count($this->nodes); return \count($this->nodes);
} }
/**
* @return \Traversable
*/
#[\ReturnTypeWillChange]
public function getIterator() public function getIterator()
{ {
return new \ArrayIterator($this->nodes); return new \ArrayIterator($this->nodes);
} }
public function setTemplateName($name) /**
* @deprecated since 2.8 (to be removed in 3.0)
*/
public function setTemplateName($name/*, $triggerDeprecation = true */)
{ {
$triggerDeprecation = 2 > \func_num_args() || \func_get_arg(1);
if ($triggerDeprecation) {
@trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0. Use setSourceContext() instead.', \E_USER_DEPRECATED);
}
$this->name = $name; $this->name = $name;
foreach ($this->nodes as $node) { foreach ($this->nodes as $node) {
if (null !== $node) { $node->setTemplateName($name, $triggerDeprecation);
$node->setTemplateName($name);
}
} }
} }
public function getTemplateName() public function getTemplateName()
{ {
return $this->name; return $this->sourceContext ? $this->sourceContext->getName() : null;
} }
public function setSourceContext(Source $source) public function setSourceContext(Source $source)
{ {
$this->sourceContext = $source; $this->sourceContext = $source;
foreach ($this->nodes as $node) { foreach ($this->nodes as $node) {
if ($node instanceof Node) {
$node->setSourceContext($source); $node->setSourceContext($source);
} }
}
$this->setTemplateName($source->getName(), false);
} }
public function getSourceContext() public function getSourceContext()
{ {
return $this->sourceContext; return $this->sourceContext;
} }
/**
* @deprecated since 1.27 (to be removed in 2.0)
*/
public function setFilename($name)
{
@trigger_error('The '.__METHOD__.' method is deprecated since version 1.27 and will be removed in 2.0. Use setTemplateName() instead.', E_USER_DEPRECATED);
$this->setTemplateName($name);
}
/**
* @deprecated since 1.27 (to be removed in 2.0)
*/
public function getFilename()
{
@trigger_error('The '.__METHOD__.' method is deprecated since version 1.27 and will be removed in 2.0. Use getTemplateName() instead.', E_USER_DEPRECATED);
return $this->name;
}
} }
class_alias('Twig\Node\Node', 'Twig_Node'); class_alias('Twig\Node\Node', 'Twig_Node');

View File

@ -22,7 +22,7 @@ use Twig\Node\Expression\AbstractExpression;
*/ */
class PrintNode extends Node implements NodeOutputInterface class PrintNode extends Node implements NodeOutputInterface
{ {
public function __construct(AbstractExpression $expr, $lineno, $tag = null) public function __construct(AbstractExpression $expr, int $lineno, string $tag = null)
{ {
parent::__construct(['expr' => $expr], [], $lineno, $tag); parent::__construct(['expr' => $expr], [], $lineno, $tag);
} }

View File

@ -20,7 +20,7 @@ use Twig\Compiler;
*/ */
class SandboxNode extends Node class SandboxNode extends Node
{ {
public function __construct(\Twig_NodeInterface $body, $lineno, $tag = null) public function __construct(Node $body, int $lineno, string $tag = null)
{ {
parent::__construct(['body' => $body], [], $lineno, $tag); parent::__construct(['body' => $body], [], $lineno, $tag);
} }
@ -34,12 +34,19 @@ class SandboxNode extends Node
->write("\$this->sandbox->enableSandbox();\n") ->write("\$this->sandbox->enableSandbox();\n")
->outdent() ->outdent()
->write("}\n") ->write("}\n")
->write("try {\n")
->indent()
->subcompile($this->getNode('body')) ->subcompile($this->getNode('body'))
->outdent()
->write("} finally {\n")
->indent()
->write("if (!\$alreadySandboxed) {\n") ->write("if (!\$alreadySandboxed) {\n")
->indent() ->indent()
->write("\$this->sandbox->disableSandbox();\n") ->write("\$this->sandbox->disableSandbox();\n")
->outdent() ->outdent()
->write("}\n") ->write("}\n")
->outdent()
->write("}\n")
; ;
} }
} }

View File

@ -13,7 +13,6 @@ namespace Twig\Node;
use Twig\Compiler; use Twig\Compiler;
use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FilterExpression;
/** /**
* Adds a check for the __toString() method when the variable is an object and the sandbox is activated. * Adds a check for the __toString() method when the variable is an object and the sandbox is activated.
@ -42,28 +41,14 @@ class SandboxedPrintNode extends PrintNode
; ;
} else { } else {
$compiler $compiler
->write('$this->env->getExtension(\'\Twig\Extension\SandboxExtension\')->ensureToStringAllowed(') ->write('$this->extensions[SandboxExtension::class]->ensureToStringAllowed(')
->subcompile($expr) ->subcompile($expr)
->raw(");\n") ->raw(', ')
->repr($expr->getTemplateLine())
->raw(", \$this->source);\n")
; ;
} }
} }
/**
* Removes node filters.
*
* This is mostly needed when another visitor adds filters (like the escaper one).
*
* @return Node
*/
protected function removeNodeFilter(Node $node)
{
if ($node instanceof FilterExpression) {
return $this->removeNodeFilter($node->getNode('node'));
}
return $node;
}
} }
class_alias('Twig\Node\SandboxedPrintNode', 'Twig_Node_SandboxedPrint'); class_alias('Twig\Node\SandboxedPrintNode', 'Twig_Node_SandboxedPrint');

View File

@ -21,7 +21,7 @@ use Twig\Node\Expression\ConstantExpression;
*/ */
class SetNode extends Node implements NodeCaptureInterface class SetNode extends Node implements NodeCaptureInterface
{ {
public function __construct($capture, \Twig_NodeInterface $names, \Twig_NodeInterface $values, $lineno, $tag = null) public function __construct(bool $capture, Node $names, Node $values, int $lineno, string $tag = null)
{ {
parent::__construct(['names' => $names, 'values' => $values], ['capture' => $capture, 'safe' => false], $lineno, $tag); parent::__construct(['names' => $names, 'values' => $values], ['capture' => $capture, 'safe' => false], $lineno, $tag);

View File

@ -18,11 +18,13 @@ use Twig\Compiler;
* *
* It removes spaces between HTML tags. * It removes spaces between HTML tags.
* *
* @deprecated since Twig 2.7, to be removed in 3.0
*
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*/ */
class SpacelessNode extends Node class SpacelessNode extends Node implements NodeOutputInterface
{ {
public function __construct(\Twig_NodeInterface $body, $lineno, $tag = 'spaceless') public function __construct(Node $body, int $lineno, string $tag = 'spaceless')
{ {
parent::__construct(['body' => $body], [], $lineno, $tag); parent::__construct(['body' => $body], [], $lineno, $tag);
} }

View File

@ -21,7 +21,7 @@ use Twig\Compiler;
*/ */
class TextNode extends Node implements NodeOutputInterface class TextNode extends Node implements NodeOutputInterface
{ {
public function __construct($data, $lineno) public function __construct(string $data, int $lineno)
{ {
parent::__construct([], ['data' => $data], $lineno); parent::__construct([], ['data' => $data], $lineno);
} }

View File

@ -20,14 +20,14 @@ use Twig\Compiler;
*/ */
class WithNode extends Node class WithNode extends Node
{ {
public function __construct(Node $body, Node $variables = null, $only = false, $lineno, $tag = null) public function __construct(Node $body, ?Node $variables, bool $only, int $lineno, string $tag = null)
{ {
$nodes = ['body' => $body]; $nodes = ['body' => $body];
if (null !== $variables) { if (null !== $variables) {
$nodes['variables'] = $variables; $nodes['variables'] = $variables;
} }
parent::__construct($nodes, ['only' => (bool) $only], $lineno, $tag); parent::__construct($nodes, ['only' => $only], $lineno, $tag);
} }
public function compile(Compiler $compiler) public function compile(Compiler $compiler)

View File

@ -11,6 +11,7 @@
namespace Twig; namespace Twig;
use Twig\Node\Node;
use Twig\NodeVisitor\NodeVisitorInterface; use Twig\NodeVisitor\NodeVisitorInterface;
/** /**
@ -18,14 +19,12 @@ use Twig\NodeVisitor\NodeVisitorInterface;
* *
* It visits all nodes and their children and calls the given visitor for each. * It visits all nodes and their children and calls the given visitor for each.
* *
* @final
*
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*/ */
class NodeTraverser final class NodeTraverser
{ {
protected $env; private $env;
protected $visitors = []; private $visitors = [];
/** /**
* @param NodeVisitorInterface[] $visitors * @param NodeVisitorInterface[] $visitors
@ -45,10 +44,8 @@ class NodeTraverser
/** /**
* Traverses a node and calls the registered visitors. * Traverses a node and calls the registered visitors.
*
* @return \Twig_NodeInterface
*/ */
public function traverse(\Twig_NodeInterface $node) public function traverse(Node $node): Node
{ {
ksort($this->visitors); ksort($this->visitors);
foreach ($this->visitors as $visitors) { foreach ($this->visitors as $visitors) {
@ -60,24 +57,23 @@ class NodeTraverser
return $node; return $node;
} }
protected function traverseForVisitor(NodeVisitorInterface $visitor, \Twig_NodeInterface $node = null) /**
* @return Node|null
*/
private function traverseForVisitor(NodeVisitorInterface $visitor, Node $node)
{ {
if (null === $node) {
return;
}
$node = $visitor->enterNode($node, $this->env); $node = $visitor->enterNode($node, $this->env);
foreach ($node as $k => $n) { foreach ($node as $k => $n) {
if (null === $n) {
continue;
}
if (false !== ($m = $this->traverseForVisitor($visitor, $n)) && null !== $m) { if (false !== ($m = $this->traverseForVisitor($visitor, $n)) && null !== $m) {
if ($m !== $n) { if ($m !== $n) {
$node->setNode($k, $m); $node->setNode($k, $m);
} }
} else { } else {
if (false === $m) {
@trigger_error('Returning "false" to remove a Node from NodeVisitorInterface::leaveNode() is deprecated since Twig version 2.9; return "null" instead.', \E_USER_DEPRECATED);
}
$node->removeNode($k); $node->removeNode($k);
} }
} }

View File

@ -23,21 +23,13 @@ use Twig\Node\Node;
*/ */
abstract class AbstractNodeVisitor implements NodeVisitorInterface abstract class AbstractNodeVisitor implements NodeVisitorInterface
{ {
final public function enterNode(\Twig_NodeInterface $node, Environment $env) final public function enterNode(Node $node, Environment $env)
{ {
if (!$node instanceof Node) {
throw new \LogicException(sprintf('%s only supports \Twig\Node\Node instances.', __CLASS__));
}
return $this->doEnterNode($node, $env); return $this->doEnterNode($node, $env);
} }
final public function leaveNode(\Twig_NodeInterface $node, Environment $env) final public function leaveNode(Node $node, Environment $env)
{ {
if (!$node instanceof Node) {
throw new \LogicException(sprintf('%s only supports \Twig\Node\Node instances.', __CLASS__));
}
return $this->doLeaveNode($node, $env); return $this->doLeaveNode($node, $env);
} }
@ -51,7 +43,7 @@ abstract class AbstractNodeVisitor implements NodeVisitorInterface
/** /**
* Called after child nodes are visited. * Called after child nodes are visited.
* *
* @return Node|false|null The modified node or null if the node must be removed * @return Node|null The modified node or null if the node must be removed
*/ */
abstract protected function doLeaveNode(Node $node, Environment $env); abstract protected function doLeaveNode(Node $node, Environment $env);
} }

View File

@ -12,6 +12,7 @@
namespace Twig\NodeVisitor; namespace Twig\NodeVisitor;
use Twig\Environment; use Twig\Environment;
use Twig\Extension\EscaperExtension;
use Twig\Node\AutoEscapeNode; use Twig\Node\AutoEscapeNode;
use Twig\Node\BlockNode; use Twig\Node\BlockNode;
use Twig\Node\BlockReferenceNode; use Twig\Node\BlockReferenceNode;
@ -27,18 +28,16 @@ use Twig\Node\PrintNode;
use Twig\NodeTraverser; use Twig\NodeTraverser;
/** /**
* @final
*
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*/ */
class EscaperNodeVisitor extends AbstractNodeVisitor final class EscaperNodeVisitor extends AbstractNodeVisitor
{ {
protected $statusStack = []; private $statusStack = [];
protected $blocks = []; private $blocks = [];
protected $safeAnalysis; private $safeAnalysis;
protected $traverser; private $traverser;
protected $defaultStrategy = false; private $defaultStrategy = false;
protected $safeVars = []; private $safeVars = [];
public function __construct() public function __construct()
{ {
@ -48,7 +47,7 @@ class EscaperNodeVisitor extends AbstractNodeVisitor
protected function doEnterNode(Node $node, Environment $env) protected function doEnterNode(Node $node, Environment $env)
{ {
if ($node instanceof ModuleNode) { if ($node instanceof ModuleNode) {
if ($env->hasExtension('\Twig\Extension\EscaperExtension') && $defaultStrategy = $env->getExtension('\Twig\Extension\EscaperExtension')->getDefaultStrategy($node->getTemplateName())) { if ($env->hasExtension(EscaperExtension::class) && $defaultStrategy = $env->getExtension(EscaperExtension::class)->getDefaultStrategy($node->getTemplateName())) {
$this->defaultStrategy = $defaultStrategy; $this->defaultStrategy = $defaultStrategy;
} }
$this->safeVars = []; $this->safeVars = [];
@ -128,7 +127,7 @@ class EscaperNodeVisitor extends AbstractNodeVisitor
return new InlinePrint($this->getEscaperFilter($type, $expression), $node->getTemplateLine()); return new InlinePrint($this->getEscaperFilter($type, $expression), $node->getTemplateLine());
} }
protected function escapePrintNode(PrintNode $node, Environment $env, $type) private function escapePrintNode(PrintNode $node, Environment $env, $type)
{ {
if (false === $type) { if (false === $type) {
return $node; return $node;
@ -145,7 +144,7 @@ class EscaperNodeVisitor extends AbstractNodeVisitor
return new $class($this->getEscaperFilter($type, $expression), $node->getTemplateLine()); return new $class($this->getEscaperFilter($type, $expression), $node->getTemplateLine());
} }
protected function preEscapeFilterNode(FilterExpression $filter, Environment $env) private function preEscapeFilterNode(FilterExpression $filter, Environment $env)
{ {
$name = $filter->getNode('filter')->getAttribute('value'); $name = $filter->getNode('filter')->getAttribute('value');
@ -164,7 +163,7 @@ class EscaperNodeVisitor extends AbstractNodeVisitor
return $filter; return $filter;
} }
protected function isSafeFor($type, \Twig_NodeInterface $expression, $env) private function isSafeFor($type, Node $expression, $env)
{ {
$safe = $this->safeAnalysis->getSafe($expression); $safe = $this->safeAnalysis->getSafe($expression);
@ -182,7 +181,7 @@ class EscaperNodeVisitor extends AbstractNodeVisitor
return \in_array($type, $safe) || \in_array('all', $safe); return \in_array($type, $safe) || \in_array('all', $safe);
} }
protected function needEscaping(Environment $env) private function needEscaping(Environment $env)
{ {
if (\count($this->statusStack)) { if (\count($this->statusStack)) {
return $this->statusStack[\count($this->statusStack) - 1]; return $this->statusStack[\count($this->statusStack) - 1];
@ -191,7 +190,7 @@ class EscaperNodeVisitor extends AbstractNodeVisitor
return $this->defaultStrategy ? $this->defaultStrategy : false; return $this->defaultStrategy ? $this->defaultStrategy : false;
} }
protected function getEscaperFilter($type, \Twig_NodeInterface $node) private function getEscaperFilter(string $type, Node $node): FilterExpression
{ {
$line = $node->getTemplateLine(); $line = $node->getTemplateLine();
$name = new ConstantExpression('escape', $line); $name = new ConstantExpression('escape', $line);

View File

@ -0,0 +1,72 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\NodeVisitor;
use Twig\Environment;
use Twig\Node\Expression\AssignNameExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\MethodCallExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\ImportNode;
use Twig\Node\ModuleNode;
use Twig\Node\Node;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class MacroAutoImportNodeVisitor implements NodeVisitorInterface
{
private $inAModule = false;
private $hasMacroCalls = false;
public function enterNode(Node $node, Environment $env)
{
if ($node instanceof ModuleNode) {
$this->inAModule = true;
$this->hasMacroCalls = false;
}
return $node;
}
public function leaveNode(Node $node, Environment $env)
{
if ($node instanceof ModuleNode) {
$this->inAModule = false;
if ($this->hasMacroCalls) {
$node->getNode('constructor_end')->setNode('_auto_macro_import', new ImportNode(new NameExpression('_self', 0), new AssignNameExpression('_self', 0), 0, 'import', true));
}
} elseif ($this->inAModule) {
if (
$node instanceof GetAttrExpression &&
$node->getNode('node') instanceof NameExpression &&
'_self' === $node->getNode('node')->getAttribute('name') &&
$node->getNode('attribute') instanceof ConstantExpression
) {
$this->hasMacroCalls = true;
$name = $node->getNode('attribute')->getAttribute('value');
$node = new MethodCallExpression($node->getNode('node'), 'macro_'.$name, $node->getNode('arguments'), $node->getTemplateLine());
$node->setAttribute('safe', true);
}
}
return $node;
}
public function getPriority()
{
// we must be ran before auto-escaping
return -10;
}
}

View File

@ -12,6 +12,7 @@
namespace Twig\NodeVisitor; namespace Twig\NodeVisitor;
use Twig\Environment; use Twig\Environment;
use Twig\Node\Node;
/** /**
* Interface for node visitor classes. * Interface for node visitor classes.
@ -23,16 +24,16 @@ interface NodeVisitorInterface
/** /**
* Called before child nodes are visited. * Called before child nodes are visited.
* *
* @return \Twig_NodeInterface The modified node * @return Node The modified node
*/ */
public function enterNode(\Twig_NodeInterface $node, Environment $env); public function enterNode(Node $node, Environment $env);
/** /**
* Called after child nodes are visited. * Called after child nodes are visited.
* *
* @return \Twig_NodeInterface|false|null The modified node or null if the node must be removed * @return Node|null The modified node or null if the node must be removed
*/ */
public function leaveNode(\Twig_NodeInterface $node, Environment $env); public function leaveNode(Node $node, Environment $env);
/** /**
* Returns the priority for this visitor. * Returns the priority for this visitor.

View File

@ -13,8 +13,6 @@ namespace Twig\NodeVisitor;
use Twig\Environment; use Twig\Environment;
use Twig\Node\BlockReferenceNode; use Twig\Node\BlockReferenceNode;
use Twig\Node\BodyNode;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\BlockReferenceExpression; use Twig\Node\Expression\BlockReferenceExpression;
use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FilterExpression; use Twig\Node\Expression\FilterExpression;
@ -22,12 +20,10 @@ use Twig\Node\Expression\FunctionExpression;
use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\NameExpression;
use Twig\Node\Expression\ParentExpression; use Twig\Node\Expression\ParentExpression;
use Twig\Node\Expression\TempNameExpression;
use Twig\Node\ForNode; use Twig\Node\ForNode;
use Twig\Node\IncludeNode; use Twig\Node\IncludeNode;
use Twig\Node\Node; use Twig\Node\Node;
use Twig\Node\PrintNode; use Twig\Node\PrintNode;
use Twig\Node\SetTempNode;
/** /**
* Tries to optimize the AST. * Tries to optimize the AST.
@ -37,28 +33,25 @@ use Twig\Node\SetTempNode;
* You can configure which optimizations you want to activate via the * You can configure which optimizations you want to activate via the
* optimizer mode. * optimizer mode.
* *
* @final
*
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*/ */
class OptimizerNodeVisitor extends AbstractNodeVisitor final class OptimizerNodeVisitor extends AbstractNodeVisitor
{ {
const OPTIMIZE_ALL = -1; public const OPTIMIZE_ALL = -1;
const OPTIMIZE_NONE = 0; public const OPTIMIZE_NONE = 0;
const OPTIMIZE_FOR = 2; public const OPTIMIZE_FOR = 2;
const OPTIMIZE_RAW_FILTER = 4; public const OPTIMIZE_RAW_FILTER = 4;
const OPTIMIZE_VAR_ACCESS = 8; // obsolete, does not do anything
public const OPTIMIZE_VAR_ACCESS = 8;
protected $loops = []; private $loops = [];
protected $loopsTargets = []; private $loopsTargets = [];
protected $optimizers; private $optimizers;
protected $prependedNodes = [];
protected $inABody = false;
/** /**
* @param int $optimizers The optimizer mode * @param int $optimizers The optimizer mode
*/ */
public function __construct($optimizers = -1) public function __construct(int $optimizers = -1)
{ {
if (!\is_int($optimizers) || $optimizers > (self::OPTIMIZE_FOR | self::OPTIMIZE_RAW_FILTER | self::OPTIMIZE_VAR_ACCESS)) { if (!\is_int($optimizers) || $optimizers > (self::OPTIMIZE_FOR | self::OPTIMIZE_RAW_FILTER | self::OPTIMIZE_VAR_ACCESS)) {
throw new \InvalidArgumentException(sprintf('Optimizer mode "%s" is not valid.', $optimizers)); throw new \InvalidArgumentException(sprintf('Optimizer mode "%s" is not valid.', $optimizers));
@ -73,27 +66,11 @@ class OptimizerNodeVisitor extends AbstractNodeVisitor
$this->enterOptimizeFor($node, $env); $this->enterOptimizeFor($node, $env);
} }
if (\PHP_VERSION_ID < 50400 && self::OPTIMIZE_VAR_ACCESS === (self::OPTIMIZE_VAR_ACCESS & $this->optimizers) && !$env->isStrictVariables() && !$env->hasExtension('\Twig\Extension\SandboxExtension')) {
if ($this->inABody) {
if (!$node instanceof AbstractExpression) {
if ('Twig_Node' !== \get_class($node)) {
array_unshift($this->prependedNodes, []);
}
} else {
$node = $this->optimizeVariables($node, $env);
}
} elseif ($node instanceof BodyNode) {
$this->inABody = true;
}
}
return $node; return $node;
} }
protected function doLeaveNode(Node $node, Environment $env) protected function doLeaveNode(Node $node, Environment $env)
{ {
$expression = $node instanceof AbstractExpression;
if (self::OPTIMIZE_FOR === (self::OPTIMIZE_FOR & $this->optimizers)) { if (self::OPTIMIZE_FOR === (self::OPTIMIZE_FOR & $this->optimizers)) {
$this->leaveOptimizeFor($node, $env); $this->leaveOptimizeFor($node, $env);
} }
@ -104,33 +81,6 @@ class OptimizerNodeVisitor extends AbstractNodeVisitor
$node = $this->optimizePrintNode($node, $env); $node = $this->optimizePrintNode($node, $env);
if (self::OPTIMIZE_VAR_ACCESS === (self::OPTIMIZE_VAR_ACCESS & $this->optimizers) && !$env->isStrictVariables() && !$env->hasExtension('\Twig\Extension\SandboxExtension')) {
if ($node instanceof BodyNode) {
$this->inABody = false;
} elseif ($this->inABody) {
if (!$expression && 'Twig_Node' !== \get_class($node) && $prependedNodes = array_shift($this->prependedNodes)) {
$nodes = [];
foreach (array_unique($prependedNodes) as $name) {
$nodes[] = new SetTempNode($name, $node->getTemplateLine());
}
$nodes[] = $node;
$node = new Node($nodes);
}
}
}
return $node;
}
protected function optimizeVariables(\Twig_NodeInterface $node, Environment $env)
{
if ('Twig_Node_Expression_Name' === \get_class($node) && $node->isSimple()) {
$this->prependedNodes[0][] = $node->getAttribute('name');
return new TempNameExpression($node->getAttribute('name'), $node->getTemplateLine());
}
return $node; return $node;
} }
@ -140,10 +90,8 @@ class OptimizerNodeVisitor extends AbstractNodeVisitor
* It replaces: * It replaces:
* *
* * "echo $this->render(Parent)Block()" with "$this->display(Parent)Block()" * * "echo $this->render(Parent)Block()" with "$this->display(Parent)Block()"
*
* @return \Twig_NodeInterface
*/ */
protected function optimizePrintNode(\Twig_NodeInterface $node, Environment $env) private function optimizePrintNode(Node $node, Environment $env): Node
{ {
if (!$node instanceof PrintNode) { if (!$node instanceof PrintNode) {
return $node; return $node;
@ -164,10 +112,8 @@ class OptimizerNodeVisitor extends AbstractNodeVisitor
/** /**
* Removes "raw" filters. * Removes "raw" filters.
*
* @return \Twig_NodeInterface
*/ */
protected function optimizeRawFilter(\Twig_NodeInterface $node, Environment $env) private function optimizeRawFilter(Node $node, Environment $env): Node
{ {
if ($node instanceof FilterExpression && 'raw' == $node->getNode('filter')->getAttribute('value')) { if ($node instanceof FilterExpression && 'raw' == $node->getNode('filter')->getAttribute('value')) {
return $node->getNode('node'); return $node->getNode('node');
@ -179,7 +125,7 @@ class OptimizerNodeVisitor extends AbstractNodeVisitor
/** /**
* Optimizes "for" tag by removing the "loop" variable creation whenever possible. * Optimizes "for" tag by removing the "loop" variable creation whenever possible.
*/ */
protected function enterOptimizeFor(\Twig_NodeInterface $node, Environment $env) private function enterOptimizeFor(Node $node, Environment $env)
{ {
if ($node instanceof ForNode) { if ($node instanceof ForNode) {
// disable the loop variable by default // disable the loop variable by default
@ -243,7 +189,7 @@ class OptimizerNodeVisitor extends AbstractNodeVisitor
/** /**
* Optimizes "for" tag by removing the "loop" variable creation whenever possible. * Optimizes "for" tag by removing the "loop" variable creation whenever possible.
*/ */
protected function leaveOptimizeFor(\Twig_NodeInterface $node, Environment $env) private function leaveOptimizeFor(Node $node, Environment $env)
{ {
if ($node instanceof ForNode) { if ($node instanceof ForNode) {
array_shift($this->loops); array_shift($this->loops);
@ -252,12 +198,12 @@ class OptimizerNodeVisitor extends AbstractNodeVisitor
} }
} }
protected function addLoopToCurrent() private function addLoopToCurrent()
{ {
$this->loops[0]->setAttribute('with_loop', true); $this->loops[0]->setAttribute('with_loop', true);
} }
protected function addLoopToAll() private function addLoopToAll()
{ {
foreach ($this->loops as $loop) { foreach ($this->loops as $loop) {
$loop->setAttribute('with_loop', true); $loop->setAttribute('with_loop', true);

View File

@ -23,20 +23,17 @@ use Twig\Node\Expression\NameExpression;
use Twig\Node\Expression\ParentExpression; use Twig\Node\Expression\ParentExpression;
use Twig\Node\Node; use Twig\Node\Node;
/** final class SafeAnalysisNodeVisitor extends AbstractNodeVisitor
* @final
*/
class SafeAnalysisNodeVisitor extends AbstractNodeVisitor
{ {
protected $data = []; private $data = [];
protected $safeVars = []; private $safeVars = [];
public function setSafeVars($safeVars) public function setSafeVars($safeVars)
{ {
$this->safeVars = $safeVars; $this->safeVars = $safeVars;
} }
public function getSafe(\Twig_NodeInterface $node) public function getSafe(Node $node)
{ {
$hash = spl_object_hash($node); $hash = spl_object_hash($node);
if (!isset($this->data[$hash])) { if (!isset($this->data[$hash])) {
@ -56,7 +53,7 @@ class SafeAnalysisNodeVisitor extends AbstractNodeVisitor
} }
} }
protected function setSafe(\Twig_NodeInterface $node, array $safe) private function setSafe(Node $node, array $safe)
{ {
$hash = spl_object_hash($node); $hash = spl_object_hash($node);
if (isset($this->data[$hash])) { if (isset($this->data[$hash])) {
@ -125,8 +122,7 @@ class SafeAnalysisNodeVisitor extends AbstractNodeVisitor
} }
} elseif ($node instanceof GetAttrExpression && $node->getNode('node') instanceof NameExpression) { } elseif ($node instanceof GetAttrExpression && $node->getNode('node') instanceof NameExpression) {
$name = $node->getNode('node')->getAttribute('name'); $name = $node->getNode('node')->getAttribute('name');
// attributes on template instances are safe if (\in_array($name, $this->safeVars)) {
if ('_self' == $name || \in_array($name, $this->safeVars)) {
$this->setSafe($node, ['all']); $this->setSafe($node, ['all']);
} else { } else {
$this->setSafe($node, []); $this->setSafe($node, []);
@ -138,7 +134,7 @@ class SafeAnalysisNodeVisitor extends AbstractNodeVisitor
return $node; return $node;
} }
protected function intersectSafe(array $a = null, array $b = null) private function intersectSafe(array $a = null, array $b = null): array
{ {
if (null === $a || null === $b) { if (null === $a || null === $b) {
return []; return [];

View File

@ -12,6 +12,7 @@
namespace Twig\NodeVisitor; namespace Twig\NodeVisitor;
use Twig\Environment; use Twig\Environment;
use Twig\Node\CheckSecurityCallNode;
use Twig\Node\CheckSecurityNode; use Twig\Node\CheckSecurityNode;
use Twig\Node\CheckToStringNode; use Twig\Node\CheckToStringNode;
use Twig\Node\Expression\Binary\ConcatBinary; use Twig\Node\Expression\Binary\ConcatBinary;
@ -26,16 +27,14 @@ use Twig\Node\PrintNode;
use Twig\Node\SetNode; use Twig\Node\SetNode;
/** /**
* @final
*
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*/ */
class SandboxNodeVisitor extends AbstractNodeVisitor final class SandboxNodeVisitor extends AbstractNodeVisitor
{ {
protected $inAModule = false; private $inAModule = false;
protected $tags; private $tags;
protected $filters; private $filters;
protected $functions; private $functions;
private $needsToStringWrap = false; private $needsToStringWrap = false;
@ -102,7 +101,8 @@ class SandboxNodeVisitor extends AbstractNodeVisitor
if ($node instanceof ModuleNode) { if ($node instanceof ModuleNode) {
$this->inAModule = false; $this->inAModule = false;
$node->getNode('constructor_end')->setNode('_security_check', new Node([new CheckSecurityNode($this->filters, $this->tags, $this->functions), $node->getNode('display_start')])); $node->setNode('constructor_end', new Node([new CheckSecurityCallNode(), $node->getNode('constructor_end')]));
$node->setNode('class_end', new Node([new CheckSecurityNode($this->filters, $this->tags, $this->functions), $node->getNode('class_end')]));
} elseif ($this->inAModule) { } elseif ($this->inAModule) {
if ($node instanceof PrintNode || $node instanceof SetNode) { if ($node instanceof PrintNode || $node instanceof SetNode) {
$this->needsToStringWrap = false; $this->needsToStringWrap = false;
@ -112,7 +112,7 @@ class SandboxNodeVisitor extends AbstractNodeVisitor
return $node; return $node;
} }
private function wrapNode(Node $node, $name) private function wrapNode(Node $node, string $name)
{ {
$expr = $node->getNode($name); $expr = $node->getNode($name);
if ($expr instanceof NameExpression || $expr instanceof GetAttrExpression) { if ($expr instanceof NameExpression || $expr instanceof GetAttrExpression) {
@ -120,7 +120,7 @@ class SandboxNodeVisitor extends AbstractNodeVisitor
} }
} }
private function wrapArrayNode(Node $node, $name) private function wrapArrayNode(Node $node, string $name)
{ {
$args = $node->getNode($name); $args = $node->getNode($name);
foreach ($args as $name => $_) { foreach ($args as $name => $_) {

View File

@ -23,8 +23,8 @@ use Twig\Node\Node;
use Twig\Node\NodeCaptureInterface; use Twig\Node\NodeCaptureInterface;
use Twig\Node\NodeOutputInterface; use Twig\Node\NodeOutputInterface;
use Twig\Node\PrintNode; use Twig\Node\PrintNode;
use Twig\Node\SpacelessNode;
use Twig\Node\TextNode; use Twig\Node\TextNode;
use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\TokenParser\TokenParserInterface; use Twig\TokenParser\TokenParserInterface;
/** /**
@ -32,22 +32,21 @@ use Twig\TokenParser\TokenParserInterface;
* *
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*/ */
class Parser implements \Twig_ParserInterface class Parser
{ {
protected $stack = []; private $stack = [];
protected $stream; private $stream;
protected $parent; private $parent;
protected $handlers; private $handlers;
protected $visitors; private $visitors;
protected $expressionParser; private $expressionParser;
protected $blocks; private $blocks;
protected $blockStack; private $blockStack;
protected $macros; private $macros;
protected $env; private $env;
protected $reservedMacroNames; private $importedSymbols;
protected $importedSymbols; private $traits;
protected $traits; private $embeddedTemplates = [];
protected $embeddedTemplates = [];
private $varNameSalt = 0; private $varNameSalt = 0;
public function __construct(Environment $env) public function __construct(Environment $env)
@ -55,48 +54,25 @@ class Parser implements \Twig_ParserInterface
$this->env = $env; $this->env = $env;
} }
/**
* @deprecated since 1.27 (to be removed in 2.0)
*/
public function getEnvironment()
{
@trigger_error('The '.__METHOD__.' method is deprecated since version 1.27 and will be removed in 2.0.', E_USER_DEPRECATED);
return $this->env;
}
public function getVarName() public function getVarName()
{ {
return sprintf('__internal_%s', hash('sha256', __METHOD__.$this->stream->getSourceContext()->getCode().$this->varNameSalt++)); return sprintf('__internal_parse_%d', $this->varNameSalt++);
}
/**
* @deprecated since 1.27 (to be removed in 2.0). Use $parser->getStream()->getSourceContext()->getPath() instead.
*/
public function getFilename()
{
@trigger_error(sprintf('The "%s" method is deprecated since version 1.27 and will be removed in 2.0. Use $parser->getStream()->getSourceContext()->getPath() instead.', __METHOD__), E_USER_DEPRECATED);
return $this->stream->getSourceContext()->getName();
} }
public function parse(TokenStream $stream, $test = null, $dropNeedle = false) public function parse(TokenStream $stream, $test = null, $dropNeedle = false)
{ {
// push all variables into the stack to keep the current state of the parser $vars = get_object_vars($this);
// using get_object_vars() instead of foreach would lead to https://bugs.php.net/71336 unset($vars['stack'], $vars['env'], $vars['handlers'], $vars['visitors'], $vars['expressionParser'], $vars['reservedMacroNames'], $vars['varNameSalt']);
// This hack can be removed when min version if PHP 7.0
$vars = [];
foreach ($this as $k => $v) {
$vars[$k] = $v;
}
unset($vars['stack'], $vars['env'], $vars['handlers'], $vars['visitors'], $vars['expressionParser'], $vars['reservedMacroNames']);
$this->stack[] = $vars; $this->stack[] = $vars;
// tag handlers // tag handlers
if (null === $this->handlers) { if (null === $this->handlers) {
$this->handlers = $this->env->getTokenParsers(); $this->handlers = [];
$this->handlers->setParser($this); foreach ($this->env->getTokenParsers() as $handler) {
$handler->setParser($this);
$this->handlers[$handler->getTag()] = $handler;
}
} }
// node visitors // node visitors
@ -116,7 +92,6 @@ class Parser implements \Twig_ParserInterface
$this->blockStack = []; $this->blockStack = [];
$this->importedSymbols = [[]]; $this->importedSymbols = [[]];
$this->embeddedTemplates = []; $this->embeddedTemplates = [];
$this->varNameSalt = 0;
try { try {
$body = $this->subparse($test, $dropNeedle); $body = $this->subparse($test, $dropNeedle);
@ -156,27 +131,27 @@ class Parser implements \Twig_ParserInterface
$rv = []; $rv = [];
while (!$this->stream->isEOF()) { while (!$this->stream->isEOF()) {
switch ($this->getCurrentToken()->getType()) { switch ($this->getCurrentToken()->getType()) {
case Token::TEXT_TYPE: case /* Token::TEXT_TYPE */ 0:
$token = $this->stream->next(); $token = $this->stream->next();
$rv[] = new TextNode($token->getValue(), $token->getLine()); $rv[] = new TextNode($token->getValue(), $token->getLine());
break; break;
case Token::VAR_START_TYPE: case /* Token::VAR_START_TYPE */ 2:
$token = $this->stream->next(); $token = $this->stream->next();
$expr = $this->expressionParser->parseExpression(); $expr = $this->expressionParser->parseExpression();
$this->stream->expect(Token::VAR_END_TYPE); $this->stream->expect(/* Token::VAR_END_TYPE */ 4);
$rv[] = new PrintNode($expr, $token->getLine()); $rv[] = new PrintNode($expr, $token->getLine());
break; break;
case Token::BLOCK_START_TYPE: case /* Token::BLOCK_START_TYPE */ 1:
$this->stream->next(); $this->stream->next();
$token = $this->getCurrentToken(); $token = $this->getCurrentToken();
if (Token::NAME_TYPE !== $token->getType()) { if (/* Token::NAME_TYPE */ 5 !== $token->getType()) {
throw new SyntaxError('A block must start with a tag name.', $token->getLine(), $this->stream->getSourceContext()); throw new SyntaxError('A block must start with a tag name.', $token->getLine(), $this->stream->getSourceContext());
} }
if (null !== $test && \call_user_func($test, $token)) { if (null !== $test && $test($token)) {
if ($dropNeedle) { if ($dropNeedle) {
$this->stream->next(); $this->stream->next();
} }
@ -188,8 +163,7 @@ class Parser implements \Twig_ParserInterface
return new Node($rv, [], $lineno); return new Node($rv, [], $lineno);
} }
$subparser = $this->handlers->getTokenParser($token->getValue()); if (!isset($this->handlers[$token->getValue()])) {
if (null === $subparser) {
if (null !== $test) { if (null !== $test) {
$e = new SyntaxError(sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext()); $e = new SyntaxError(sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext());
@ -206,6 +180,7 @@ class Parser implements \Twig_ParserInterface
$this->stream->next(); $this->stream->next();
$subparser = $this->handlers[$token->getValue()];
$node = $subparser->parse($token); $node = $subparser->parse($token);
if (null !== $node) { if (null !== $node) {
$rv[] = $node; $rv[] = $node;
@ -224,26 +199,6 @@ class Parser implements \Twig_ParserInterface
return new Node($rv, [], $lineno); return new Node($rv, [], $lineno);
} }
/**
* @deprecated since 1.27 (to be removed in 2.0)
*/
public function addHandler($name, $class)
{
@trigger_error('The '.__METHOD__.' method is deprecated since version 1.27 and will be removed in 2.0.', E_USER_DEPRECATED);
$this->handlers[$name] = $class;
}
/**
* @deprecated since 1.27 (to be removed in 2.0)
*/
public function addNodeVisitor(NodeVisitorInterface $visitor)
{
@trigger_error('The '.__METHOD__.' method is deprecated since version 1.27 and will be removed in 2.0.', E_USER_DEPRECATED);
$this->visitors[] = $visitor;
}
public function getBlockStack() public function getBlockStack()
{ {
return $this->blockStack; return $this->blockStack;
@ -286,28 +241,17 @@ class Parser implements \Twig_ParserInterface
public function setMacro($name, MacroNode $node) public function setMacro($name, MacroNode $node)
{ {
if ($this->isReservedMacroName($name)) {
throw new SyntaxError(sprintf('"%s" cannot be used as a macro name as it is a reserved keyword.', $name), $node->getTemplateLine(), $this->stream->getSourceContext());
}
$this->macros[$name] = $node; $this->macros[$name] = $node;
} }
/**
* @deprecated since Twig 2.7 as there are no reserved macro names anymore, will be removed in 3.0.
*/
public function isReservedMacroName($name) public function isReservedMacroName($name)
{ {
if (null === $this->reservedMacroNames) { @trigger_error(sprintf('The "%s" method is deprecated since Twig 2.7 and will be removed in 3.0.', __METHOD__), \E_USER_DEPRECATED);
$this->reservedMacroNames = [];
$r = new \ReflectionClass($this->env->getBaseTemplateClass());
foreach ($r->getMethods() as $method) {
$methodName = strtolower($method->getName());
if ('get' === substr($methodName, 0, 3) && isset($methodName[3])) { return false;
$this->reservedMacroNames[] = substr($methodName, 3);
}
}
}
return \in_array(strtolower($name), $this->reservedMacroNames);
} }
public function addTrait($trait) public function addTrait($trait)
@ -334,19 +278,8 @@ class Parser implements \Twig_ParserInterface
public function getImportedSymbol($type, $alias) public function getImportedSymbol($type, $alias)
{ {
if (null !== $this->peekBlockStack()) { // if the symbol does not exist in the current scope (0), try in the main/global scope (last index)
foreach ($this->importedSymbols as $functions) { return $this->importedSymbols[0][$type][$alias] ?? ($this->importedSymbols[\count($this->importedSymbols) - 1][$type][$alias] ?? null);
if (isset($functions[$type][$alias])) {
if (\count($this->blockStack) > 1) {
return null;
}
return $functions[$type][$alias];
}
}
} else {
return isset($this->importedSymbols[0][$type][$alias]) ? $this->importedSymbols[0][$type][$alias] : null;
}
} }
public function isMainScope() public function isMainScope()
@ -398,13 +331,14 @@ class Parser implements \Twig_ParserInterface
return $this->stream->getCurrent(); return $this->stream->getCurrent();
} }
protected function filterBodyNodes(\Twig_NodeInterface $node) private function filterBodyNodes(Node $node, bool $nested = false)
{ {
// check that the body does not contain non-empty output nodes // check that the body does not contain non-empty output nodes
if ( if (
($node instanceof TextNode && !ctype_space($node->getAttribute('data'))) ($node instanceof TextNode && !ctype_space($node->getAttribute('data')))
|| ||
(!$node instanceof TextNode && !$node instanceof BlockReferenceNode && $node instanceof NodeOutputInterface) // the "&& !$node instanceof SpacelessNode" part of the condition must be removed in 3.0
(!$node instanceof TextNode && !$node instanceof BlockReferenceNode && ($node instanceof NodeOutputInterface && !$node instanceof SpacelessNode))
) { ) {
if (false !== strpos((string) $node, \chr(0xEF).\chr(0xBB).\chr(0xBF))) { if (false !== strpos((string) $node, \chr(0xEF).\chr(0xBB).\chr(0xBF))) {
$t = substr($node->getAttribute('data'), 3); $t = substr($node->getAttribute('data'), 3);
@ -417,17 +351,37 @@ class Parser implements \Twig_ParserInterface
throw new SyntaxError('A template that extends another one cannot include content outside Twig blocks. Did you forget to put the content inside a {% block %} tag?', $node->getTemplateLine(), $this->stream->getSourceContext()); throw new SyntaxError('A template that extends another one cannot include content outside Twig blocks. Did you forget to put the content inside a {% block %} tag?', $node->getTemplateLine(), $this->stream->getSourceContext());
} }
// bypass nodes that will "capture" the output // bypass nodes that "capture" the output
if ($node instanceof NodeCaptureInterface) { if ($node instanceof NodeCaptureInterface) {
// a "block" tag in such a node will serve as a block definition AND be displayed in place as well
return $node; return $node;
} }
if ($node instanceof NodeOutputInterface) { // to be removed completely in Twig 3.0
if (!$nested && $node instanceof SpacelessNode) {
@trigger_error(sprintf('Using the spaceless tag at the root level of a child template in "%s" at line %d is deprecated since Twig 2.5.0 and will become a syntax error in 3.0.', $this->stream->getSourceContext()->getName(), $node->getTemplateLine()), \E_USER_DEPRECATED);
}
// "block" tags that are not captured (see above) are only used for defining
// the content of the block. In such a case, nesting it does not work as
// expected as the definition is not part of the default template code flow.
if ($nested && ($node instanceof BlockReferenceNode || $node instanceof \Twig_Node_BlockReference)) {
//throw new SyntaxError('A block definition cannot be nested under non-capturing nodes.', $node->getTemplateLine(), $this->stream->getSourceContext());
@trigger_error(sprintf('Nesting a block definition under a non-capturing node in "%s" at line %d is deprecated since Twig 2.5.0 and will become a syntax error in 3.0.', $this->stream->getSourceContext()->getName(), $node->getTemplateLine()), \E_USER_DEPRECATED);
return; return;
} }
// the "&& !$node instanceof SpacelessNode" part of the condition must be removed in 3.0
if ($node instanceof NodeOutputInterface && !$node instanceof SpacelessNode) {
return;
}
// here, $nested means "being at the root level of a child template"
// we need to discard the wrapping "Twig_Node" for the "body" node
$nested = $nested || ('Twig_Node' !== \get_class($node) && Node::class !== \get_class($node));
foreach ($node as $k => $n) { foreach ($node as $k => $n) {
if (null !== $n && null === $this->filterBodyNodes($n)) { if (null !== $n && null === $this->filterBodyNodes($n, $nested)) {
$node->removeNode($k); $node->removeNode($k);
} }
} }

View File

@ -31,7 +31,7 @@ abstract class BaseDumper
abstract protected function formatTime(Profile $profile, $percent); abstract protected function formatTime(Profile $profile, $percent);
private function dumpProfile(Profile $profile, $prefix = '', $sibling = false) private function dumpProfile(Profile $profile, $prefix = '', $sibling = false): string
{ {
if ($profile->isRoot()) { if ($profile->isRoot()) {
$this->root = $profile->getDuration(); $this->root = $profile->getDuration();

View File

@ -15,10 +15,8 @@ use Twig\Profiler\Profile;
/** /**
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/ */
class BlackfireDumper final class BlackfireDumper
{ {
public function dump(Profile $profile) public function dump(Profile $profile)
{ {
@ -42,7 +40,7 @@ EOF;
return $str; return $str;
} }
private function dumpChildren($parent, Profile $profile, &$data) private function dumpChildren(string $parent, Profile $profile, &$data)
{ {
foreach ($profile as $p) { foreach ($profile as $p) {
if ($p->isTemplate()) { if ($p->isTemplate()) {
@ -55,7 +53,7 @@ EOF;
} }
} }
private function dumpProfile($edge, Profile $profile, &$data) private function dumpProfile(string $edge, Profile $profile, &$data)
{ {
if (isset($data[$edge])) { if (isset($data[$edge])) {
++$data[$edge]['ct']; ++$data[$edge]['ct'];

View File

@ -15,10 +15,8 @@ use Twig\Profiler\Profile;
/** /**
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/ */
class HtmlDumper extends BaseDumper final class HtmlDumper extends BaseDumper
{ {
private static $colors = [ private static $colors = [
'block' => '#dfd', 'block' => '#dfd',

View File

@ -15,10 +15,8 @@ use Twig\Profiler\Profile;
/** /**
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/ */
class TextDumper extends BaseDumper final class TextDumper extends BaseDumper
{ {
protected function formatTemplate(Profile $profile, $prefix) protected function formatTemplate(Profile $profile, $prefix)
{ {

View File

@ -21,7 +21,7 @@ use Twig\Node\Node;
*/ */
class EnterProfileNode extends Node class EnterProfileNode extends Node
{ {
public function __construct($extensionName, $type, $name, $varName) public function __construct(string $extensionName, string $type, string $name, string $varName)
{ {
parent::__construct([], ['extension_name' => $extensionName, 'name' => $name, 'type' => $type, 'var_name' => $varName]); parent::__construct([], ['extension_name' => $extensionName, 'name' => $name, 'type' => $type, 'var_name' => $varName]);
} }
@ -29,9 +29,9 @@ class EnterProfileNode extends Node
public function compile(Compiler $compiler) public function compile(Compiler $compiler)
{ {
$compiler $compiler
->write(sprintf('$%s = $this->env->getExtension(', $this->getAttribute('var_name'))) ->write(sprintf('$%s = $this->extensions[', $this->getAttribute('var_name')))
->repr($this->getAttribute('extension_name')) ->repr($this->getAttribute('extension_name'))
->raw(");\n") ->raw("];\n")
->write(sprintf('$%s->enter($%s = new \Twig\Profiler\Profile($this->getTemplateName(), ', $this->getAttribute('var_name'), $this->getAttribute('var_name').'_prof')) ->write(sprintf('$%s->enter($%s = new \Twig\Profiler\Profile($this->getTemplateName(), ', $this->getAttribute('var_name'), $this->getAttribute('var_name').'_prof'))
->repr($this->getAttribute('type')) ->repr($this->getAttribute('type'))
->raw(', ') ->raw(', ')

View File

@ -21,7 +21,7 @@ use Twig\Node\Node;
*/ */
class LeaveProfileNode extends Node class LeaveProfileNode extends Node
{ {
public function __construct($varName) public function __construct(string $varName)
{ {
parent::__construct([], ['var_name' => $varName]); parent::__construct([], ['var_name' => $varName]);
} }

View File

@ -24,16 +24,16 @@ use Twig\Profiler\Profile;
/** /**
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/ */
class ProfilerNodeVisitor extends AbstractNodeVisitor final class ProfilerNodeVisitor extends AbstractNodeVisitor
{ {
private $extensionName; private $extensionName;
private $varName;
public function __construct($extensionName) public function __construct(string $extensionName)
{ {
$this->extensionName = $extensionName; $this->extensionName = $extensionName;
$this->varName = sprintf('__internal_%s', hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $extensionName));
} }
protected function doEnterNode(Node $node, Environment $env) protected function doEnterNode(Node $node, Environment $env)
@ -44,33 +44,25 @@ class ProfilerNodeVisitor extends AbstractNodeVisitor
protected function doLeaveNode(Node $node, Environment $env) protected function doLeaveNode(Node $node, Environment $env)
{ {
if ($node instanceof ModuleNode) { if ($node instanceof ModuleNode) {
$varName = $this->getVarName(); $node->setNode('display_start', new Node([new EnterProfileNode($this->extensionName, Profile::TEMPLATE, $node->getTemplateName(), $this->varName), $node->getNode('display_start')]));
$node->setNode('display_start', new Node([new EnterProfileNode($this->extensionName, Profile::TEMPLATE, $node->getTemplateName(), $varName), $node->getNode('display_start')])); $node->setNode('display_end', new Node([new LeaveProfileNode($this->varName), $node->getNode('display_end')]));
$node->setNode('display_end', new Node([new LeaveProfileNode($varName), $node->getNode('display_end')]));
} elseif ($node instanceof BlockNode) { } elseif ($node instanceof BlockNode) {
$varName = $this->getVarName();
$node->setNode('body', new BodyNode([ $node->setNode('body', new BodyNode([
new EnterProfileNode($this->extensionName, Profile::BLOCK, $node->getAttribute('name'), $varName), new EnterProfileNode($this->extensionName, Profile::BLOCK, $node->getAttribute('name'), $this->varName),
$node->getNode('body'), $node->getNode('body'),
new LeaveProfileNode($varName), new LeaveProfileNode($this->varName),
])); ]));
} elseif ($node instanceof MacroNode) { } elseif ($node instanceof MacroNode) {
$varName = $this->getVarName();
$node->setNode('body', new BodyNode([ $node->setNode('body', new BodyNode([
new EnterProfileNode($this->extensionName, Profile::MACRO, $node->getAttribute('name'), $varName), new EnterProfileNode($this->extensionName, Profile::MACRO, $node->getAttribute('name'), $this->varName),
$node->getNode('body'), $node->getNode('body'),
new LeaveProfileNode($varName), new LeaveProfileNode($this->varName),
])); ]));
} }
return $node; return $node;
} }
private function getVarName()
{
return sprintf('__internal_%s', hash('sha256', $this->extensionName));
}
public function getPriority() public function getPriority()
{ {
return 0; return 0;

View File

@ -14,14 +14,14 @@ namespace Twig\Profiler;
/** /**
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
* *
* @final * @final since Twig 2.4.0
*/ */
class Profile implements \IteratorAggregate, \Serializable class Profile implements \IteratorAggregate, \Serializable
{ {
const ROOT = 'ROOT'; public const ROOT = 'ROOT';
const BLOCK = 'block'; public const BLOCK = 'block';
const TEMPLATE = 'template'; public const TEMPLATE = 'template';
const MACRO = 'macro'; public const MACRO = 'macro';
private $template; private $template;
private $name; private $name;
@ -30,8 +30,12 @@ class Profile implements \IteratorAggregate, \Serializable
private $ends = []; private $ends = [];
private $profiles = []; private $profiles = [];
public function __construct($template = 'main', $type = self::ROOT, $name = 'main') public function __construct(string $template = 'main', string $type = self::ROOT, string $name = 'main')
{ {
if (__CLASS__ !== static::class) {
@trigger_error('Overriding '.__CLASS__.' is deprecated since Twig 2.4.0 and the class will be final in 3.0.', \E_USER_DEPRECATED);
}
$this->template = $template; $this->template = $template;
$this->type = $type; $this->type = $type;
$this->name = 0 === strpos($name, '__internal_') ? 'INTERNAL' : $name; $this->name = 0 === strpos($name, '__internal_') ? 'INTERNAL' : $name;
@ -153,17 +157,18 @@ class Profile implements \IteratorAggregate, \Serializable
$this->enter(); $this->enter();
} }
public function getIterator() #[\ReturnTypeWillChange]
public function getIterator(): \Traversable
{ {
return new \ArrayIterator($this->profiles); return new \ArrayIterator($this->profiles);
} }
public function serialize() public function serialize(): string
{ {
return serialize($this->__serialize()); return serialize($this->__serialize());
} }
public function unserialize($data) public function unserialize($data): void
{ {
$this->__unserialize(unserialize($data)); $this->__unserialize(unserialize($data));
} }
@ -171,7 +176,7 @@ class Profile implements \IteratorAggregate, \Serializable
/** /**
* @internal * @internal
*/ */
public function __serialize() public function __serialize(): array
{ {
return [$this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles]; return [$this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles];
} }
@ -179,7 +184,7 @@ class Profile implements \IteratorAggregate, \Serializable
/** /**
* @internal * @internal
*/ */
public function __unserialize(array $data) public function __unserialize(array $data): void
{ {
list($this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles) = $data; list($this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles) = $data;
} }

View File

@ -23,7 +23,7 @@ class FactoryRuntimeLoader implements RuntimeLoaderInterface
/** /**
* @param array $map An array where keys are class names and values factory callables * @param array $map An array where keys are class names and values factory callables
*/ */
public function __construct($map = []) public function __construct(array $map = [])
{ {
$this->map = $map; $this->map = $map;
} }

View File

@ -15,13 +15,24 @@ namespace Twig\Sandbox;
* Exception thrown when a not allowed filter is used in a template. * Exception thrown when a not allowed filter is used in a template.
* *
* @author Martin Hasoň <martin.hason@gmail.com> * @author Martin Hasoň <martin.hason@gmail.com>
*
* @final
*/ */
class SecurityNotAllowedFilterError extends SecurityError class SecurityNotAllowedFilterError extends SecurityError
{ {
private $filterName; private $filterName;
public function __construct($message, $functionName, $lineno = -1, $filename = null, \Exception $previous = null) public function __construct(string $message, string $functionName, int $lineno = -1, string $filename = null, \Exception $previous = null)
{ {
if (-1 !== $lineno) {
@trigger_error(sprintf('Passing $lineno as a 3th argument of the %s constructor is deprecated since Twig 2.8.1.', __CLASS__), \E_USER_DEPRECATED);
}
if (null !== $filename) {
@trigger_error(sprintf('Passing $filename as a 4th argument of the %s constructor is deprecated since Twig 2.8.1.', __CLASS__), \E_USER_DEPRECATED);
}
if (null !== $previous) {
@trigger_error(sprintf('Passing $previous as a 5th argument of the %s constructor is deprecated since Twig 2.8.1.', __CLASS__), \E_USER_DEPRECATED);
}
parent::__construct($message, $lineno, $filename, $previous); parent::__construct($message, $lineno, $filename, $previous);
$this->filterName = $functionName; $this->filterName = $functionName;
} }

View File

@ -15,13 +15,24 @@ namespace Twig\Sandbox;
* Exception thrown when a not allowed function is used in a template. * Exception thrown when a not allowed function is used in a template.
* *
* @author Martin Hasoň <martin.hason@gmail.com> * @author Martin Hasoň <martin.hason@gmail.com>
*
* @final
*/ */
class SecurityNotAllowedFunctionError extends SecurityError class SecurityNotAllowedFunctionError extends SecurityError
{ {
private $functionName; private $functionName;
public function __construct($message, $functionName, $lineno = -1, $filename = null, \Exception $previous = null) public function __construct(string $message, string $functionName, int $lineno = -1, string $filename = null, \Exception $previous = null)
{ {
if (-1 !== $lineno) {
@trigger_error(sprintf('Passing $lineno as a 3th argument of the %s constructor is deprecated since Twig 2.8.1.', __CLASS__), \E_USER_DEPRECATED);
}
if (null !== $filename) {
@trigger_error(sprintf('Passing $filename as a 4th argument of the %s constructor is deprecated since Twig 2.8.1.', __CLASS__), \E_USER_DEPRECATED);
}
if (null !== $previous) {
@trigger_error(sprintf('Passing $previous as a 5th argument of the %s constructor is deprecated since Twig 2.8.1.', __CLASS__), \E_USER_DEPRECATED);
}
parent::__construct($message, $lineno, $filename, $previous); parent::__construct($message, $lineno, $filename, $previous);
$this->functionName = $functionName; $this->functionName = $functionName;
} }

View File

@ -15,14 +15,25 @@ namespace Twig\Sandbox;
* Exception thrown when a not allowed class method is used in a template. * Exception thrown when a not allowed class method is used in a template.
* *
* @author Kit Burton-Senior <mail@kitbs.com> * @author Kit Burton-Senior <mail@kitbs.com>
*
* @final
*/ */
class SecurityNotAllowedMethodError extends SecurityError class SecurityNotAllowedMethodError extends SecurityError
{ {
private $className; private $className;
private $methodName; private $methodName;
public function __construct($message, $className, $methodName, $lineno = -1, $filename = null, \Exception $previous = null) public function __construct(string $message, string $className, string $methodName, int $lineno = -1, string $filename = null, \Exception $previous = null)
{ {
if (-1 !== $lineno) {
@trigger_error(sprintf('Passing $lineno as a 3th argument of the %s constructor is deprecated since Twig 2.8.1.', __CLASS__), \E_USER_DEPRECATED);
}
if (null !== $filename) {
@trigger_error(sprintf('Passing $filename as a 4th argument of the %s constructor is deprecated since Twig 2.8.1.', __CLASS__), \E_USER_DEPRECATED);
}
if (null !== $previous) {
@trigger_error(sprintf('Passing $previous as a 5th argument of the %s constructor is deprecated since Twig 2.8.1.', __CLASS__), \E_USER_DEPRECATED);
}
parent::__construct($message, $lineno, $filename, $previous); parent::__construct($message, $lineno, $filename, $previous);
$this->className = $className; $this->className = $className;
$this->methodName = $methodName; $this->methodName = $methodName;

View File

@ -15,14 +15,25 @@ namespace Twig\Sandbox;
* Exception thrown when a not allowed class property is used in a template. * Exception thrown when a not allowed class property is used in a template.
* *
* @author Kit Burton-Senior <mail@kitbs.com> * @author Kit Burton-Senior <mail@kitbs.com>
*
* @final
*/ */
class SecurityNotAllowedPropertyError extends SecurityError class SecurityNotAllowedPropertyError extends SecurityError
{ {
private $className; private $className;
private $propertyName; private $propertyName;
public function __construct($message, $className, $propertyName, $lineno = -1, $filename = null, \Exception $previous = null) public function __construct(string $message, string $className, string $propertyName, int $lineno = -1, string $filename = null, \Exception $previous = null)
{ {
if (-1 !== $lineno) {
@trigger_error(sprintf('Passing $lineno as a 3th argument of the %s constructor is deprecated since Twig 2.8.1.', __CLASS__), \E_USER_DEPRECATED);
}
if (null !== $filename) {
@trigger_error(sprintf('Passing $filename as a 4th argument of the %s constructor is deprecated since Twig 2.8.1.', __CLASS__), \E_USER_DEPRECATED);
}
if (null !== $previous) {
@trigger_error(sprintf('Passing $previous as a 5th argument of the %s constructor is deprecated since Twig 2.8.1.', __CLASS__), \E_USER_DEPRECATED);
}
parent::__construct($message, $lineno, $filename, $previous); parent::__construct($message, $lineno, $filename, $previous);
$this->className = $className; $this->className = $className;
$this->propertyName = $propertyName; $this->propertyName = $propertyName;

View File

@ -15,13 +15,24 @@ namespace Twig\Sandbox;
* Exception thrown when a not allowed tag is used in a template. * Exception thrown when a not allowed tag is used in a template.
* *
* @author Martin Hasoň <martin.hason@gmail.com> * @author Martin Hasoň <martin.hason@gmail.com>
*
* @final
*/ */
class SecurityNotAllowedTagError extends SecurityError class SecurityNotAllowedTagError extends SecurityError
{ {
private $tagName; private $tagName;
public function __construct($message, $tagName, $lineno = -1, $filename = null, \Exception $previous = null) public function __construct(string $message, string $tagName, int $lineno = -1, string $filename = null, \Exception $previous = null)
{ {
if (-1 !== $lineno) {
@trigger_error(sprintf('Passing $lineno as a 3th argument of the %s constructor is deprecated since Twig 2.8.1.', __CLASS__), \E_USER_DEPRECATED);
}
if (null !== $filename) {
@trigger_error(sprintf('Passing $filename as a 4th argument of the %s constructor is deprecated since Twig 2.8.1.', __CLASS__), \E_USER_DEPRECATED);
}
if (null !== $previous) {
@trigger_error(sprintf('Passing $previous as a 5th argument of the %s constructor is deprecated since Twig 2.8.1.', __CLASS__), \E_USER_DEPRECATED);
}
parent::__construct($message, $lineno, $filename, $previous); parent::__construct($message, $lineno, $filename, $previous);
$this->tagName = $tagName; $this->tagName = $tagName;
} }

Some files were not shown because too many files have changed in this diff Show More