Skip to content

Commit

Permalink
feature #2273 Make Twig_Error report real source path when possible (…
Browse files Browse the repository at this point in the history
…nicolas-grekas)

This PR was merged into the 1.x branch.

Discussion
----------

Make Twig_Error report real source path when possible

To get useful stack traces like this one:
![capture du 2016-11-22 12-16-07](https://cloud.githubusercontent.com/assets/243674/20521829/7cc1fa80-b0ad-11e6-81b6-028e66ac9436.png)

Commits
-------

44f0f9c Make Twig_Error report real source path when possible
  • Loading branch information
fabpot committed Dec 8, 2016
2 parents b6f28e0 + 44f0f9c commit e4125e6
Show file tree
Hide file tree
Showing 21 changed files with 241 additions and 96 deletions.
1 change: 1 addition & 0 deletions CHANGELOG
@@ -1,5 +1,6 @@
* 1.29.0 (2016-XX-XX)

* made Twig_Error report real source path when possible
* added support for {{ _self }} to provide an upgrade path from 1.x to 2.0 (replaces {{ _self.templateName }})
* deprecated silent display of undefined blocks
* deprecated support for mbstring.func_overload != 0
Expand Down
4 changes: 2 additions & 2 deletions lib/Twig/Environment.php
Expand Up @@ -741,10 +741,10 @@ public function compileSource($source, $name = null)
try {
return $this->compile($this->parse($this->tokenize($source)));
} catch (Twig_Error $e) {
$e->setTemplateName($source->getName());
$e->setSourceContext($source);
throw $e;
} catch (Exception $e) {
throw new Twig_Error_Syntax(sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $source->getName(), $e);
throw new Twig_Error_Syntax(sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $source, $e);
}
}

Expand Down
82 changes: 72 additions & 10 deletions lib/Twig/Error.php
Expand Up @@ -39,6 +39,9 @@ class Twig_Error extends Exception
protected $rawMessage;
protected $previous;

private $sourcePath;
private $sourceCode;

