minor #24007 [ExpressionLanguage] SyntaxError : make a proposal when no function or variable found (fmata)

This PR was merged into the 3.4 branch.

Discussion
----------

[ExpressionLanguage] SyntaxError : make a proposal when no function or variable found

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

Now when an Expression fails to be parsed, a SyntaxError is raised with an explicit message but it's impossible to extract the function, variable or the pair token  type/token value to parse.
When a functional user encounters a SyntaxError, by default there is no mechanism to understand why what he typed is incorrect.

This PR exposes when it's possible the subject of the violation and in case of function or variable undefined in the Expression, a proposal is made.

```php
<?php

require 'vendor/autoload.php';

use Symfony\Component\ExpressionLanguage\Lexer;
use Symfony\Component\ExpressionLanguage\Parser;
use Symfony\Component\ExpressionLanguage\SyntaxError;

$lexer = new Lexer();
$parser = new Parser(array());

try {
    $parser->parse($lexer->tokenize('user.city.departement in [departement, departement2]'), array('user', 'departement1', 'departement2'));
} catch (SyntaxError $e) {
    echo $e->getMessage();
}
```
Outputs :

> Variable "departement" is not valid around position 27 for expression `user.city.departement in [departement, departement2]`.
>
> Did you mean "departement1"?

Commits
-------

f19cf8a916 [ExpressionLanguage] make a proposal in SyntaxError message
This commit is contained in:
Fabien Potencier 2017-09-10 16:46:16 -07:00
commit 565a1c4b48
3 changed files with 30 additions and 3 deletions

View File

@ -195,13 +195,13 @@ class Parser
default:
if ('(' === $this->stream->current->value) {
if (false === isset($this->functions[$token->value])) {
throw new SyntaxError(sprintf('The function "%s" does not exist', $token->value), $token->cursor, $this->stream->getExpression());
throw new SyntaxError(sprintf('The function "%s" does not exist', $token->value), $token->cursor, $this->stream->getExpression(), $token->value, array_keys($this->functions));
}
$node = new Node\FunctionNode($token->value, $this->parseArguments());
} else {
if (!in_array($token->value, $this->names, true)) {
throw new SyntaxError(sprintf('Variable "%s" is not valid', $token->value), $token->cursor, $this->stream->getExpression());
throw new SyntaxError(sprintf('Variable "%s" is not valid', $token->value), $token->cursor, $this->stream->getExpression(), $token->value, $this->names);
}
// is the name used in the compiled code different

View File

@ -13,7 +13,7 @@ namespace Symfony\Component\ExpressionLanguage;
class SyntaxError extends \LogicException
{
public function __construct($message, $cursor = 0, $expression = '')
public function __construct($message, $cursor = 0, $expression = '', $subject = null, array $proposals = null)
{
$message = sprintf('%s around position %d', $message, $cursor);
if ($expression) {
@ -21,6 +21,21 @@ class SyntaxError extends \LogicException
}
$message .= '.';
if (null !== $subject && null !== $proposals) {
$minScore = INF;
foreach ($proposals as $proposal) {
$distance = levenshtein($subject, $proposal);
if ($distance < $minScore) {
$guess = $proposal;
$minScore = $distance;
}
}
if (isset($guess) && $minScore < 3) {
$message .= sprintf(' Did you mean "%s"?', $guess);
}
}
parent::__construct($message);
}
}

View File

@ -195,4 +195,16 @@ class ParserTest extends TestCase
),
);
}
/**
* @expectedException \Symfony\Component\ExpressionLanguage\SyntaxError
* @expectedExceptionMessage Did you mean "baz"?
*/
public function testNameProposal()
{
$lexer = new Lexer();
$parser = new Parser(array());
$parser->parse($lexer->tokenize('foo > bar'), array('foo', 'baz'));
}
}