Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[ExpressionLanguage] Add more configurability to the parsing/linting methods #53806

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Symfony/Component/ExpressionLanguage/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ CHANGELOG
---

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

7.0
---
Expand Down
20 changes: 15 additions & 5 deletions src/Symfony/Component/ExpressionLanguage/ExpressionLanguage.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,10 @@ public function evaluate(Expression|string $expression, array $values = []): mix

/**
* Parses an expression.
*
* @param int-mask-of<Parser::IGNORE_*> $flags
*/
public function parse(Expression|string $expression, array $names): ParsedExpression
public function parse(Expression|string $expression, array $names, int $flags = 0): ParsedExpression
{
if ($expression instanceof ParsedExpression) {
return $expression;
Expand All @@ -78,7 +80,7 @@ public function parse(Expression|string $expression, array $names): ParsedExpres
$cacheItem = $this->cache->getItem(rawurlencode($expression.'//'.implode('|', $cacheKeyItems)));

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

$cacheItem->set($parsedExpression);
Expand All @@ -91,17 +93,25 @@ public function parse(Expression|string $expression, array $names): ParsedExpres
/**
* Validates the syntax of an expression.
*
* @param array|null $names The list of acceptable variable names in the expression, or null to accept any names
* @param array|null $names The list of acceptable variable names in the expression
* @param int-mask-of<Parser::IGNORE_*> $flags
*
* @throws SyntaxError When the passed expression is invalid
*/
public function lint(Expression|string $expression, ?array $names): void
public function lint(Expression|string $expression, ?array $names, int $flags = 0): void
{
if (null === $names) {
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__);

$flags |= Parser::IGNORE_UNKNOWN_VARIABLES;
$names = [];
}

if ($expression instanceof ParsedExpression) {
return;
}

$this->getParser()->lint($this->getLexer()->tokenize((string) $expression), $names);
$this->getParser()->lint($this->getLexer()->tokenize((string) $expression), $names, $flags);
}

/**
Expand Down
40 changes: 27 additions & 13 deletions src/Symfony/Component/ExpressionLanguage/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@ class Parser
public const OPERATOR_LEFT = 1;
public const OPERATOR_RIGHT = 2;

public const IGNORE_UNKNOWN_VARIABLES = 1;
public const IGNORE_UNKNOWN_FUNCTIONS = 2;

private TokenStream $stream;
private array $unaryOperators;
private array $binaryOperators;
private ?array $names;
private bool $lint = false;
private array $names;
private int $flags = 0;

public function __construct(
private array $functions,
Expand Down Expand Up @@ -87,34 +90,45 @@ public function __construct(
* variable 'container' can be used in the expression
* but the compiled code will use 'this'.
*
* @param int-mask-of<Parser::IGNORE_*> $flags
*
* @throws SyntaxError
*/
public function parse(TokenStream $stream, array $names = []): Node\Node
public function parse(TokenStream $stream, array $names = [], int $flags = 0): Node\Node
{
$this->lint = false;

return $this->doParse($stream, $names);
return $this->doParse($stream, $names, $flags);
}

/**
* Validates the syntax of an expression.
*
* The syntax of the passed expression will be checked, but not parsed.
* If you want to skip checking dynamic variable names, pass `null` instead of the array.
* If you want to skip checking dynamic variable names, pass `Parser::IGNORE_UNKNOWN_VARIABLES` instead of the array.
*
* @param int-mask-of<Parser::IGNORE_*> $flags
*
* @throws SyntaxError When the passed expression is invalid
*/
public function lint(TokenStream $stream, ?array $names = []): void
public function lint(TokenStream $stream, ?array $names = [], int $flags = 0): void
{
$this->lint = true;
$this->doParse($stream, $names);
if (null === $names) {
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__);

$flags |= self::IGNORE_UNKNOWN_VARIABLES;
$names = [];
}

$this->doParse($stream, $names, $flags);
}