/**
* Constructor.
*
Expand All @@ -51,13 +54,23 @@ class Twig_Error extends Exception
*
* By default, automatic guessing is enabled.
*
* @param string $message The error message
* @param int $lineno The template line where the error occurred
* @param string $name The template logical name where the error occurred
* @param Exception $previous The previous exception
* @param string $message The error message
* @param int $lineno The template line where the error occurred
* @param Twig_Source|string|null $source The source context where the error occurred
* @param Exception $previous The previous exception
*/
public function __construct($message, $lineno = -1, $name = null, Exception $previous = null)
public function __construct($message, $lineno = -1, $source = null, Exception $previous = null)
{
if (null === $source) {
$name = null;
} elseif (!$source instanceof Twig_Source) {
// for compat with the Twig C ext., passing the template name as string is accepted
$name = $source;
} else {
$name = $source->getName();
$this->sourceCode = $source->getCode();
$this->sourcePath = $source->getPath();
}
if (PHP_VERSION_ID < 50300) {
$this->previous = $previous;
parent::__construct('');
Expand All @@ -68,7 +81,7 @@ public function __construct($message, $lineno = -1, $name = null, Exception $pre
$this->lineno = $lineno;
$this->filename = $name;

if (-1 === $lineno || null === $name) {
if (-1 === $lineno || null === $name || null === $this->sourcePath) {
$this->guessTemplateInfo();
}

Expand All @@ -92,11 +105,11 @@ public function getRawMessage()
*
* @return string The name
*
* @deprecated since 1.27 (to be removed in 2.0). Use getTemplateName() instead.
* @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 getTemplateName() instead.', __METHOD__), E_USER_DEPRECATED);
@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;
}
Expand All @@ -106,11 +119,11 @@ public function getTemplateFile()
*
* @param string $name The name
*
* @deprecated since 1.27 (to be removed in 2.0). Use setTemplateName() instead.
* @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 setTemplateName() instead.', __METHOD__), E_USER_DEPRECATED);
@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;

Expand All @@ -121,20 +134,29 @@ public function setTemplateFile($name)
* 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();
}
Expand All @@ -161,6 +183,32 @@ public function setTemplateLine($lineno)
$this->updateRepr();
}

/**
* Gets the source context of the Twig template where the error occurred.
*
* @return Twig_Source|null
*/
public function getSourceContext()
{
return $this->filename ? new Twig_Source($this->sourceCode, $this->filename, $this->sourcePath) : null;
}

/**
* Sets the source context of the Twig template where the error occurred.
*/
public function setSourceContext(Twig_Source $source = null)
{
if (null === $source) {
$this->sourceCode = $this->filename = $this->sourcePath = null;
} else {
$this->sourceCode = $source->getCode();
$this->filename = $source->getName();
$this->sourcePath = $source->getPath();
}

$this->updateRepr();
}

public function guess()
{
$this->guessTemplateInfo();
Expand Down Expand Up @@ -199,6 +247,13 @@ protected function updateRepr()
{
$this->message = $this->rawMessage;

if ($this->sourcePath && $this->lineno > 0) {
$this->file = $this->sourcePath;
$this->line = $this->lineno;

return;
}

$dot = false;
if ('.' === substr($this->message, -1)) {
$this->message = substr($this->message, 0, -1);
Expand Down Expand Up @@ -263,6 +318,13 @@ protected function guessTemplateInfo()
$this->filename = $template->getTemplateName();
}

// update template path if any
if (null !== $template && null === $this->sourcePath) {
$src = $template->getSourceContext();
$this->sourceCode = $src->getCode();
$this->sourcePath = $src->getPath();
}

if (null === $template || $this->lineno > -1) {
return;
}
Expand Down
14 changes: 12 additions & 2 deletions lib/Twig/Error/Loader.php
Expand Up @@ -24,8 +24,18 @@
*/
class Twig_Error_Loader extends Twig_Error
{
public function __construct($message, $lineno = -1, $filename = null, Exception $previous = null)
private $guess = false;

public function __construct($message, $lineno = -1, $source = null, Exception $previous = null)
{
parent::__construct($message, false, null, $previous);
$this->guess = true;
}

protected function guessTemplateInfo()
{
parent::__construct($message, false, false, $previous);
if ($this->guess) {
parent::guessTemplateInfo();
}
}
}
41 changes: 22 additions & 19 deletions lib/Twig/ExpressionParser.php
Expand Up @@ -189,7 +189,7 @@ public function parsePrimaryExpression()
$negClass = 'Twig_Node_Expression_Unary_Neg';
$posClass = 'Twig_Node_Expression_Unary_Pos';
if (!(in_array($ref->getName(), array($negClass, $posClass)) || $ref->isSubclassOf($negClass) || $ref->isSubclassOf($posClass))) {
throw new Twig_Error_Syntax(sprintf('Unexpected unary operator "%s".', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()->getName());
throw new Twig_Error_Syntax(sprintf('Unexpected unary operator "%s".', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
}

$this->parser->getStream()->next();
Expand All @@ -205,7 +205,7 @@ public function parsePrimaryExpression()
} 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->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()->getName());
throw new Twig_Error_Syntax(sprintf('Unexpected token "%s" of value "%s".', Twig_Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
}
}

Expand Down Expand Up @@ -296,7 +296,7 @@ public function parseHashExpression()
} 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->getValue()), $current->getLine(), $stream->getSourceContext()->getName());
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->getValue()), $current->getLine(), $stream->getSourceContext());
}

