bug #22229 [ExpressionLanguage] Provide the expression in syntax errors (k0pernikus, stof)

This PR was merged into the 2.7 branch.

Discussion
----------

[ExpressionLanguage] Provide the expression in syntax errors

| Q             | A
| ------------- | ---
| Branch?       | 2.7
| Bug fix?      | no
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #19445
| License       | MIT
| Doc PR        | n/a

This finishes the PR #19449.

Commits
-------

7cd744133d Complete the injection of the expression in all syntax errors
dc55db2a9d add expression text to SyntaxError
This commit is contained in:
Fabien Potencier 2017-04-03 14:52:00 -07:00
commit a9da8a3858
6 changed files with 70 additions and 22 deletions

View File

@ -59,12 +59,12 @@ class Lexer
} elseif (false !== strpos(')]}', $expression[$cursor])) { } elseif (false !== strpos(')]}', $expression[$cursor])) {
// closing bracket // closing bracket
if (empty($brackets)) { if (empty($brackets)) {
throw new SyntaxError(sprintf('Unexpected "%s"', $expression[$cursor]), $cursor); throw new SyntaxError(sprintf('Unexpected "%s"', $expression[$cursor]), $cursor, $expression);
} }
list($expect, $cur) = array_pop($brackets); list($expect, $cur) = array_pop($brackets);
if ($expression[$cursor] != strtr($expect, '([{', ')]}')) { if ($expression[$cursor] != strtr($expect, '([{', ')]}')) {
throw new SyntaxError(sprintf('Unclosed "%s"', $expect), $cur); throw new SyntaxError(sprintf('Unclosed "%s"', $expect), $cur, $expression);
} }
$tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1); $tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1);
@ -87,7 +87,7 @@ class Lexer
$cursor += strlen($match[0]); $cursor += strlen($match[0]);
} else { } else {
// unlexable // unlexable
throw new SyntaxError(sprintf('Unexpected character "%s"', $expression[$cursor]), $cursor); throw new SyntaxError(sprintf('Unexpected character "%s"', $expression[$cursor]), $cursor, $expression);
} }
} }
@ -95,9 +95,9 @@ class Lexer
if (!empty($brackets)) { if (!empty($brackets)) {
list($expect, $cur) = array_pop($brackets); list($expect, $cur) = array_pop($brackets);
throw new SyntaxError(sprintf('Unclosed "%s"', $expect), $cur); throw new SyntaxError(sprintf('Unclosed "%s"', $expect), $cur, $expression);
} }
return new TokenStream($tokens); return new TokenStream($tokens, $expression);
} }
} }

View File