/**
* @param int-mask-of<Parser::IGNORE_*> $flags
*
* @throws SyntaxError
*/
private function doParse(TokenStream $stream, ?array $names = []): Node\Node
private function doParse(TokenStream $stream, array $names, int $flags): Node\Node
{
$this->flags = $flags;
$this->stream = $stream;
$this->names = $names;

Expand Down Expand Up @@ -224,13 +238,13 @@ public function parsePrimaryExpression(): Node\Node

default:
if ('(' === $this->stream->current->value) {
if (false === isset($this->functions[$token->value])) {
if (!($this->flags & self::IGNORE_UNKNOWN_FUNCTIONS) && false === isset($this->functions[$token->value])) {
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 (!$this->lint || \is_array($this->names)) {
if (!($this->flags & self::IGNORE_UNKNOWN_VARIABLES)) {
if (!\in_array($token->value, $this->names, true)) {
throw new SyntaxError(sprintf('Variable "%s" is not valid.', $token->value), $token->cursor, $this->stream->getExpression(), $token->value, $this->names);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ public function testRegisterAfterCompile($registerCallback)
public function testLintDoesntThrowOnValidExpression()
{
$el = new ExpressionLanguage();
$el->lint('1 + 1', null);
$el->lint('1 + 1', []);

$this->expectNotToPerformAssertions();
}
Expand Down
38 changes: 33 additions & 5 deletions src/Symfony/Component/ExpressionLanguage/Tests/ParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ public function testNameProposal()
/**
* @dataProvider getLintData
*/
public function testLint($expression, $names, ?string $exception = null)
public function testLint($expression, $names, int $checks = 0, ?string $exception = null)
{
if ($exception) {
$this->expectException(SyntaxError::class);
Expand All @@ -304,7 +304,7 @@ public function testLint($expression, $names, ?string $exception = null)

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

// Parser does't return anything when the correct expression is passed
$this->expectNotToPerformAssertions();
Expand All @@ -321,9 +321,20 @@ public static function getLintData(): array
'expression' => 'foo["some_key"]?.callFunction(a ? b)',
'names' => ['foo', 'a', 'b'],
],
'allow expression without names' => [
'allow expression with unknown names' => [
'expression' => 'foo.bar',
'names' => null,
'names' => [],
'checks' => Parser::IGNORE_UNKNOWN_VARIABLES,
],
'allow expression with unknown functions' => [
'expression' => 'foo()',
'names' => [],
'checks' => Parser::IGNORE_UNKNOWN_FUNCTIONS,
],
'allow expression with unknown functions and names' => [
'expression' => 'foo(bar)',
'names' => [],
'checks' => Parser::IGNORE_UNKNOWN_FUNCTIONS | Parser::IGNORE_UNKNOWN_VARIABLES,
],
'array with trailing comma' => [
'expression' => '[value1, value2, value3,]',
Expand All @@ -333,69 +344,86 @@ public static function getLintData(): array
'expression' => '{val1: value1, val2: value2, val3: value3,}',
'names' => ['value1', 'value2', 'value3'],
],
'disallow expression without names' => [
'disallow expression with unknown names by default' => [
'expression' => 'foo.bar',
'names' => [],
'checks' => 0,
'exception' => 'Variable "foo" is not valid around position 1 for expression `foo.bar',
],
'disallow expression with unknown functions by default' => [
'expression' => 'foo()',
'names' => [],
'checks' => 0,
'exception' => 'The function "foo" does not exist around position 1 for expression `foo()',
],
'operator collisions' => [
'expression' => 'foo.not in [bar]',
'names' => ['foo', 'bar'],
],
'incorrect expression ending' => [
'expression' => 'foo["a"] foo["b"]',
'names' => ['foo'],
'checks' => 0,
'exception' => 'Unexpected token "name" of value "foo" '.
'around position 10 for expression `foo["a"] foo["b"]`.',
],
'incorrect operator' => [
'expression' => 'foo["some_key"] // 2',
'names' => ['foo'],
'checks' => 0,
'exception' => 'Unexpected token "operator" of value "/" '.
'around position 18 for expression `foo["some_key"] // 2`.',
],
'incorrect array' => [
'expression' => '[value1, value2 value3]',
'names' => ['value1', 'value2', 'value3'],
'checks' => 0,
'exception' => 'An array element must be followed by a comma. '.
'Unexpected token "name" of value "value3" ("punctuation" expected with value ",") '.
'around position 17 for expression `[value1, value2 value3]`.',
],
'incorrect array element' => [
'expression' => 'foo["some_key")',
'names' => ['foo'],
'checks' => 0,
'exception' => 'Unclosed "[" around position 3 for expression `foo["some_key")`.',
],
'incorrect hash key' => [
'expression' => '{+: value1}',
'names' => ['value1'],
'checks' => 0,
'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}`.',
],
'missed array key' => [
'expression' => 'foo[]',
'names' => ['foo'],
'checks' => 0,
'exception' => 'Unexpected token "punctuation" of value "]" around position 5 for expression `foo[]`.',
],
'missed closing bracket in sub expression' => [
'expression' => 'foo[(bar ? bar : "default"]',
'names' => ['foo', 'bar'],
'checks' => 0,
'exception' => 'Unclosed "(" around position 4 for expression `foo[(bar ? bar : "default"]`.',
],
'incorrect hash following' => [
'expression' => '{key: foo key2: bar}',
'names' => ['foo', 'bar'],
'checks' => 0,
'exception' => 'A hash value must be followed by a comma. '.
'Unexpected token "name" of value "key2" ("punctuation" expected with value ",") '.
'around position 11 for expression `{key: foo key2: bar}`.',
],
'incorrect hash assign' => [
'expression' => '{key => foo}',
'names' => ['foo'],
'checks' => 0,
'exception' => 'Unexpected character "=" around position 5 for expression `{key => foo}`.',
],
'incorrect array as hash using' => [
'expression' => '[foo: foo]',
'names' => ['foo'],
'checks' => 0,
'exception' => 'An array element must be followed by a comma. '.
'Unexpected token "punctuation" of value ":" ("punctuation" expected with value ",") '.
'around position 5 for expression `[foo: foo]`.',
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/ExpressionLanguage/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"require": {
"php": ">=8.2",
"symfony/cache": "^6.4|^7.0",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/service-contracts": "^2.5|^3"
},
"autoload": {
Expand Down