From 2506be1794fb2fcad5b0a8183c32be1d14c4599a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 17 Oct 2012 19:29:45 +0200 Subject: [PATCH] optimized the way Twig exceptions are managed As guessing the template name and the line number of where an error occurred is quite expensive, this is now avoided as much as possible. --- CHANGELOG | 1 + lib/Twig/Compiler.php | 12 ++++++- lib/Twig/Environment.php | 4 +- lib/Twig/Error.php | 36 ++++++++++++++++++- lib/Twig/Error/Loader.php | 12 ++++++ lib/Twig/ExpressionParser.php | 14 ++++---- lib/Twig/Node.php | 4 +- lib/Twig/Node/Expression/Filter.php | 2 +- lib/Twig/Node/Expression/Function.php | 2 +- lib/Twig/Node/Expression/Test.php | 2 +- lib/Twig/Node/Expression/Test/Defined.php | 2 +- lib/Twig/Node/Sandbox.php | 13 ------- lib/Twig/Parser.php | 29 ++++++++++----- lib/Twig/Template.php | 21 +++++++++--- lib/Twig/Token.php | 4 +- lib/Twig/TokenParser/AutoEscape.php | 15 ++++---- lib/Twig/TokenParser/Block.php | 4 +- lib/Twig/TokenParser/Extends.php | 4 +- lib/Twig/TokenParser/If.php | 13 ++++--- lib/Twig/TokenParser/Macro.php | 13 ++++--- lib/Twig/TokenParser/Sandbox.php | 13 +++++++ lib/Twig/TokenParser/Set.php | 4 +- lib/Twig/TokenParser/Use.php | 5 +-- lib/Twig/TokenParserBroker.php | 4 +- lib/Twig/TokenStream.php | 4 +- test/Twig/Tests/Fixtures/tags/include/missing.test | 8 ++++ .../Fixtures/tags/include/missing_nested.test | 16 +++++++++ test/Twig/Tests/ParserTest.php | 10 +++--- 28 files changed, 186 insertions(+), 85 deletions(-) create mode 100644 test/Twig/Tests/Fixtures/tags/include/missing.test create mode 100644 test/Twig/Tests/Fixtures/tags/include/missing_nested.test diff --git a/CHANGELOG b/CHANGELOG index db781f8..614b362 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ * 1.11.0 (2012-XX-XX) + * optimized the way Twig exceptions are managed (to make them faster) * added Twig_ExistsLoaderInterface (implementing this interface in your loader make the chain loader much faster) * 1.10.3 (2012-10-19) diff --git a/lib/Twig/Compiler.php b/lib/Twig/Compiler.php index d03dfa0..786a75f 100644 --- a/lib/Twig/Compiler.php +++ b/lib/Twig/Compiler.php @@ -25,6 +25,7 @@ class Twig_Compiler implements Twig_CompilerInterface protected $debugInfo; protected $sourceOffset; protected $sourceLine; + protected $filename; /** * Constructor. @@ -37,6 +38,11 @@ class Twig_Compiler implements Twig_CompilerInterface $this->debugInfo = array(); } + public function getFilename() + { + return $this->filename; + } + /** * Returns the environment instance related to this compiler. * @@ -73,6 +79,10 @@ class Twig_Compiler implements Twig_CompilerInterface $this->sourceLine = 0; $this->indentation = $indentation; + if ($node instanceof Twig_Node_Module) { + $this->filename = $node->getAttribute('filename'); + } + $node->compile($this); return $this; @@ -246,7 +256,7 @@ class Twig_Compiler implements Twig_CompilerInterface { // can't outdent by more steps that the current indentation level if ($this->indentation < $step) { - throw new Twig_Error('Unable to call outdent() as the indentation would become negative'); + throw new LogicException('Unable to call outdent() as the indentation would become negative'); } $this->indentation -= $step; diff --git a/lib/Twig/Environment.php b/lib/Twig/Environment.php index 9763db8..64095e8 100644 --- a/lib/Twig/Environment.php +++ b/lib/Twig/Environment.php @@ -711,7 +711,7 @@ class Twig_Environment } elseif ($parser instanceof Twig_TokenParserBrokerInterface) { $this->parsers->addTokenParserBroker($parser); } else { - throw new Twig_Error_Runtime('getTokenParsers() must return an array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances'); + throw new LogicException('getTokenParsers() must return an array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances'); } } } @@ -1100,6 +1100,6 @@ class Twig_Environment } } - throw new Twig_Error_Runtime(sprintf('Failed to write cache file "%s".', $file)); + throw new RuntimeException(sprintf('Failed to write cache file "%s".', $file)); } } diff --git a/lib/Twig/Error.php b/lib/Twig/Error.php index 28de6a7..138151b 100644 --- a/lib/Twig/Error.php +++ b/lib/Twig/Error.php @@ -12,6 +12,23 @@ /** * Twig base exception. * + * This exception class and its children must only be used when + * an error occurs during the loading of a template, when a syntax error + * is detected in a template, or when rendering a template. Other + * errors must use regular PHP exception classes (like when the template + * cache directory is not writable for instance). + * + * To help debugging template issues, this class tracks the original template + * name and line where the error occurred. + * + * Whenever possible, you must set these information (original template name + * and line number) yourself by passing them to the constructor. If some or all + * these information are not available from where you throw the exception, then + * this class will guess them automatically (when the line number is set to -1 + * and/or the filename is set to null). As this is a costly operation, this + * can be disabled by passing false for both the filename and the line number + * when creating a new instance of this class. + * * @package twig * @author Fabien Potencier */ @@ -25,6 +42,15 @@ class Twig_Error extends Exception /** * Constructor. * + * Set both the line number and the filename to false to + * disable automatic guessing of the original template name + * and line number. + * + * Set the line number to -1 to enable its automatic guessing. + * Set the filename to null to enable its automatic guessing. + * + * By default, automatic guessing is enabled. + * * @param string $message The error message * @param integer $lineno The template line where the error occurred * @param string $filename The template file name where the error occurred @@ -105,6 +131,12 @@ class Twig_Error extends Exception $this->updateRepr(); } + public function guess() + { + $this->guessTemplateInfo(); + $this->updateRepr(); + } + /** * For PHP < 5.3.0, provides access to the getPrevious() method. * @@ -134,7 +166,7 @@ class Twig_Error extends Exception $dot = true; } - if (null !== $this->filename) { + if ($this->filename) { if (is_string($this->filename) || (is_object($this->filename) && method_exists($this->filename, '__toString'))) { $filename = sprintf('"%s"', $this->filename); } else { @@ -143,7 +175,7 @@ class Twig_Error extends Exception $this->message .= sprintf(' in %s', $filename); } - if ($this->lineno >= 0) { + if ($this->lineno && $this->lineno >= 0) { $this->message .= sprintf(' at line %d', $this->lineno); } diff --git a/lib/Twig/Error/Loader.php b/lib/Twig/Error/Loader.php index 418a776..7124974 100644 --- a/lib/Twig/Error/Loader.php +++ b/lib/Twig/Error/Loader.php @@ -12,9 +12,21 @@ /** * Exception thrown when an error occurs during template loading. * + * Automatic template information guessing is always turned off as + * if a template cannot be loaded, there is nothing to guess. + * However, when a template is loaded from another one, then, we need + * to find the current context and this is automatically done by + * Twig_Template::displayWithErrorHandling(). + * + * This strategy makes Twig_Environment::resolveTemplate() much faster. + * * @package twig * @author Fabien Potencier */ class Twig_Error_Loader extends Twig_Error { + public function __construct($message, $lineno = -1, $filename = null, Exception $previous = null) + { + parent::__construct($message, false, false, $previous); + } } diff --git a/lib/Twig/ExpressionParser.php b/lib/Twig/ExpressionParser.php index 7c1ce24..3309bb5 100644 --- a/lib/Twig/ExpressionParser.php +++ b/lib/Twig/ExpressionParser.php @@ -158,7 +158,7 @@ class Twig_ExpressionParser } elseif ($token->test(Twig_Token::PUNCTUATION_TYPE, '{')) { $node = $this->parseHashExpression(); } else { - throw new Twig_Error_Syntax(sprintf('Unexpected token "%s" of value "%s"', Twig_Token::typeToEnglish($token->getType(), $token->getLine()), $token->getValue()), $token->getLine()); + throw new Twig_Error_Syntax(sprintf('Unexpected token "%s" of value "%s"', Twig_Token::typeToEnglish($token->getType(), $token->getLine()), $token->getValue()), $token->getLine(), $this->parser->getFilename()); } } @@ -252,7 +252,7 @@ class Twig_ExpressionParser } else { $current = $stream->getCurrent(); - throw new Twig_Error_Syntax(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"', Twig_Token::typeToEnglish($current->getType(), $current->getLine()), $current->getValue()), $current->getLine()); + throw new Twig_Error_Syntax(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"', Twig_Token::typeToEnglish($current->getType(), $current->getLine()), $current->getValue()), $current->getLine(), $this->parser->getFilename()); } $stream->expect(Twig_Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)'); @@ -291,11 +291,11 @@ class Twig_ExpressionParser switch ($name) { case 'parent': if (!count($this->parser->getBlockStack())) { - throw new Twig_Error_Syntax('Calling "parent" outside a block is forbidden', $line); + throw new Twig_Error_Syntax('Calling "parent" outside a block is forbidden', $line, $this->parser->getFilename()); } if (!$this->parser->getParent() && !$this->parser->hasTraits()) { - throw new Twig_Error_Syntax('Calling "parent" on a template that does not extend nor "use" another template is forbidden', $line); + throw new Twig_Error_Syntax('Calling "parent" on a template that does not extend nor "use" another template is forbidden', $line, $this->parser->getFilename()); } return new Twig_Node_Expression_Parent($this->parser->peekBlockStack(), $line); @@ -303,7 +303,7 @@ class Twig_ExpressionParser return new Twig_Node_Expression_BlockReference($args->getNode(0), false, $line); case 'attribute': if (count($args) < 2) { - throw new Twig_Error_Syntax('The "attribute" function takes at least two arguments (the variable and the attributes)', $line); + throw new Twig_Error_Syntax('The "attribute" function takes at least two arguments (the variable and the attributes)', $line, $this->parser->getFilename()); } return new Twig_Node_Expression_GetAttr($args->getNode(0), $args->getNode(1), count($args) > 2 ? $args->getNode(2) : new Twig_Node_Expression_Array(array(), $line), Twig_TemplateInterface::ANY_CALL, $line); @@ -351,7 +351,7 @@ class Twig_ExpressionParser } } } else { - throw new Twig_Error_Syntax('Expected name or number', $lineno); + throw new Twig_Error_Syntax('Expected name or number', $lineno, $this->parser->getFilename()); } } else { $type = Twig_TemplateInterface::ARRAY_CALL; @@ -446,7 +446,7 @@ class Twig_ExpressionParser while (true) { $token = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE, null, 'Only variables can be assigned to'); if (in_array($token->getValue(), array('true', 'false', 'none'))) { - throw new Twig_Error_Syntax(sprintf('You cannot assign a value to "%s"', $token->getValue()), $token->getLine()); + throw new Twig_Error_Syntax(sprintf('You cannot assign a value to "%s"', $token->getValue()), $token->getLine(), $this->parser->getFilename()); } $targets[] = new Twig_Node_Expression_AssignName($token->getValue(), $token->getLine()); diff --git a/lib/Twig/Node.php b/lib/Twig/Node.php index 651ffc4..04f735c 100644 --- a/lib/Twig/Node.php +++ b/lib/Twig/Node.php @@ -139,7 +139,7 @@ class Twig_Node implements Twig_NodeInterface public function getAttribute($name) { if (!array_key_exists($name, $this->attributes)) { - throw new Twig_Error_Runtime(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, get_class($this))); } return $this->attributes[$name]; @@ -188,7 +188,7 @@ class Twig_Node implements Twig_NodeInterface public function getNode($name) { if (!array_key_exists($name, $this->nodes)) { - throw new Twig_Error_Runtime(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, get_class($this))); } return $this->nodes[$name]; diff --git a/lib/Twig/Node/Expression/Filter.php b/lib/Twig/Node/Expression/Filter.php index 8a0903a..eb9cd31 100644 --- a/lib/Twig/Node/Expression/Filter.php +++ b/lib/Twig/Node/Expression/Filter.php @@ -26,7 +26,7 @@ class Twig_Node_Expression_Filter extends Twig_Node_Expression $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives)); } - throw new Twig_Error_Syntax($message, $this->getLine()); + throw new Twig_Error_Syntax($message, $this->getLine(), $compiler->getFilename()); } $this->compileFilter($compiler, $filter); diff --git a/lib/Twig/Node/Expression/Function.php b/lib/Twig/Node/Expression/Function.php index 9342bb1..5640377 100644 --- a/lib/Twig/Node/Expression/Function.php +++ b/lib/Twig/Node/Expression/Function.php @@ -25,7 +25,7 @@ class Twig_Node_Expression_Function extends Twig_Node_Expression $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives)); } - throw new Twig_Error_Syntax($message, $this->getLine()); + throw new Twig_Error_Syntax($message, $this->getLine(), $compiler->getFilename()); } $compiler->raw($function->compile().'('); diff --git a/lib/Twig/Node/Expression/Test.php b/lib/Twig/Node/Expression/Test.php index 4e0b25e..076e39d 100644 --- a/lib/Twig/Node/Expression/Test.php +++ b/lib/Twig/Node/Expression/Test.php @@ -25,7 +25,7 @@ class Twig_Node_Expression_Test extends Twig_Node_Expression $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives)); } - throw new Twig_Error_Syntax($message, $this->getLine()); + throw new Twig_Error_Syntax($message, $this->getLine(), $compiler->getFilename()); } $name = $this->getAttribute('name'); diff --git a/lib/Twig/Node/Expression/Test/Defined.php b/lib/Twig/Node/Expression/Test/Defined.php index e7c6828..915e60a 100644 --- a/lib/Twig/Node/Expression/Test/Defined.php +++ b/lib/Twig/Node/Expression/Test/Defined.php @@ -35,7 +35,7 @@ class Twig_Node_Expression_Test_Defined extends Twig_Node_Expression_Test $this->changeIgnoreStrictCheck($node); } else { - throw new Twig_Error_Syntax('The "defined" test only works with simple variables', $this->getLine()); + throw new Twig_Error_Syntax('The "defined" test only works with simple variables', $this->getLine(), $compiler->getFilename()); } } diff --git a/lib/Twig/Node/Sandbox.php b/lib/Twig/Node/Sandbox.php index fbafd99..cbfcb41 100644 --- a/lib/Twig/Node/Sandbox.php +++ b/lib/Twig/Node/Sandbox.php @@ -20,19 +20,6 @@ class Twig_Node_Sandbox extends Twig_Node public function __construct(Twig_NodeInterface $body, $lineno, $tag = null) { parent::__construct(array('body' => $body), array(), $lineno, $tag); - - // in a sandbox tag, only include tags are allowed - if (!$this->getNode('body') instanceof Twig_Node_Include) { - foreach ($this->getNode('body') as $node) { - if ($node instanceof Twig_Node_Text && ctype_space($node->getAttribute('data'))) { - continue; - } - - if (!$node instanceof Twig_Node_Include) { - throw new Twig_Error_Syntax('Only "include" tags are allowed within a "sandbox" section', $node->getLine()); - } - } - } } /** diff --git a/lib/Twig/Parser.php b/lib/Twig/Parser.php index b2959e5..1179913 100644 --- a/lib/Twig/Parser.php +++ b/lib/Twig/Parser.php @@ -53,6 +53,11 @@ class Twig_Parser implements Twig_ParserInterface return sprintf('__internal_%s', hash('sha1', uniqid(mt_rand(), true), false)); } + public function getFilename() + { + return $this->stream->getFilename(); + } + /** * Converts a token stream to a node tree. * @@ -100,14 +105,18 @@ class Twig_Parser implements Twig_ParserInterface } } } catch (Twig_Error_Syntax $e) { - if (null === $e->getTemplateFile()) { - $e->setTemplateFile($this->stream->getFilename()); + if (!$e->getTemplateFile()) { + $e->setTemplateFile($this->getFilename()); + } + + if (!$e->getTemplateLine()) { + $e->setTemplateLine($this->stream->getCurrent()->getLine()); } throw $e; } - $node = new Twig_Node_Module(new Twig_Node_Body(array($body)), $this->parent, new Twig_Node($this->blocks), new Twig_Node($this->macros), new Twig_Node($this->traits), $this->embeddedTemplates, $this->stream->getFilename()); + $node = new Twig_Node_Module(new Twig_Node_Body(array($body)), $this->parent, new Twig_Node($this->blocks), new Twig_Node($this->macros), new Twig_Node($this->traits), $this->embeddedTemplates, $this->getFilename()); $traverser = new Twig_NodeTraverser($this->env, $this->visitors); @@ -144,7 +153,7 @@ class Twig_Parser implements Twig_ParserInterface $token = $this->getCurrentToken(); if ($token->getType() !== Twig_Token::NAME_TYPE) { - throw new Twig_Error_Syntax('A block must start with a tag name', $token->getLine(), $this->stream->getFilename()); + throw new Twig_Error_Syntax('A block must start with a tag name', $token->getLine(), $this->getFilename()); } if (null !== $test && call_user_func($test, $token)) { @@ -167,7 +176,7 @@ class Twig_Parser implements Twig_ParserInterface $error .= sprintf(' (expecting closing tag for the "%s" tag defined near line %s)', $test[0]->getTag(), $lineno); } - throw new Twig_Error_Syntax($error, $token->getLine(), $this->stream->getFilename()); + throw new Twig_Error_Syntax($error, $token->getLine(), $this->getFilename()); } $message = sprintf('Unknown tag name "%s"', $token->getValue()); @@ -175,7 +184,7 @@ class Twig_Parser implements Twig_ParserInterface $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives)); } - throw new Twig_Error_Syntax($message, $token->getLine(), $this->stream->getFilename()); + throw new Twig_Error_Syntax($message, $token->getLine(), $this->getFilename()); } $this->stream->next(); @@ -187,7 +196,7 @@ class Twig_Parser implements Twig_ParserInterface break; default: - throw new Twig_Error_Syntax('Lexer or parser ended up in unsupported state.', -1, $this->stream->getFilename()); + throw new Twig_Error_Syntax('Lexer or parser ended up in unsupported state.', 0, $this->getFilename()); } } @@ -259,7 +268,7 @@ class Twig_Parser implements Twig_ParserInterface } if (in_array($name, $this->reservedMacroNames)) { - throw new Twig_Error_Syntax(sprintf('"%s" cannot be used as a macro name as it is a reserved keyword', $name), $node->getLine()); + throw new Twig_Error_Syntax(sprintf('"%s" cannot be used as a macro name as it is a reserved keyword', $name), $node->getLine(), $this->getFilename()); } $this->macros[$name] = $node; @@ -360,10 +369,10 @@ class Twig_Parser implements Twig_ParserInterface (!$node instanceof Twig_Node_Text && !$node instanceof Twig_Node_BlockReference && $node instanceof Twig_NodeOutputInterface) ) { if (false !== strpos((string) $node, chr(0xEF).chr(0xBB).chr(0xBF))) { - throw new Twig_Error_Syntax('A template that extends another one cannot have a body but a byte order mark (BOM) has been detected; it must be removed.', $node->getLine(), $this->stream->getFilename()); + throw new Twig_Error_Syntax('A template that extends another one cannot have a body but a byte order mark (BOM) has been detected; it must be removed.', $node->getLine(), $this->getFilename()); } - throw new Twig_Error_Syntax('A template that extends another one cannot have a body.', $node->getLine(), $this->stream->getFilename()); + throw new Twig_Error_Syntax('A template that extends another one cannot have a body.', $node->getLine(), $this->getFilename()); } // bypass "set" nodes as they "capture" the output diff --git a/lib/Twig/Template.php b/lib/Twig/Template.php index 5a9d53c..039c8ca 100644 --- a/lib/Twig/Template.php +++ b/lib/Twig/Template.php @@ -264,6 +264,17 @@ abstract class Twig_Template implements Twig_TemplateInterface try { $this->doDisplay($context, $blocks); } catch (Twig_Error $e) { + if (!$e->getTemplateFile()) { + $e->setTemplateFile($this->getTemplateName()); + } + + // this is mostly useful for Twig_Error_Loader exceptions + // see Twig_Error_Loader + if (false === $e->getTemplateLine()) { + $e->setTemplateLine(-1); + $e->guess(); + } + throw $e; } catch (Exception $e) { throw new Twig_Error_Runtime(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, null, $e); @@ -350,11 +361,11 @@ abstract class Twig_Template implements Twig_TemplateInterface } if (is_object($object)) { - throw new Twig_Error_Runtime(sprintf('Key "%s" in object (with ArrayAccess) of type "%s" does not exist', $item, get_class($object))); + throw new Twig_Error_Runtime(sprintf('Key "%s" in object (with ArrayAccess) of type "%s" does not exist', $item, get_class($object)), -1, $this->getTemplateName()); } elseif (is_array($object)) { - throw new Twig_Error_Runtime(sprintf('Key "%s" for array with keys "%s" does not exist', $item, implode(', ', array_keys($object)))); + throw new Twig_Error_Runtime(sprintf('Key "%s" for array with keys "%s" does not exist', $item, implode(', ', array_keys($object))), -1, $this->getTemplateName()); } else { - throw new Twig_Error_Runtime(sprintf('Impossible to access a key ("%s") on a "%s" variable', $item, gettype($object))); + throw new Twig_Error_Runtime(sprintf('Impossible to access a key ("%s") on a "%s" variable', $item, gettype($object)), -1, $this->getTemplateName()); } } } @@ -368,7 +379,7 @@ abstract class Twig_Template implements Twig_TemplateInterface return null; } - throw new Twig_Error_Runtime(sprintf('Item "%s" for "%s" does not exist', $item, is_array($object) ? 'Array' : $object)); + throw new Twig_Error_Runtime(sprintf('Item "%s" for "%s" does not exist', $item, is_array($object) ? 'Array' : $object), -1, $this->getTemplateName()); } $class = get_class($object); @@ -411,7 +422,7 @@ abstract class Twig_Template implements Twig_TemplateInterface return null; } - throw new Twig_Error_Runtime(sprintf('Method "%s" for object "%s" does not exist', $item, get_class($object))); + throw new Twig_Error_Runtime(sprintf('Method "%s" for object "%s" does not exist', $item, get_class($object)), -1, $this->getTemplateName()); } if ($isDefinedTest) { diff --git a/lib/Twig/Token.php b/lib/Twig/Token.php index 918bb91..1232696 100644 --- a/lib/Twig/Token.php +++ b/lib/Twig/Token.php @@ -169,7 +169,7 @@ class Twig_Token $name = 'INTERPOLATION_END_TYPE'; break; default: - throw new Twig_Error_Syntax(sprintf('Token of type "%s" does not exist.', $type), $line); + throw new LogicException(sprintf('Token of type "%s" does not exist.', $type)); } return $short ? $name : 'Twig_Token::'.$name; @@ -213,7 +213,7 @@ class Twig_Token case self::INTERPOLATION_END_TYPE: return 'end of string interpolation'; default: - throw new Twig_Error_Syntax(sprintf('Token of type "%s" does not exist.', $type), $line); + throw new LogicException(sprintf('Token of type "%s" does not exist.', $type)); } } } diff --git a/lib/Twig/TokenParser/AutoEscape.php b/lib/Twig/TokenParser/AutoEscape.php index 0040845..2756028 100644 --- a/lib/Twig/TokenParser/AutoEscape.php +++ b/lib/Twig/TokenParser/AutoEscape.php @@ -39,13 +39,14 @@ class Twig_TokenParser_AutoEscape extends Twig_TokenParser public function parse(Twig_Token $token) { $lineno = $token->getLine(); + $stream = $this->parser->getStream(); - if ($this->parser->getStream()->test(Twig_Token::BLOCK_END_TYPE)) { + if ($stream->test(Twig_Token::BLOCK_END_TYPE)) { $value = 'html'; } else { $expr = $this->parser->getExpressionParser()->parseExpression(); if (!$expr instanceof Twig_Node_Expression_Constant) { - throw new Twig_Error_Syntax('An escaping strategy must be a string or a Boolean.', $lineno); + throw new Twig_Error_Syntax('An escaping strategy must be a string or a Boolean.', $stream->getCurrent()->getLine(), $stream->getFilename()); } $value = $expr->getAttribute('value'); @@ -55,18 +56,18 @@ class Twig_TokenParser_AutoEscape extends Twig_TokenParser $value = 'html'; } - if ($compat && $this->parser->getStream()->test(Twig_Token::NAME_TYPE)) { + if ($compat && $stream->test(Twig_Token::NAME_TYPE)) { if (false === $value) { - throw new Twig_Error_Syntax('Unexpected escaping strategy as you set autoescaping to false.', $lineno); + throw new Twig_Error_Syntax('Unexpected escaping strategy as you set autoescaping to false.', $stream->getCurrent()->getLine(), $stream->getFilename()); } - $value = $this->parser->getStream()->next()->getValue(); + $value = $stream->next()->getValue(); } } - $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + $stream->expect(Twig_Token::BLOCK_END_TYPE); $body = $this->parser->subparse(array($this, 'decideBlockEnd'), true); - $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + $stream->expect(Twig_Token::BLOCK_END_TYPE); return new Twig_Node_AutoEscape($value, $body, $lineno, $this->getTag()); } diff --git a/lib/Twig/TokenParser/Block.php b/lib/Twig/TokenParser/Block.php index 994078e..a2e017f 100644 --- a/lib/Twig/TokenParser/Block.php +++ b/lib/Twig/TokenParser/Block.php @@ -35,7 +35,7 @@ class Twig_TokenParser_Block extends Twig_TokenParser $stream = $this->parser->getStream(); $name = $stream->expect(Twig_Token::NAME_TYPE)->getValue(); if ($this->parser->hasBlock($name)) { - throw new Twig_Error_Syntax(sprintf("The block '$name' has already been defined line %d", $this->parser->getBlock($name)->getLine()), $lineno); + throw new Twig_Error_Syntax(sprintf("The block '$name' has already been defined line %d", $this->parser->getBlock($name)->getLine()), $stream->getCurrent()->getLine(), $stream->getFilename()); } $this->parser->setBlock($name, $block = new Twig_Node_Block($name, new Twig_Node(array()), $lineno)); $this->parser->pushLocalScope(); @@ -49,7 +49,7 @@ class Twig_TokenParser_Block extends Twig_TokenParser $value = $stream->next()->getValue(); if ($value != $name) { - throw new Twig_Error_Syntax(sprintf("Expected endblock for block '$name' (but %s given)", $value), $lineno); + throw new Twig_Error_Syntax(sprintf("Expected endblock for block '$name' (but %s given)", $value), $stream->getCurrent()->getLine(), $stream->getFilename()); } } } else { diff --git a/lib/Twig/TokenParser/Extends.php b/lib/Twig/TokenParser/Extends.php index 54f49ad..110bc8b 100644 --- a/lib/Twig/TokenParser/Extends.php +++ b/lib/Twig/TokenParser/Extends.php @@ -29,11 +29,11 @@ class Twig_TokenParser_Extends extends Twig_TokenParser public function parse(Twig_Token $token) { if (!$this->parser->isMainScope()) { - throw new Twig_Error_Syntax('Cannot extend from a block', $token->getLine()); + throw new Twig_Error_Syntax('Cannot extend from a block', $token->getLine(), $this->parser->getFilename()); } if (null !== $this->parser->getParent()) { - throw new Twig_Error_Syntax('Multiple extends tags are forbidden', $token->getLine()); + throw new Twig_Error_Syntax('Multiple extends tags are forbidden', $token->getLine(), $this->parser->getFilename()); } $this->parser->setParent($this->parser->getExpressionParser()->parseExpression()); diff --git a/lib/Twig/TokenParser/If.php b/lib/Twig/TokenParser/If.php index 1a694af..3d7d1f5 100644 --- a/lib/Twig/TokenParser/If.php +++ b/lib/Twig/TokenParser/If.php @@ -36,22 +36,23 @@ class Twig_TokenParser_If extends Twig_TokenParser { $lineno = $token->getLine(); $expr = $this->parser->getExpressionParser()->parseExpression(); - $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + $stream = $this->parser->getStream(); + $stream->expect(Twig_Token::BLOCK_END_TYPE); $body = $this->parser->subparse(array($this, 'decideIfFork')); $tests = array($expr, $body); $else = null; $end = false; while (!$end) { - switch ($this->parser->getStream()->next()->getValue()) { + switch ($stream->next()->getValue()) { case 'else': - $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + $stream->expect(Twig_Token::BLOCK_END_TYPE); $else = $this->parser->subparse(array($this, 'decideIfEnd')); break; case 'elseif': $expr = $this->parser->getExpressionParser()->parseExpression(); - $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + $stream->expect(Twig_Token::BLOCK_END_TYPE); $body = $this->parser->subparse(array($this, 'decideIfFork')); $tests[] = $expr; $tests[] = $body; @@ -62,11 +63,11 @@ class Twig_TokenParser_If extends Twig_TokenParser break; default: - throw new Twig_Error_Syntax(sprintf('Unexpected end of template. Twig was looking for the following tags "else", "elseif", or "endif" to close the "if" block started at line %d)', $lineno), -1); + throw new Twig_Error_Syntax(sprintf('Unexpected end of template. Twig was looking for the following tags "else", "elseif", or "endif" to close the "if" block started at line %d)', $lineno), $stream->getCurrent()->getLine(), $stream->getFilename()); } } - $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + $stream->expect(Twig_Token::BLOCK_END_TYPE); return new Twig_Node_If(new Twig_Node($tests), $else, $lineno, $this->getTag()); } diff --git a/lib/Twig/TokenParser/Macro.php b/lib/Twig/TokenParser/Macro.php index ffd5848..de10059 100644 --- a/lib/Twig/TokenParser/Macro.php +++ b/lib/Twig/TokenParser/Macro.php @@ -30,22 +30,23 @@ class Twig_TokenParser_Macro extends Twig_TokenParser public function parse(Twig_Token $token) { $lineno = $token->getLine(); - $name = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE)->getValue(); + $stream = $this->parser->getStream(); + $name = $stream->expect(Twig_Token::NAME_TYPE)->getValue(); $arguments = $this->parser->getExpressionParser()->parseArguments(); - $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + $stream->expect(Twig_Token::BLOCK_END_TYPE); $this->parser->pushLocalScope(); $body = $this->parser->subparse(array($this, 'decideBlockEnd'), true); - if ($this->parser->getStream()->test(Twig_Token::NAME_TYPE)) { - $value = $this->parser->getStream()->next()->getValue(); + if ($stream->test(Twig_Token::NAME_TYPE)) { + $value = $stream->next()->getValue(); if ($value != $name) { - throw new Twig_Error_Syntax(sprintf("Expected endmacro for macro '$name' (but %s given)", $value), $lineno); + throw new Twig_Error_Syntax(sprintf("Expected endmacro for macro '$name' (but %s given)", $value), $stream->getCurrent()->getLine(), $stream->getFilename()); } } $this->parser->popLocalScope(); - $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + $stream->expect(Twig_Token::BLOCK_END_TYPE); $this->parser->setMacro($name, new Twig_Node_Macro($name, new Twig_Node_Body(array($body)), $arguments, $lineno, $this->getTag())); diff --git a/lib/Twig/TokenParser/Sandbox.php b/lib/Twig/TokenParser/Sandbox.php index 0277c70..9457325 100644 --- a/lib/Twig/TokenParser/Sandbox.php +++ b/lib/Twig/TokenParser/Sandbox.php @@ -35,6 +35,19 @@ class Twig_TokenParser_Sandbox extends Twig_TokenParser $body = $this->parser->subparse(array($this, 'decideBlockEnd'), true); $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + // in a sandbox tag, only include tags are allowed + if (!$body instanceof Twig_Node_Include) { + foreach ($body as $node) { + if ($node instanceof Twig_Node_Text && ctype_space($node->getAttribute('data'))) { + continue; + } + + if (!$node instanceof Twig_Node_Include) { + throw new Twig_Error_Syntax('Only "include" tags are allowed within a "sandbox" section', $node->getLine(), $this->parser->getFilename()); + } + } + } + return new Twig_Node_Sandbox($body, $token->getLine(), $this->getTag()); } diff --git a/lib/Twig/TokenParser/Set.php b/lib/Twig/TokenParser/Set.php index 3b4479c..6dbd1db 100644 --- a/lib/Twig/TokenParser/Set.php +++ b/lib/Twig/TokenParser/Set.php @@ -49,13 +49,13 @@ class Twig_TokenParser_Set extends Twig_TokenParser $stream->expect(Twig_Token::BLOCK_END_TYPE); if (count($names) !== count($values)) { - throw new Twig_Error_Syntax("When using set, you must have the same number of variables and assignements.", $lineno); + throw new Twig_Error_Syntax("When using set, you must have the same number of variables and assignements.", $stream->getCurrent()->getLine(), $stream->getFilename()); } } else { $capture = true; if (count($names) > 1) { - throw new Twig_Error_Syntax("When using set with a block, you cannot have a multi-target.", $lineno); + throw new Twig_Error_Syntax("When using set with a block, you cannot have a multi-target.", $stream->getCurrent()->getLine(), $stream->getFilename()); } $stream->expect(Twig_Token::BLOCK_END_TYPE); diff --git a/lib/Twig/TokenParser/Use.php b/lib/Twig/TokenParser/Use.php index beafc80..85f084a 100644 --- a/lib/Twig/TokenParser/Use.php +++ b/lib/Twig/TokenParser/Use.php @@ -35,13 +35,12 @@ class Twig_TokenParser_Use extends Twig_TokenParser public function parse(Twig_Token $token) { $template = $this->parser->getExpressionParser()->parseExpression(); + $stream = $this->parser->getStream(); if (!$template instanceof Twig_Node_Expression_Constant) { - throw new Twig_Error_Syntax('The template references in a "use" statement must be a string.', $token->getLine()); + throw new Twig_Error_Syntax('The template references in a "use" statement must be a string.', $stream->getCurrent()->getLine(), $stream->getFilename()); } - $stream = $this->parser->getStream(); - $targets = array(); if ($stream->test('with')) { $stream->next(); diff --git a/lib/Twig/TokenParserBroker.php b/lib/Twig/TokenParserBroker.php index b214e99..f2c27ee 100644 --- a/lib/Twig/TokenParserBroker.php +++ b/lib/Twig/TokenParserBroker.php @@ -32,13 +32,13 @@ class Twig_TokenParserBroker implements Twig_TokenParserBrokerInterface { foreach ($parsers as $parser) { if (!$parser instanceof Twig_TokenParserInterface) { - throw new Twig_Error('$parsers must a an array of Twig_TokenParserInterface'); + throw new LogicException('$parsers must a an array of Twig_TokenParserInterface'); } $this->parsers[$parser->getTag()] = $parser; } foreach ($brokers as $broker) { if (!$broker instanceof Twig_TokenParserBrokerInterface) { - throw new Twig_Error('$brokers must a an array of Twig_TokenParserBrokerInterface'); + throw new LogicException('$brokers must a an array of Twig_TokenParserBrokerInterface'); } $this->brokers[] = $broker; } diff --git a/lib/Twig/TokenStream.php b/lib/Twig/TokenStream.php index 5708091..292c11f 100644 --- a/lib/Twig/TokenStream.php +++ b/lib/Twig/TokenStream.php @@ -58,7 +58,7 @@ class Twig_TokenStream public function next() { if (!isset($this->tokens[++$this->current])) { - throw new Twig_Error_Syntax('Unexpected end of template', -1, $this->filename); + throw new Twig_Error_Syntax('Unexpected end of template', $this->token[$this->current - 1]->getLine(), $this->filename); } return $this->tokens[$this->current - 1]; @@ -97,7 +97,7 @@ class Twig_TokenStream public function look($number = 1) { if (!isset($this->tokens[$this->current + $number])) { - throw new Twig_Error_Syntax('Unexpected end of template', -1, $this->filename); + throw new Twig_Error_Syntax('Unexpected end of template', $this->token[$this->current + $number - 1]->getLine(), $this->filename); } return $this->tokens[$this->current + $number]; diff --git a/test/Twig/Tests/Fixtures/tags/include/missing.test b/test/Twig/Tests/Fixtures/tags/include/missing.test new file mode 100644 index 0000000..f25e871 --- /dev/null +++ b/test/Twig/Tests/Fixtures/tags/include/missing.test @@ -0,0 +1,8 @@ +--TEST-- +"include" tag +--TEMPLATE-- +{% include "foo.twig" %} +--DATA-- +return array(); +--EXCEPTION-- +Twig_Error_Loader: Template "foo.twig" is not defined in "index.twig" at line 2. diff --git a/test/Twig/Tests/Fixtures/tags/include/missing_nested.test b/test/Twig/Tests/Fixtures/tags/include/missing_nested.test new file mode 100644 index 0000000..86c1864 --- /dev/null +++ b/test/Twig/Tests/Fixtures/tags/include/missing_nested.test @@ -0,0 +1,16 @@ +--TEST-- +"include" tag +--TEMPLATE-- +{% extends "base.twig" %} + +{% block content %} + {{ parent() }} +{% endblock %} +--TEMPLATE(base.twig)-- +{% block content %} + {% include "foo.twig" %} +{% endblock %} +--DATA-- +return array(); +--EXCEPTION-- +Twig_Error_Loader: Template "foo.twig" is not defined in "base.twig" at line 3. diff --git a/test/Twig/Tests/ParserTest.php b/test/Twig/Tests/ParserTest.php index b0facf7..9c757ee 100644 --- a/test/Twig/Tests/ParserTest.php +++ b/test/Twig/Tests/ParserTest.php @@ -15,7 +15,7 @@ class Twig_Tests_ParserTest extends PHPUnit_Framework_TestCase */ public function testSetMacroThrowsExceptionOnReservedMethods() { - $parser = new Twig_Parser(new Twig_Environment()); + $parser = $this->getParser(); $parser->setMacro('display', $this->getMock('Twig_Node_Macro', array(), array(), '', null)); } @@ -40,7 +40,7 @@ class Twig_Tests_ParserTest extends PHPUnit_Framework_TestCase */ public function testFilterBodyNodes($input, $expected) { - $parser = $this->getParserForFilterBodyNodes(); + $parser = $this->getParser(); $this->assertEquals($expected, $parser->filterBodyNodes($input)); } @@ -69,7 +69,7 @@ class Twig_Tests_ParserTest extends PHPUnit_Framework_TestCase */ public function testFilterBodyNodesThrowsException($input) { - $parser = $this->getParserForFilterBodyNodes(); + $parser = $this->getParser(); $parser->filterBodyNodes($input); } @@ -88,7 +88,7 @@ class Twig_Tests_ParserTest extends PHPUnit_Framework_TestCase */ public function testFilterBodyNodesWithBOM() { - $parser = $this->getParserForFilterBodyNodes(); + $parser = $this->getParser(); $parser->filterBodyNodes(new Twig_Node_Text(chr(0xEF).chr(0xBB).chr(0xBF), 1)); } @@ -135,7 +135,7 @@ EOF )); } - protected function getParserForFilterBodyNodes() + protected function getParser() { $parser = new TestParser(new Twig_Environment()); $parser->setParent(new Twig_Node()); -- 1.7.2.5