@ -99,7 +99,7 @@ class Parser
$node = $this->parseExpression(); $node = $this->parseExpression();
if (!$stream->isEOF()) { if (!$stream->isEOF()) {
throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s"', $stream->current->type, $stream->current->value), $stream->current->cursor); throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s"', $stream->current->type, $stream->current->value), $stream->current->cursor, $stream->getExpression());
} }
return $node; return $node;
@ -195,13 +195,13 @@ class Parser
default: default:
if ('(' === $this->stream->current->value) { if ('(' === $this->stream->current->value) {
if (false === isset($this->functions[$token->value])) { if (false === isset($this->functions[$token->value])) {
throw new SyntaxError(sprintf('The function "%s" does not exist', $token->value), $token->cursor); throw new SyntaxError(sprintf('The function "%s" does not exist', $token->value), $token->cursor, $this->stream->getExpression());
} }
$node = new Node\FunctionNode($token->value, $this->parseArguments()); $node = new Node\FunctionNode($token->value, $this->parseArguments());
} else { } else {
if (!in_array($token->value, $this->names, true)) { if (!in_array($token->value, $this->names, true)) {
throw new SyntaxError(sprintf('Variable "%s" is not valid', $token->value), $token->cursor); throw new SyntaxError(sprintf('Variable "%s" is not valid', $token->value), $token->cursor, $this->stream->getExpression());
} }
// is the name used in the compiled code different // is the name used in the compiled code different
@ -227,7 +227,7 @@ class Parser
} elseif ($token->test(Token::PUNCTUATION_TYPE, '{')) { } elseif ($token->test(Token::PUNCTUATION_TYPE, '{')) {
$node = $this->parseHashExpression(); $node = $this->parseHashExpression();
} else { } else {
throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s"', $token->type, $token->value), $token->cursor); throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s"', $token->type, $token->value), $token->cursor, $this->stream->getExpression());
} }
} }
@ -289,7 +289,7 @@ class Parser
} else { } else {
$current = $this->stream->current; $current = $this->stream->current;
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"', $current->type, $current->value), $current->cursor); 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"', $current->type, $current->value), $current->cursor, $this->stream->getExpression());
} }
$this->stream->expect(Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)'); $this->stream->expect(Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)');
@ -327,7 +327,7 @@ class Parser
// As a result, if $token is NOT an operator OR $token->value is NOT a valid property or method name, an exception shall be thrown. // As a result, if $token is NOT an operator OR $token->value is NOT a valid property or method name, an exception shall be thrown.
($token->type !== Token::OPERATOR_TYPE || !preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A', $token->value)) ($token->type !== Token::OPERATOR_TYPE || !preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A', $token->value))
) { ) {
throw new SyntaxError('Expected name', $token->cursor); throw new SyntaxError('Expected name', $token->cursor, $this->stream->getExpression());
} }
$arg = new Node\ConstantNode($token->value); $arg = new Node\ConstantNode($token->value);
@ -345,7 +345,7 @@ class Parser
$node = new Node\GetAttrNode($node, $arg, $arguments, $type); $node = new Node\GetAttrNode($node, $arg, $arguments, $type);
} elseif ('[' === $token->value) { } elseif ('[' === $token->value) {
if ($node instanceof Node\GetAttrNode && Node\GetAttrNode::METHOD_CALL === $node->attributes['type'] && PHP_VERSION_ID < 50400) { if ($node instanceof Node\GetAttrNode && Node\GetAttrNode::METHOD_CALL === $node->attributes['type'] && PHP_VERSION_ID < 50400) {
throw new SyntaxError('Array calls on a method call is only supported on PHP 5.4+', $token->cursor); throw new SyntaxError('Array calls on a method call is only supported on PHP 5.4+', $token->cursor, $this->stream->getExpression());
} }
$this->stream->next(); $this->stream->next();

View File

@ -13,8 +13,14 @@ namespace Symfony\Component\ExpressionLanguage;
class SyntaxError extends \LogicException class SyntaxError extends \LogicException
{ {
public function __construct($message, $cursor = 0) public function __construct($message, $cursor = 0, $expression = '')
{ {
parent::__construct(sprintf('%s around position %d.', $message, $cursor)); $message = sprintf('%s around position %d', $message, $cursor);
if ($expression) {
$message = sprintf('%s for expression `%s`', $message, $expression);
}
$message .= '.';
parent::__construct($message);
} }
} }

View File

@ -18,14 +18,43 @@ use Symfony\Component\ExpressionLanguage\TokenStream;
class LexerTest extends TestCase class LexerTest extends TestCase
{ {
/**
* @var Lexer
*/
private $lexer;
protected function setUp()
{
$this->lexer = new Lexer();
}
/** /**
* @dataProvider getTokenizeData * @dataProvider getTokenizeData
*/ */
public function testTokenize($tokens, $expression) public function testTokenize($tokens, $expression)
{ {
$tokens[] = new Token('end of expression', null, strlen($expression) + 1); $tokens[] = new Token('end of expression', null, strlen($expression) + 1);
$lexer = new Lexer(); $this->assertEquals(new TokenStream($tokens, $expression), $this->lexer->tokenize($expression));
$this->assertEquals(new TokenStream($tokens), $lexer->tokenize($expression)); }
/**
* @expectedException \Symfony\Component\ExpressionLanguage\SyntaxError
* @expectedExceptionMessage Unexpected character "'" around position 33 for expression `service(faulty.expression.example').dummyMethod()`.
*/
public function testTokenizeThrowsErrorWithMessage()
{
$expression = "service(faulty.expression.example').dummyMethod()";
$this->lexer->tokenize($expression);
}
/**
* @expectedException \Symfony\Component\ExpressionLanguage\SyntaxError
* @expectedExceptionMessage Unclosed "(" around position 7 for expression `service(unclosed.expression.dummyMethod()`.
*/
public function testTokenizeThrowsErrorOnUnclosedBrace()
{
$expression = 'service(unclosed.expression.dummyMethod()';
$this->lexer->tokenize($expression);
} }
public function getTokenizeData() public function getTokenizeData()

View File

@ -20,7 +20,7 @@ class ParserTest extends TestCase
{ {
/** /**
* @expectedException \Symfony\Component\ExpressionLanguage\SyntaxError * @expectedException \Symfony\Component\ExpressionLanguage\SyntaxError
* @expectedExceptionMessage Variable "foo" is not valid around position 1. * @expectedExceptionMessage Variable "foo" is not valid around position 1 for expression `foo`.
*/ */
public function testParseWithInvalidName() public function testParseWithInvalidName()
{ {
@ -31,7 +31,7 @@ class ParserTest extends TestCase
/** /**
* @expectedException \Symfony\Component\ExpressionLanguage\SyntaxError * @expectedException \Symfony\Component\ExpressionLanguage\SyntaxError
* @expectedExceptionMessage Variable "foo" is not valid around position 1. * @expectedExceptionMessage Variable "foo" is not valid around position 1 for expression `foo`.
*/ */
public function testParseWithZeroInNames() public function testParseWithZeroInNames()
{ {

View File

@ -22,16 +22,19 @@ class TokenStream
private $tokens; private $tokens;
private $position = 0; private $position = 0;
private $expression;
/** /**
* Constructor. * Constructor.
* *
* @param array $tokens An array of tokens * @param array $tokens An array of tokens
* @param string $expression
*/ */
public function __construct(array $tokens) public function __construct(array $tokens, $expression = '')
{ {
$this->tokens = $tokens; $this->tokens = $tokens;
$this->current = $tokens[0]; $this->current = $tokens[0];
$this->expression = $expression;
} }
/** /**
@ -50,7 +53,7 @@ class TokenStream
public function next() public function next()
{ {
if (!isset($this->tokens[$this->position])) { if (!isset($this->tokens[$this->position])) {
throw new SyntaxError('Unexpected end of expression', $this->current->cursor); throw new SyntaxError('Unexpected end of expression', $this->current->cursor, $this->expression);
} }
++$this->position; ++$this->position;
@ -69,7 +72,7 @@ class TokenStream
{ {
$token = $this->current; $token = $this->current;
if (!$token->test($type, $value)) { if (!$token->test($type, $value)) {
throw new SyntaxError(sprintf('%sUnexpected token "%s" of value "%s" ("%s" expected%s)', $message ? $message.'. ' : '', $token->type, $token->value, $type, $value ? sprintf(' with value "%s"', $value) : ''), $token->cursor); throw new SyntaxError(sprintf('%sUnexpected token "%s" of value "%s" ("%s" expected%s)', $message ? $message.'. ' : '', $token->type, $token->value, $type, $value ? sprintf(' with value "%s"', $value) : ''), $token->cursor, $this->expression);
} }
$this->next(); $this->next();
} }
@ -83,4 +86,14 @@ class TokenStream
{ {
return $this->current->type === Token::EOF_TYPE; return $this->current->type === Token::EOF_TYPE;
} }
/**
* @internal
*
* @return string
*/
public function getExpression()
{
return $this->expression;
}
} }