Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 623540b

Browse filesBrowse files
committed
[ExpressionLanguage] Added expression language syntax validator
1 parent cde44fc commit 623540b
Copy full SHA for 623540b

File tree

7 files changed

+302
-10
lines changed
Filter options

7 files changed

+302
-10
lines changed

‎src/Symfony/Component/ExpressionLanguage/ExpressionLanguage.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/ExpressionLanguage/ExpressionLanguage.php
+13Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,19 @@ public function parse($expression, array $names)
9797
return $parsedExpression;
9898
}
9999

100+
/**
101+
* @param Expression|string $expression The expression to check parse available
102+
* @param array|null $names When provided null - expression syntax will be checked without check names
103+
*/
104+
public function dryRun($expression, ?array $names)
105+
{
106+
if ($expression instanceof ParsedExpression) {
107+
return;
108+
}
109+
110+
$this->getParser()->parse($this->getLexer()->tokenize((string) $expression), $names, true);
111+
}
112+
100113
/**
101114
* Registers a function.
102115
*

‎src/Symfony/Component/ExpressionLanguage/Parser.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/ExpressionLanguage/Parser.php
+19-10Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class Parser
3131
private $binaryOperators;
3232
private $functions;
3333
private $names;
34+
private $dryRun;
3435

3536
public function __construct(array $functions)
3637
{
@@ -85,21 +86,25 @@ public function __construct(array $functions)
8586
* variable 'container' can be used in the expression
8687
* but the compiled code will use 'this'.
8788
*
88-
* @return Node\Node A node tree
89+
* In "dry run" mode, the syntax of the passed expression will be checked, but not parsed.
90+
* In this case allowed null for names for skip checking dynamic values.
91+
*
92+
* @return Node\Node|null A node tree or null when called on "dry run" mode
8993
*
9094
* @throws SyntaxError
9195
*/
92-
public function parse(TokenStream $stream, array $names = [])
96+
public function parse(TokenStream $stream, ?array $names = [], bool $dryRun = false)
9397
{
9498
$this->stream = $stream;
9599
$this->names = $names;
100+
$this->dryRun = $dryRun;
96101

97102
$node = $this->parseExpression();
98103
if (!$stream->isEOF()) {
99104
throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s"', $stream->current->type, $stream->current->value), $stream->current->cursor, $stream->getExpression());
100105
}
101106

102-
return $node;
107+
return $this->dryRun ? null : $node;
103108
}
104109

105110
public function parseExpression(int $precedence = 0)
@@ -197,13 +202,17 @@ public function parsePrimaryExpression()
197202

