Skip to content

Commit 4dce460

Browse files
committed
[ExpressionLanguage] Add more configurability to the parsing/linting methods
1 parent 749ad6e commit 4dce460

File tree

5 files changed

+54
-21
lines changed

5 files changed

+54
-21
lines changed

src/Symfony/Component/ExpressionLanguage/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ CHANGELOG
55
---
66

77
* Add support for PHP `min` and `max` functions
8+
* Add `Parser::IGNORE_UNKNOWN_VARIABLES` and `Parser::IGNORE_UNKNOWN_FUNCTIONS` to control whether
9+
parsing and linting should check for unknown variables and functions.
810

911
7.0
1012
---

src/Symfony/Component/ExpressionLanguage/ExpressionLanguage.php

+11-4
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public function evaluate(Expression|string $expression, array $values = []): mix
6262
/**
6363
* Parses an expression.
6464
*/
65-
public function parse(Expression|string $expression, array $names): ParsedExpression
65+
public function parse(Expression|string $expression, array $names, int $checks = 0): ParsedExpression
6666
{
6767
if ($expression instanceof ParsedExpression) {
6868
return $expression;
@@ -78,7 +78,7 @@ public function parse(Expression|string $expression, array $names): ParsedExpres
7878
$cacheItem = $this->cache->getItem(rawurlencode($expression.'//'.implode('|', $cacheKeyItems)));
7979

8080
if (null === $parsedExpression = $cacheItem->get()) {
81-
$nodes = $this->getParser()->parse($this->getLexer()->tokenize((string) $expression), $names);
81+
$nodes = $this->getParser()->parse($this->getLexer()->tokenize((string) $expression), $names, $checks);
8282
$parsedExpression = new ParsedExpression((string) $expression, $nodes);
8383

8484
$cacheItem->set($parsedExpression);
@@ -95,13 +95,20 @@ public function parse(Expression|string $expression, array $names): ParsedExpres
9595
*
9696
* @throws SyntaxError When the passed expression is invalid
9797
*/
98-
public function lint(Expression|string $expression, ?array $names): void
98+
public function lint(Expression|string $expression, ?array $names, int $checks = 0): void
9999
{
100+
if (null === $names) {
101+
trigger_deprecation('symfony/expression-language', '7.1', 'Passing "null" as the second argument of "%s()" is deprecated, pass "self::IGNORE_UNKNOWN_VARIABLES" instead as a third argument.', __METHOD__);
102+
103+
$checks |= Parser::IGNORE_UNKNOWN_VARIABLES;
104+
$names = [];
105+
}
106+
100107
if ($expression instanceof ParsedExpression) {
101108
return;
102109
}
103110

104-
$this->getParser()->lint($this->getLexer()->tokenize((string) $expression), $names);
111+
$this->getParser()->lint($this->getLexer()->tokenize((string) $expression), $names, $checks);
105112
}
106113

107114
/**

src/Symfony/Component/ExpressionLanguage/Parser.php

+25-13
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,14 @@ class Parser
2626
public const OPERATOR_LEFT = 1;
2727
public const OPERATOR_RIGHT = 2;
2828

29+
public const IGNORE_UNKNOWN_VARIABLES = 1;
30+
public const IGNORE_UNKNOWN_FUNCTIONS = 2;
31+
2932
private TokenStream $stream;
3033
private array $unaryOperators;
3134
private array $binaryOperators;
32-
private ?array $names;
33-
private bool $lint = false;
35+
private array $names;
36+
private int $checks = 0;
3437

3538
public function __construct(
3639
private array $functions,
@@ -87,34 +90,43 @@ public function __construct(
8790
* variable 'container' can be used in the expression
8891
* but the compiled code will use 'this'.
8992
*
93+
* @param int $checks A bit field of `Parser::IGNORE_UNKNOWN_VARIABLES` and `Parser::IGNORE_UNKNOWN_FUNCTIONS`
94+
*
9095
* @throws SyntaxError
9196
*/
92-
public function parse(TokenStream $stream, array $names = []): Node\Node
97+
public function parse(TokenStream $stream, array $names = [], int $checks = 0): Node\Node
9398
{
94-
$this->lint = false;
95-
96-
return $this->doParse($stream, $names);
99+
return $this->doParse($stream, $names, $checks);
97100
}
98101

99102
/**
100103
* Validates the syntax of an expression.
101104
*
102105
* The syntax of the passed expression will be checked, but not parsed.
103-
* If you want to skip checking dynamic variable names, pass `null` instead of the array.
106+
* If you want to skip checking dynamic variable names, pass `Parser::IGNORE_UNKNOWN_VARIABLES` instead of the array.
107+
*
108+
* @param int $checks A bit field of `Parser::IGNORE_UNKNOWN_VARIABLES` and `Parser::IGNORE_UNKNOWN_FUNCTIONS`
104109
*
105110
* @throws SyntaxError When the passed expression is invalid
106111
*/
107-
public function lint(TokenStream $stream, ?array $names = []): void
112+
public function lint(TokenStream $stream, ?array $names = [], int $checks = 0): void
108113
{
109-
$this->lint = true;
110-
$this->doParse($stream, $names);
114+
if (null === $names) {
115+
trigger_deprecation('symfony/expression-language', '7.1', 'Passing "null" as the second argument of "%s()" is deprecated, pass "self::IGNORE_UNKNOWN_VARIABLES" instead as a third argument.', __METHOD__);
116+
117+
$checks |= self::IGNORE_UNKNOWN_VARIABLES;
118+
$names = [];
119+
}
120+
121+
$this->doParse($stream, $names, $checks);
111122
}
112123

113124
/**
114125
* @throws SyntaxError
115126
*/
116-
private function doParse(TokenStream $stream, ?array $names = []): Node\Node
127+
private function doParse(TokenStream $stream, array $names, int $checks): Node\Node
117128
{
129+
$this->checks = $checks;
118130
$this->stream = $stream;
119131
$this->names = $names;
120132

@@ -224,13 +236,13 @@ public function parsePrimaryExpression(): Node\Node
224236

225237
default:
226238
if ('(' === $this->stream->current->value) {
227-
if (false === isset($this->functions[$token->value])) {
239+
if (!($this->checks & self::IGNORE_UNKNOWN_FUNCTIONS) && false === isset($this->functions[$token->value])) {
228240
throw new SyntaxError(sprintf('The function "%s" does not exist.', $token->value), $token->cursor, $this->stream->getExpression(), $token->value, array_keys($this->functions));
229241
}
230242

231243
$node = new Node\FunctionNode($token->value, $this->parseArguments());
232244
} else {
233-
if (!$this->lint || \is_array($this->names)) {
245+
if (!($this->checks & self::IGNORE_UNKNOWN_VARIABLES)) {
234246
if (!\in_array($token->value, $this->names, true)) {
235247
throw new SyntaxError(sprintf('Variable "%s" is not valid.', $token->value), $token->cursor, $this->stream->getExpression(), $token->value, $this->names);
236248
}

src/Symfony/Component/ExpressionLanguage/Tests/ExpressionLanguageTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ public function testRegisterAfterCompile($registerCallback)
461461
public function testLintDoesntThrowOnValidExpression()
462462
{
463463
$el = new ExpressionLanguage();
464-
$el->lint('1 + 1', null);
464+
$el->lint('1 + 1', []);
465465

466466
$this->expectNotToPerformAssertions();
467467
}

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

+15-3
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ public function testNameProposal()
295295
/**
296296
* @dataProvider getLintData
297297
*/
298-
public function testLint($expression, $names, ?string $exception = null)
298+
public function testLint($expression, $names, int $checks = 0, ?string $exception = null)
299299
{
300300
if ($exception) {
301301
$this->expectException(SyntaxError::class);
@@ -304,7 +304,7 @@ public function testLint($expression, $names, ?string $exception = null)
304304

305305
$lexer = new Lexer();
306306
$parser = new Parser([]);
307-
$parser->lint($lexer->tokenize($expression), $names);
307+
$parser->lint($lexer->tokenize($expression), $names, $checks);
308308

309309
// Parser does't return anything when the correct expression is passed
310310
$this->expectNotToPerformAssertions();
@@ -323,7 +323,8 @@ public static function getLintData(): array
323323
],
324324
'allow expression without names' => [
325325
'expression' => 'foo.bar',
326-
'names' => null,
326+
'names' => [],
327+
'checks' => Parser::IGNORE_UNKNOWN_VARIABLES,
327328
],
328329
'array with trailing comma' => [
329330
'expression' => '[value1, value2, value3,]',
@@ -336,6 +337,7 @@ public static function getLintData(): array
336337
'disallow expression without names' => [
337338
'expression' => 'foo.bar',
338339
'names' => [],
340+
'checks' => 0,
339341
'exception' => 'Variable "foo" is not valid around position 1 for expression `foo.bar',
340342
],
341343
'operator collisions' => [
@@ -345,57 +347,67 @@ public static function getLintData(): array
345347
'incorrect expression ending' => [
346348
'expression' => 'foo["a"] foo["b"]',
347349
'names' => ['foo'],
350+
'checks' => 0,
348351
'exception' => 'Unexpected token "name" of value "foo" '.
349352
'around position 10 for expression `foo["a"] foo["b"]`.',
350353
],
351354
'incorrect operator' => [
352355
'expression' => 'foo["some_key"] // 2',
353356
'names' => ['foo'],
357+
'checks' => 0,
354358
'exception' => 'Unexpected token "operator" of value "/" '.
355359
'around position 18 for expression `foo["some_key"] // 2`.',
356360
],
357361
'incorrect array' => [
358362
'expression' => '[value1, value2 value3]',
359363
'names' => ['value1', 'value2', 'value3'],
364+
'checks' => 0,
360365
'exception' => 'An array element must be followed by a comma. '.
361366
'Unexpected token "name" of value "value3" ("punctuation" expected with value ",") '.
362367
'around position 17 for expression `[value1, value2 value3]`.',
363368
],
364369
'incorrect array element' => [
365370
'expression' => 'foo["some_key")',
366371
'names' => ['foo'],
372+
'checks' => 0,
367373
'exception' => 'Unclosed "[" around position 3 for expression `foo["some_key")`.',
368374
],
369375
'incorrect hash key' => [
370376
'expression' => '{+: value1}',
371377
'names' => ['value1'],
378+
'checks' => 0,
372379
'exception' => 'A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "operator" of value "+" around position 2 for expression `{+: value1}`.',
373380
],
374381
'missed array key' => [
375382
'expression' => 'foo[]',
376383
'names' => ['foo'],
384+
'checks' => 0,
377385
'exception' => 'Unexpected token "punctuation" of value "]" around position 5 for expression `foo[]`.',
378386
],
379387
'missed closing bracket in sub expression' => [
380388
'expression' => 'foo[(bar ? bar : "default"]',
381389
'names' => ['foo', 'bar'],
390+
'checks' => 0,
382391
'exception' => 'Unclosed "(" around position 4 for expression `foo[(bar ? bar : "default"]`.',
383392
],
384393
'incorrect hash following' => [
385394
'expression' => '{key: foo key2: bar}',
386395
'names' => ['foo', 'bar'],
396+
'checks' => 0,
387397
'exception' => 'A hash value must be followed by a comma. '.
388398
'Unexpected token "name" of value "key2" ("punctuation" expected with value ",") '.
389399
'around position 11 for expression `{key: foo key2: bar}`.',
390400
],
391401
'incorrect hash assign' => [
392402
'expression' => '{key => foo}',
393403
'names' => ['foo'],
404+
'checks' => 0,
394405
'exception' => 'Unexpected character "=" around position 5 for expression `{key => foo}`.',
395406
],
396407
'incorrect array as hash using' => [
397408
'expression' => '[foo: foo]',
398409
'names' => ['foo'],
410+
'checks' => 0,
399411
'exception' => 'An array element must be followed by a comma. '.
400412
'Unexpected token "punctuation" of value ":" ("punctuation" expected with value ",") '.
401413
'around position 5 for expression `[foo: foo]`.',

0 commit comments

Comments
 (0)