$stream->expect(Twig_Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)');
Expand Down Expand Up @@ -335,25 +335,25 @@ public function getFunctionNode($name, $line)
case 'parent':
$this->parseArguments();
if (!count($this->parser->getBlockStack())) {
throw new Twig_Error_Syntax('Calling "parent" outside a block is forbidden.', $line, $this->parser->getStream()->getSourceContext()->getName());
throw new Twig_Error_Syntax('Calling "parent" outside a block is forbidden.', $line, $this->parser->getStream()->getSourceContext());
}

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, $this->parser->getStream()->getSourceContext()->getName());
throw new Twig_Error_Syntax('Calling "parent" on a template that does not extend nor "use" another template is forbidden.', $line, $this->parser->getStream()->getSourceContext());
}

return new Twig_Node_Expression_Parent($this->parser->peekBlockStack(), $line);
case 'block':
$args = $this->parseArguments();
if (count($args) < 1) {
throw new Twig_Error_Syntax('The "block" function takes one argument (the block name).', $line, $this->parser->getStream()->getSourceContext()->getName());
throw new Twig_Error_Syntax('The "block" function takes one argument (the block name).', $line, $this->parser->getStream()->getSourceContext());
}

return new Twig_Node_Expression_BlockReference($args->getNode(0), count($args) > 1 ? $args->getNode(1) : null, $line);
case 'attribute':
$args = $this->parseArguments();
if (count($args) < 2) {
throw new Twig_Error_Syntax('The "attribute" function takes at least two arguments (the variable and the attributes).', $line, $this->parser->getStream()->getSourceContext()->getName());
throw new Twig_Error_Syntax('The "attribute" function takes at least two arguments (the variable and the attributes).', $line, $this->parser->getStream()->getSourceContext());
}

return new Twig_Node_Expression_GetAttr($args->getNode(0), $args->getNode(1), count($args) > 2 ? $args->getNode(2) : null, Twig_Template::ANY_CALL, $line);
Expand Down Expand Up @@ -402,18 +402,18 @@ public function parseSubscriptExpression($node)
}
}
} else {
throw new Twig_Error_Syntax('Expected name or number.', $lineno, $stream->getSourceContext()->getName());
throw new Twig_Error_Syntax('Expected name or number.', $lineno, $stream->getSourceContext());
}