198203
$node = new Node\FunctionNode($token->value, $this->parseArguments());
199204
} else {
200-
if (!\in_array($token->value, $this->names, true)) {
201-
throw new SyntaxError(sprintf('Variable "%s" is not valid', $token->value), $token->cursor, $this->stream->getExpression(), $token->value, $this->names);
202-
}
203-
204-
// is the name used in the compiled code different
205-
// from the name used in the expression?
206-
if (\is_int($name = array_search($token->value, $this->names))) {
205+
if (!$this->dryRun || \is_array($this->names)) {
206+
if (!\in_array($token->value, $this->names, true)) {
207+
throw new SyntaxError(sprintf('Variable "%s" is not valid', $token->value), $token->cursor, $this->stream->getExpression(), $token->value, $this->names);
208+
}
209+
210+
// is the name used in the compiled code different
211+
// from the name used in the expression?
212+
if (\is_int($name = array_search($token->value, $this->names))) {
213+
$name = $token->value;
214+
}
215+
} else {
207216
$name = $token->value;
208217
}
209218

‎src/Symfony/Component/ExpressionLanguage/Tests/ParserTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/ExpressionLanguage/Tests/ParserTest.php
+92Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\ExpressionLanguage\Lexer;
1616
use Symfony\Component\ExpressionLanguage\Node;
1717
use Symfony\Component\ExpressionLanguage\Parser;
18+
use Symfony\Component\ExpressionLanguage\SyntaxError;
1819

1920
class ParserTest extends TestCase
2021
{
@@ -201,4 +202,95 @@ public function testNameProposal()
201202

202203
$parser->parse($lexer->tokenize('foo > bar'), ['foo', 'baz']);
203204
}
205+
206+
/**
207+
* @dataProvider getParseInDryRunModeData
208+
*/
209+
public function testParseInDryRunMode($expression, $names, ?string $exception = null)
210+
{
211+
if ($exception) {
212+
$this->expectException(SyntaxError::class);
213+
$this->expectExceptionMessage($exception);
214+
}
215+
216+
$lexer = new Lexer();
217+
$parser = new Parser([]);
218+
$this->assertNull($parser->parse($lexer->tokenize($expression), $names, true));
219+
}
220+
221+
public function getParseInDryRunModeData(): array
222+
{
223+
return [
224+
'valid expression' => [
225+
'expression' => 'foo["some_key"].callFunction(a ? b)',
226+
'names' => ['foo', 'a', 'b'],
227+
],
228+
'allow expression without names' => [
229+
'expression' => 'foo.bar',
230+
'names' => null,
231+
],
232+
'disallow expression without names' => [
233+
'expression' => 'foo.bar',
234+
'names' => [],
235+
'exception' => 'Variable "foo" is not valid around position 1 for expression `foo.bar',
236+
],
237+
'operator collisions' => [
238+
'expression' => 'foo.not in [bar]',
239+
'names' => ['foo', 'bar'],
240+
],
241+
'incorrect expression ending' => [
242+
'expression' => 'foo["a"] foo["b"]',
243+
'names' => ['foo'],
244+
'exception' => 'Unexpected token "name" of value "foo" '.
245+
'around position 10 for expression `foo["a"] foo["b"]`.',
246+
],
247+
'incorrect operator' => [
248+
'expression' => 'foo["some_key"] // 2',
249+
'names' => ['foo'],
250+
'exception' => 'Unexpected token "operator" of value "/" '.
251+
'around position 18 for expression `foo["some_key"] // 2`.',
252+
],
253+
'incorrect array' => [
254+
'expression' => '[value1, value2 value3]',
255+
'names' => ['value1', 'value2', 'value3'],
256+
'exception' => 'An array element must be followed by a comma. '.
257+
'Unexpected token "name" of value "value3" ("punctuation" expected with value ",") '.
258+
'around position 17 for expression `[value1, value2 value3]`.',
259+
],
260+
'incorrect array element' => [
261+
'expression' => 'foo["some_key")',
262+
'names' => ['foo'],
263+
'exception' => 'Unclosed "[" around position 3 for expression `foo["some_key")`.',
264+
],
265+
'missed array key' => [
266+
'expression' => 'foo[]',
267+
'names' => ['foo'],
268+
'exception' => 'Unexpected token "punctuation" of value "]" around position 5 for expression `foo[]`.',
269+
],
270+
'missed closing bracket in sub expression' => [
271+
'expression' => 'foo[(bar ? bar : "default"]',
272+
'names' => ['foo', 'bar'],
273+
'exception' => 'Unclosed "(" around position 4 for expression `foo[(bar ? bar : "default"]`.',
274+
],
275+
'incorrect hash following' => [
276+
'expression' => '{key: foo key2: bar}',
277+
'names' => ['foo', 'bar'],
278+
'exception' => 'A hash value must be followed by a comma. '.
279+
'Unexpected token "name" of value "key2" ("punctuation" expected with value ",") '.
280+
'around position 11 for expression `{key: foo key2: bar}`.',
281+
],
282+
'incorrect hash assign' => [
283+
'expression' => '{key => foo}',
284+
'names' => ['foo'],
285+
'exception' => 'Unexpected character "=" around position 5 for expression `{key => foo}`.',
286+
],
287+
'incorrect array as hash using' => [
288+
'expression' => '[foo: foo]',
289+
'names' => ['foo'],
290+
'exception' => 'An array element must be followed by a comma. '.
291+
'Unexpected token "punctuation" of value ":" ("punctuation" expected with value ",") '.
292+
'around position 5 for expression `[foo: foo]`.',
293+
],
294+
];
295+
}
204296
}
+87Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\ExpressionLanguage\Tests\Validator\Constraints;
13+
14+
use PHPUnit\Framework\MockObject\MockObject;
15+
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
16+
use Symfony\Component\ExpressionLanguage\SyntaxError;
17+
use Symfony\Component\ExpressionLanguage\Validator\Constraints\ExpressionLanguageSyntax;
18+
use Symfony\Component\ExpressionLanguage\Validator\Constraints\ExpressionLanguageSyntaxValidator;
19+
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
20+
21+
class ExpressionLanguageSyntaxTest extends ConstraintValidatorTestCase
22+
{
23+
/**
24+
* @var \PHPUnit\Framework\MockObject\MockObject|ExpressionLanguage
25+
*/
26+
protected $expressionLanguage;
27+
28+
protected function createValidator()
29+
{
30+
return new ExpressionLanguageSyntaxValidator($this->expressionLanguage);
31+
}
32+
33+
protected function setUp(): void
34+
{
35+
$this->expressionLanguage = $this->createExpressionLanguage();
36+
37+
parent::setUp();
38+
}
39+
40+
public function testExpressionValid(): void
41+
{
42+
$this->expressionLanguage->expects($this->once())
43+
->method('dryRun')
44+
->with($this->value, []);
45+
46+
$this->validator->validate($this->value, new ExpressionLanguageSyntax([
47+
'message' => 'myMessage',
48+
]));
49+
50+
$this->assertNoViolation();
51+
}
52+
53+
public function testExpressionWithoutNames(): void
54+
{
55+
$this->expressionLanguage->expects($this->once())
56+
->method('dryRun')
57+
->with($this->value, null);
58+
59+
$this->validator->validate($this->value, new ExpressionLanguageSyntax([
60+
'message' => 'myMessage',
61+
'validateNames' => false,
62+
]));
63+
64+
$this->assertNoViolation();
65+
}
66+
67+
public function testExpressionIsNotValid(): void
68+
{
69+
$this->expressionLanguage->expects($this->once())
70+
->method('dryRun')
71+
->with($this->value, [])
72+
->willThrowException(new SyntaxError('Test exception', 42));
73+
74+
$this->validator->validate($this->value, new ExpressionLanguageSyntax([
75+
'message' => 'myMessage',
76+
]));
77+
78+
$this->buildViolation('myMessage')
79+
->setParameter('{{ syntax_error }}', '"Test exception around position 42."')
80+
->assertRaised();
81+
}
82+
83+
protected function createExpressionLanguage(): MockObject
84+
{
85+
return $this->getMockBuilder('\Symfony\Component\ExpressionLanguage\ExpressionLanguage')->getMock();
86+
}
87+
}
+34Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\ExpressionLanguage\Validator\Constraints;
13+
14+
use Symfony\Component\Validator\Constraint;
15+
16+
/**
17+
* @Annotation
18+
* @Target({"PROPERTY", "METHOD", "ANNOTATION"})
19+
*/
20+
class ExpressionLanguageSyntax extends Constraint
21+
{
22+
public $message = 'This value should have correct expression language syntax.';
23+
public $service;
24+
public $validateNames = true;
25+
public $names = [];
26+
27+
/**
28+
* {@inheritdoc}
29+
*/
30+
public function validatedBy()
31+
{
32+
return $this->service;
33+
}
34+
}
+51Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\ExpressionLanguage\Validator\Constraints;
13+
14+
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
15+
use Symfony\Component\ExpressionLanguage\SyntaxError;
16+
use Symfony\Component\Validator\Constraint;
17+
use Symfony\Component\Validator\ConstraintValidator;
18+
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
19+
20+
class ExpressionLanguageSyntaxValidator extends ConstraintValidator
21+
{
22+
private $expressionLanguage;
23+
24+
public function __construct(ExpressionLanguage $expressionLanguage)
25+
{
26+
$this->expressionLanguage = $expressionLanguage;
27+
}
28+
29+
/**
30+
* {@inheritdoc}
31+
*/
32+
public function validate($expression, Constraint $constraint): void
33+
{
34+
if (!$constraint instanceof ExpressionLanguageSyntax) {
35+
throw new UnexpectedTypeException($constraint, ExpressionLanguageSyntax::class);
36+
}
37+
38+
if (!\is_string($expression)) {
39+
throw new UnexpectedTypeException($expression, 'string');
40+
}
41+
42+
try {
43+
$this->expressionLanguage->dryRun($expression, ($constraint->validateNames ? ($constraint->names ?? []) : null));
44+
} catch (SyntaxError $exception) {
45+
$this->context->buildViolation($constraint->message)
46+
->setParameter('{{ syntax_error }}', $this->formatValue($exception->getMessage()))
47+
->setInvalidValue((string) $expression)
48+
->addViolation();
49+
}
50+
}
51+
}

‎src/Symfony/Component/ExpressionLanguage/composer.json

Copy file name to clipboardExpand all lines: src/Symfony/Component/ExpressionLanguage/composer.json
+6Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@
2020
"symfony/cache": "^4.4|^5.0",
2121
"symfony/service-contracts": "^1.1|^2"
2222
},
23+
"require-dev": {
24+
"symfony/validator": "^4.4|^5.0"
25+
},
26+
"suggest": {
27+
"symfony/validator": "For using the expression language syntax constraint"
28+
},
2329
"autoload": {
2430
"psr-4": { "Symfony\\Component\\ExpressionLanguage\\": "" },
2531
"exclude-from-classmap": [

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.