8000 [ExpressionLanguage] Add more configurability to the parsing/linting … · symfony/symfony@c6182c0 · GitHub
[go: up one dir, main page]

Skip to content

Commit c6182c0

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

File tree

6 files changed

+79
-24
lines changed

6 files changed

+79
-24
lines changed

src/Symfony/Component/ExpressionLanguage/CHANGELOG.md

L 8000 ines changed: 2 additions & 0 deletions
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` flags 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

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@ public function evaluate(Expression|string $expression, array $values = []): mix
6161

6262
/**
6363
* Parses an expression.
64+
*
65+
* @param int-mask-of<Parser::IGNORE_*> $flags
6466
*/
65-
public function parse(Expression|string $expression, array $names): ParsedExpression
67+
public function parse(Expression|string $expression, array $names, int $flags = 0): ParsedExpression
6668
{
6769
if ($expression instanceof ParsedExpression) {
6870
return $expression;
@@ -78,7 +80,7 @@ public function parse(Expression|string $expression, array $names): ParsedExpres
7880
$cacheItem = $this->cache->getItem(rawurlencode($expression.'//'.implode('|', $cacheKeyItems)));
7981

8082
if (null === $parsedExpression = $cacheItem->get()) {
81-
$nodes = $this->getParser()->parse($this->getLexer()->tokenize((string) $expression), $names);
83+
$nodes = $this->getParser()->parse($this->getLexer()->tokenize((string) $expression), $names, $flags);
8284
$parsedExpression = new ParsedExpression((string) $expression, $nodes);
8385

8486
$cacheItem->set($parsedExpression);
@@ -91,17 +93,25 @@ public function parse(Expression|string $expression, array $names): ParsedExpres
9193
/**
9294
* Validates the syntax of an expression.
9395
*
94-
* @param array|null $names The list of acceptable variable names in the expression, or null to accept any names
96+
* @param array|null $names The list of acceptable variable names in the expression
97+
* @param int-mask-of<Parser::IGNORE_*> $flags
9598
*
9699
* @throws SyntaxError When the passed expression is invalid
97100
*/
98-
public function lint(Expression|string $expression, ?array $names): void
101+
public function lint(Expression|string $expression, ?array $names, int $flags = 0): void
99102
{
103+
if (null === $names) {
104+
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__);
105+
106+
$flags |= Parser::IGNORE_UNKNOWN_VARIABLES;
107+
$names = [];
108+
}
109+
100110
if ($expression instanceof ParsedExpression) {
101111
return;
102112
}
103113

104-
$this->getParser()->lint($this->getLexer()->tokenize((string) $expression), $names);
114+
$this->g 9E88 etParser()->lint($this->getLexer()->tokenize((string) $expression), $names, $flags);
105115
}
106116

107117
/**

src/Symfony/Component/ExpressionLanguage/Parser.php

Lines changed: 27 additions & 13 deletions
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 $flags = 0;
3437

3538
public function __construct(
3639
private array $functions,
@@ -87,34 +90,45 @@ public function __construct(
8790
* variable 'container' can be used in the expression
8891
* but the compiled code will use 'this'.
8992
*
93+
* @param int-mask-of<Parser::IGNORE_*> $flags
94+
*
9095
* @throws SyntaxError
9196
*/
92-
public function parse(TokenStream $stream, array $names = []): Node\Node
97+
public function parse(TokenStream $stream, array $names = [], int $flags = 0): Node\Node
9398
{
94-
$this->lint = false;
95-
96-
return $this->doParse($stream, $names);
99+
return $this->doParse($stream, $names, $flags);
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-mask-of<Parser::IGNORE_*> $flags
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 $flags = 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+
$flags |= self::IGNORE_UNKNOWN_VARIABLES;
118+
$names = [];
119+
}
120+
121+
$this->doParse($stream, $names, $flags);
111122
}
112123

113124
/**
125+
* @param int-mask-of<Parser::IGNORE_*> $flags
126+
*
114127
* @throws SyntaxError
115128
*/
116-
private function doParse(TokenStream $stream, ?array $names = []): Node\Node
129+
private function doParse(TokenStream $stream, array $names, int $flags): Node\Node
117130
{
131+
$this->flags = $flags;
118132
$this->stream = $stream;
119133
$this->names = $names;
120134

@@ -224,13 +238,13 @@ public function parsePrimaryExpression(): Node\Node
224238

225239
default:
226240
if ('(' === $this->stream->current->value) {
227-
if (false === isset($this->functions[$token->value])) {
241+
if (!($this->flags & self::IGNORE_UNKNOWN_FUNCTIONS) && false === isset($this->functions[$token->value])) {
228242
throw new SyntaxError(sprintf('The function "%s" does not exist.', $token->value), $token->cursor, $this->stream->getExpression(), $token->value, array_keys($this->functions));
229243
}
230244

231245
$node = new Node\FunctionNode($token->value, $this->parseArguments());
232246
} else {
233-
if (!$this->lint || \is_array($this->names)) {
247+
if (!($this->flags & self::IGNORE_UNKNOWN_VARIABLES)) {
234248
if (!\in_array($token->value, $this->names, true)) {
235249
throw new SyntaxError(sprintf('Variable "%s" is not valid.', $token->value), $token->cursor, $this->stream->getExpression(), $token->value, $this->names);
236250
}

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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 33 additions & 5 deletions
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();
@@ -321,9 +321,20 @@ public static function getLintData(): array
321321
'expression' => 'foo["some_key"]?.callFunction(a ? b)',
322322
'names' => ['foo', 'a', 'b'],
323323
],
324-
'allow expression without names' => [
324+
'allow expression with unknown names' => [
325325
'expression' => 'foo.bar',
326-
'names' => null,
326+
'names' => [],
327+
'checks' => Parser::IGNORE_UNKNOWN_VARIABLES,
328+
],
329+
'allow expression with unknown functions' => [
330+
'expression' => 'foo()',
331+
'names' => [],
332+
'checks' => Parser::IGNORE_UNKNOWN_FUNCTIONS,
333+
],
334+
'allow expression with unknown functions and names' => [
335+
'expression' => 'foo(bar)',
336+
'names' => [],
337+
'checks' => Parser::IGNORE_UNKNOWN_FUNCTIONS | Parser::IGNORE_UNKNOWN_VARIABLES,
327338
],
328339
'array with trailing comma' => [
329340
'expression' => '[value1, value2, value3,]',
@@ -333,69 +344,86 @@ public static function getLintData(): array
333344
'expression' => '{val1: value1, val2: value2, val3: value3,}',
334345
'names' => ['value1', 'value2', 'value3'],
335346
],
336-
'disallow expression without names' => [
347+
'disallow expression with unknown names by default' => [
337348
'expression' => 'foo.bar',
338349
'names' => [],
350+
'checks' => 0,
339351
'exception' => 'Variable "foo" is not valid around position 1 for expression `foo.bar',
340352
],
353+
'disallow expression with unknown functions by default' => [
354+
'expression' => 'foo()',
355+
'names' => [],
356+
'checks' => 0,
357+
'exception' => 'The function "foo" does not exist around position 1 for expression `foo()',
358+
],
341359
'operator collisions' => [
342360
'expression' => 'foo.not in [bar]',
343361
'names' => ['foo', 'bar'],
344362
],
345363
'incorrect expression ending' => [
346364
'expression' => 'foo["a"] foo["b"]',
347365
'names' => ['foo'],
366+
'checks' => 0,
348367
'exception' => 'Unexpected token "name" of value "foo" '.
349368
'around position 10 for expression `foo["a"] foo["b"]`.',
350369
],
351370
'incorrect operator' => [
352371
'expression' => 'foo["some_key"] // 2',
353372
'names' => ['foo'],
373+
'checks' => 0,
354374
'exception' => 'Unexpected token "operator" of value "/" '.
355375
'around position 18 for expression `foo["some_key"] // 2`.',
356376
],
357377
'incorrect array' => [
358378
'expression' => '[value1, value2 value3]',
359379
'names' => ['value1', 'value2', 'value3'],
380+
'checks' => 0,
360381
'exception' => 'An array element must be followed by a comma. '.
361382
'Unexpected token "name" of value "value3" ("punctuation" expected with value ",") '.
362383
'around position 17 for expression `[value1, value2 value3]`.',
363384
],
364385
'incorrect array element' => [
365386
'expression' => 'foo["some_key")',
366387
'names' => ['foo'],
388+
'checks' => 0,
367389
'exception' => 'Unclosed "[" around position 3 for expression `foo["some_key")`.',
368390
],
369391
'incorrect hash key' => [
370392
'expression' => '{+: value1}',
371393
'names' => ['value1'],
394+
'checks' => 0,
372395
'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}`.',
373396
],
374397
'missed array key' => [
375398
'expression' => 'foo[]',
376399
'names' => ['foo'],
400+
'checks' => 0,
377401
'exception' => 'Unexpected token "punctuation" of value "]" around position 5 for expression `foo[]`.',
378402
],
379403
'missed closing bracket in sub expression' => [
380404
'expression' => 'foo[(bar ? bar : "default"]',
381405
'names' => ['foo', 'bar'],
406+
'checks' => 0,
382407
'exception' => 'Unclosed "(" around position 4 for expression `foo[(bar ? bar : "default"]`.',
383408
],
384409
'incorrect hash following' => [
385410
'expression' => '{key: foo key2: bar}',
386411
'names' => ['foo', 'bar'],
412+
'checks' => 0,
387413
'exception' => 'A hash value must be followed by a comma. '.
388414
'Unexpected token "name" of value "key2" ("punctuation" expected with value ",") '.
389415
'around position 11 for expression `{key: foo key2: bar}`.',
390416
],
391417
'incorrect hash assign' => [
392418
'expression' => '{key => foo}',
393419
'names' => ['foo'],
420+
'checks' => 0,
394421
'exception' => 'Unexpected character "=" around position 5 for expression `{key => foo}`.',
395422
],
396423
'incorrect array as hash using' => [
397424
'expression' => '[foo: foo]',
398425
'names' => ['foo'],
426+
'checks' => 0,
399427
'exception' => 'An array element must be followed by a comma. '.
400428
'Unexpected token "punctuation" of value ":" ("punctuation" expected with value ",") '.
401429
'around position 5 for expression `[foo: foo]`.',

src/Symfony/Component/ExpressionLanguage/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"require": {
1919
"php": ">=8.2",
2020
"symfony/cache": "^6.4|^7.0",
21+
"symfony/deprecation-contracts": "^2.5|^3",
2122
"symfony/service-contracts": "^2.5|^3"
2223
},
2324
"autoload": {

0 commit comments

Comments
 (0)
0