if ($node instanceof Twig_Node_Expression_Name && null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) {
if (!$arg instanceof Twig_Node_Expression_Constant) {
throw new Twig_Error_Syntax(sprintf('Dynamic macro names are not supported (called on "%s").', $node->getAttribute('name')), $token->getLine(), $stream->getSourceContext()->getName());
throw new Twig_Error_Syntax(sprintf('Dynamic macro names are not supported (called on "%s").', $node->getAttribute('name')), $token->getLine(), $stream->getSourceContext());
}

$name = $arg->getAttribute('value');

if ($this->parser->isReservedMacroName($name)) {
throw new Twig_Error_Syntax(sprintf('"%s" cannot be called as macro as it is a reserved keyword.', $name), $token->getLine(), $stream->getSourceContext()->getName());
throw new Twig_Error_Syntax(sprintf('"%s" cannot be called as macro as it is a reserved keyword.', $name), $token->getLine(), $stream->getSourceContext());
}

$node = new Twig_Node_Expression_MethodCall($node, 'get'.$name, $arguments, $lineno);
Expand Down Expand Up @@ -523,15 +523,15 @@ public function parseArguments($namedArguments = false, $definition = false)
$name = null;
if ($namedArguments && $token = $stream->nextIf(Twig_Token::OPERATOR_TYPE, '=')) {
if (!$value instanceof Twig_Node_Expression_Name) {
throw new Twig_Error_Syntax(sprintf('A parameter name must be a string, "%s" given.', get_class($value)), $token->getLine(), $stream->getSourceContext()->getName());
throw new Twig_Error_Syntax(sprintf('A parameter name must be a string, "%s" given.', get_class($value)), $token->getLine(), $stream->getSourceContext());
}
$name = $value->getAttribute('name');

if ($definition) {
$value = $this->parsePrimaryExpression();

if (!$this->checkConstantExpression($value)) {
throw new Twig_Error_Syntax(sprintf('A default value for an argument must be a constant (a boolean, a string, a number, or an array).'), $token->getLine(), $stream->getSourceContext()->getName());
throw new Twig_Error_Syntax(sprintf('A default value for an argument must be a constant (a boolean, a string, a number, or an array).'), $token->getLine(), $stream->getSourceContext());
}
} else {
$value = $this->parseExpression();
Expand Down Expand Up @@ -565,7 +565,7 @@ public function parseAssignmentExpression()
$token = $stream->expect(Twig_Token::NAME_TYPE, null, 'Only variables can be assigned to');
$value = $token->getValue();
if (in_array(strtolower($value), array('true', 'false', 'none', 'null'))) {
throw new Twig_Error_Syntax(sprintf('You cannot assign a value to "%s".', $value), $token->getLine(), $stream->getSourceContext()->getName());
throw new Twig_Error_Syntax(sprintf('You cannot assign a value to "%s".', $value), $token->getLine(), $stream->getSourceContext());
}
$targets[] = new Twig_Node_Expression_AssignName($value, $token->getLine());

Expand Down Expand Up @@ -629,7 +629,7 @@ private function getTest($line)
}
}

$e = new Twig_Error_Syntax(sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext()->getName());
$e = new Twig_Error_Syntax(sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext());
$e->addSuggestions($name, array_keys($this->env->getTests()));

throw $e;
Expand All @@ -646,7 +646,8 @@ private function getTestNodeClass($test)
if ($test->getAlternative()) {
$message .= sprintf('. Use "%s" instead', $test->getAlternative());
}
$message .= sprintf(' in %s at line %d.', $stream->getSourceContext()->getName(), $stream->getCurrent()->getLine());
$src = $stream->getSourceContext();
$message .= sprintf(' in %s at line %d.', $src->getPath() ? $src->getPath() : $src->getName(), $stream->getCurrent()->getLine());

@trigger_error($message, E_USER_DEPRECATED);
}
Expand All @@ -661,7 +662,7 @@ private function getTestNodeClass($test)
protected function getFunctionNodeClass($name, $line)
{
if (false === $function = $this->env->getFunction($name)) {
$e = new Twig_Error_Syntax(sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext()->getName());
$e = new Twig_Error_Syntax(sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext());
$e->addSuggestions($name, array_keys($this->env->getFunctions()));

throw $e;
Expand All @@ -675,7 +676,8 @@ protected function getFunctionNodeClass($name, $line)
if ($function->getAlternative()) {
$message .= sprintf('. Use "%s" instead', $function->getAlternative());
}
$message .= sprintf(' in %s at line %d.', $this->parser->getStream()->getSourceContext()->getName(), $line);
$src = $this->parser->getStream()->getSourceContext();
$message .= sprintf(' in %s at line %d.', $src->getPath() ? $src->getPath() : $src->getName(), $line);

@trigger_error($message, E_USER_DEPRECATED);
}
Expand All @@ -690,7 +692,7 @@ protected function getFunctionNodeClass($name, $line)
protected function getFilterNodeClass($name, $line)
{
if (false === $filter = $this->env->getFilter($name)) {
$e = new Twig_Error_Syntax(sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext()->getName());
$e = new Twig_Error_Syntax(sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext());
$e->addSuggestions($name, array_keys($this->env->getFilters()));

throw $e;
Expand All @@ -704,7 +706,8 @@ protected function getFilterNodeClass($name, $line)
if ($filter->getAlternative()) {
$message .= sprintf('. Use "%s" instead', $filter->getAlternative());
}
$message .= sprintf(' in %s at line %d.', $this->parser->getStream()->getSourceContext()->getName(), $line);
$src = $this->parser->getStream()->getSourceContext();
$message .= sprintf(' in %s at line %d.', $src->getPath() ? $src->getPath() : $src->getName(), $line);

@trigger_error($message, E_USER_DEPRECATED);
}
Expand Down

0 comments on commit e4125e6

Please sign in to comment.