From d1e2f42e10e9a53b02f81b7a9c333f65679b34e7 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 23 May 2023 17:24:39 +0200 Subject: [PATCH 01/27] [7.0] Bump to PHP 8.2 minimum --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index b123a5c..a516235 100644 --- a/composer.json +++ b/composer.json @@ -16,9 +16,9 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/cache": "^6.4|^7.0", "symfony/service-contracts": "^2.5|^3" }, "autoload": { From 1b1b2d28146961d4d9357e49f089c7e174d7bfee Mon Sep 17 00:00:00 2001 From: Frank Fiebig Date: Tue, 4 Jul 2023 11:21:50 +0200 Subject: [PATCH 02/27] [Lock] 7.0 remove deprecations in Lock Component --- CHANGELOG.md | 5 ++++ Node/BinaryNode.php | 42 ++++++++++++-------------------- Tests/ExpressionLanguageTest.php | 2 +- Tests/Node/BinaryNodeTest.php | 23 ++++++++--------- composer.json | 1 - 5 files changed, 31 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b06620c..f54f943 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.0 +--- + + * The `in` and `not in` operators now use strict comparison + 6.3 --- diff --git a/Node/BinaryNode.php b/Node/BinaryNode.php index 4833116..d1a98a0 100644 --- a/Node/BinaryNode.php +++ b/Node/BinaryNode.php @@ -30,8 +30,8 @@ class BinaryNode extends Node private const FUNCTIONS = [ '**' => 'pow', '..' => 'range', - 'in' => '\\'.self::class.'::inArray', - 'not in' => '!\\'.self::class.'::inArray', + 'in' => '\\in_array', + 'not in' => '!\\in_array', 'contains' => 'str_contains', 'starts with' => 'str_starts_with', 'ends with' => 'str_ends_with', @@ -71,9 +71,14 @@ public function compile(Compiler $compiler): void ->compile($this->nodes['left']) ->raw(', ') ->compile($this->nodes['right']) - ->raw(')') ; + if ('in' === $operator || 'not in' === $operator) { + $compiler->raw(', true'); + } + + $compiler->raw(')'); + return; } @@ -100,12 +105,11 @@ public function evaluate(array $functions, array $values): mixed if (isset(self::FUNCTIONS[$operator])) { $right = $this->nodes['right']->evaluate($functions, $values); - if ('not in' === $operator) { - return !self::inArray($left, $right); - } - $f = self::FUNCTIONS[$operator]; - - return $f($left, $right); + return match ($operator) { + 'in' => \in_array($left, $right, true), + 'not in' => !\in_array($left, $right, true), + default => self::FUNCTIONS[$operator]($left, $right), + }; } switch ($operator) { @@ -143,9 +147,9 @@ public function evaluate(array $functions, array $values): mixed case '<=': return $left <= $right; case 'not in': - return !self::inArray($left, $right); + return !\in_array($left, $right, true); case 'in': - return self::inArray($left, $right); + return \in_array($left, $right, true); case '+': return $left + $right; case '-': @@ -176,22 +180,6 @@ public function toArray(): array return ['(', $this->nodes['left'], ' '.$this->attributes['operator'].' ', $this->nodes['right'], ')']; } - /** - * @internal to be replaced by an inline strict call to in_array() in version 7.0 - */ - public static function inArray($value, array $array): bool - { - if (false === $key = array_search($value, $array)) { - return false; - } - - if (!\in_array($value, $array, true)) { - trigger_deprecation('symfony/expression-language', '6.3', 'The "in" operator will use strict comparisons in Symfony 7.0. Loose match found with key "%s" for value %s. Normalize the array parameter so it only has the expected types or implement loose matching in your own expression function.', $key, json_encode($value)); - } - - return true; - } - private function evaluateMatches(string $regexp, ?string $str): int { set_error_handler(function ($t, $m) use ($regexp) { diff --git a/Tests/ExpressionLanguageTest.php b/Tests/ExpressionLanguageTest.php index bef2395..b2e072b 100644 --- a/Tests/ExpressionLanguageTest.php +++ b/Tests/ExpressionLanguageTest.php @@ -269,7 +269,7 @@ public function testOperatorCollisions() $expressionLanguage = new ExpressionLanguage(); $expression = 'foo.not in [bar]'; $compiled = $expressionLanguage->compile($expression, ['foo', 'bar']); - $this->assertSame('\Symfony\Component\ExpressionLanguage\Node\BinaryNode::inArray($foo->not, [0 => $bar])', $compiled); + $this->assertSame('\in_array($foo->not, [0 => $bar], true)', $compiled); $result = $expressionLanguage->evaluate($expression, ['foo' => (object) ['not' => 'test'], 'bar' => 'test']); $this->assertTrue($result); diff --git a/Tests/Node/BinaryNodeTest.php b/Tests/Node/BinaryNodeTest.php index 610c6b0..bfbcd2d 100644 --- a/Tests/Node/BinaryNodeTest.php +++ b/Tests/Node/BinaryNodeTest.php @@ -11,7 +11,6 @@ namespace Symfony\Component\ExpressionLanguage\Tests\Node; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\ExpressionLanguage\Compiler; use Symfony\Component\ExpressionLanguage\Node\ArrayNode; use Symfony\Component\ExpressionLanguage\Node\BinaryNode; @@ -21,8 +20,6 @@ class BinaryNodeTest extends AbstractNodeTestCase { - use ExpectDeprecationTrait; - public static function getEvaluateData(): array { $array = new ArrayNode(); @@ -116,10 +113,10 @@ public static function getCompileData(): array ['pow(5, 2)', new BinaryNode('**', new ConstantNode(5), new ConstantNode(2))], ['("a" . "b")', new BinaryNode('~', new ConstantNode('a'), new ConstantNode('b'))], - ['\Symfony\Component\ExpressionLanguage\Node\BinaryNode::inArray("a", [0 => "a", 1 => "b"])', new BinaryNode('in', new ConstantNode('a'), $array)], - ['\Symfony\Component\ExpressionLanguage\Node\BinaryNode::inArray("c", [0 => "a", 1 => "b"])', new BinaryNode('in', new ConstantNode('c'), $array)], - ['!\Symfony\Component\ExpressionLanguage\Node\BinaryNode::inArray("c", [0 => "a", 1 => "b"])', new BinaryNode('not in', new ConstantNode('c'), $array)], - ['!\Symfony\Component\ExpressionLanguage\Node\BinaryNode::inArray("a", [0 => "a", 1 => "b"])', new BinaryNode('not in', new ConstantNode('a'), $array)], + ['\in_array("a", [0 => "a", 1 => "b"], true)', new BinaryNode('in', new ConstantNode('a'), $array)], + ['\in_array("c", [0 => "a", 1 => "b"], true)', new BinaryNode('in', new ConstantNode('c'), $array)], + ['!\in_array("c", [0 => "a", 1 => "b"], true)', new BinaryNode('not in', new ConstantNode('c'), $array)], + ['!\in_array("a", [0 => "a", 1 => "b"], true)', new BinaryNode('not in', new ConstantNode('a'), $array)], ['range(1, 3)', new BinaryNode('..', new ConstantNode(1), new ConstantNode(3))], @@ -219,17 +216,17 @@ public function testCompileMatchesWithInvalidRegexpAsExpression() } /** - * @group legacy + * @testWith [1] + * ["true"] */ - public function testInOperatorStrictness() + public function testInOperatorStrictness(mixed $value) { $array = new ArrayNode(); - $array->addElement(new ConstantNode('a')); + $array->addElement(new ConstantNode('1')); $array->addElement(new ConstantNode(true)); - $node = new BinaryNode('in', new ConstantNode('b'), $array); + $node = new BinaryNode('in', new ConstantNode($value), $array); - $this->expectDeprecation('Since symfony/expression-language 6.3: The "in" operator will use strict comparisons in Symfony 7.0. Loose match found with key "1" for value "b". Normalize the array parameter so it only has the expected types or implement loose matching in your own expression function.'); - $this->assertTrue($node->evaluate([], [])); + $this->assertFalse($node->evaluate([], [])); } } diff --git a/composer.json b/composer.json index a516235..b1652e8 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,6 @@ ], "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", "symfony/cache": "^6.4|^7.0", "symfony/service-contracts": "^2.5|^3" }, From d57df58b2d4f948985b4c5dcd433b04fcd356434 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sun, 2 Jul 2023 23:52:21 +0200 Subject: [PATCH 03/27] [Components] Convert to native return types --- Compiler.php | 10 ++----- ExpressionFunctionProviderInterface.php | 2 +- ExpressionLanguage.php | 19 +++--------- Node/FunctionNode.php | 5 +--- Node/Node.php | 29 ++++-------------- ParsedExpression.php | 5 +--- Parser.php | 39 +++++-------------------- SerializedParsedExpression.php | 5 +--- TokenStream.php | 8 ++--- 9 files changed, 26 insertions(+), 96 deletions(-) diff --git a/Compiler.php b/Compiler.php index ab50d36..6ebec06 100644 --- a/Compiler.php +++ b/Compiler.php @@ -28,10 +28,7 @@ public function __construct(array $functions) $this->functions = $functions; } - /** - * @return array - */ - public function getFunction(string $name) + public function getFunction(string $name): array { return $this->functions[$name]; } @@ -66,10 +63,7 @@ public function compile(Node\Node $node): static return $this; } - /** - * @return string - */ - public function subcompile(Node\Node $node) + public function subcompile(Node\Node $node): string { $current = $this->source; $this->source = ''; diff --git a/ExpressionFunctionProviderInterface.php b/ExpressionFunctionProviderInterface.php index 479aeef..272954c 100644 --- a/ExpressionFunctionProviderInterface.php +++ b/ExpressionFunctionProviderInterface.php @@ -19,5 +19,5 @@ interface ExpressionFunctionProviderInterface /** * @return ExpressionFunction[] */ - public function getFunctions(); + public function getFunctions(): array; } diff --git a/ExpressionLanguage.php b/ExpressionLanguage.php index 9e10740..816e319 100644 --- a/ExpressionLanguage.php +++ b/ExpressionLanguage.php @@ -110,13 +110,11 @@ public function lint(Expression|string $expression, ?array $names): void * @param callable $compiler A callable able to compile the function * @param callable $evaluator A callable able to evaluate the function * - * @return void - * * @throws \LogicException when registering a function after calling evaluate(), compile() or parse() * * @see ExpressionFunction */ - public function register(string $name, callable $compiler, callable $evaluator) + public function register(string $name, callable $compiler, callable $evaluator): void { if (isset($this->parser)) { throw new \LogicException('Registering functions after calling evaluate(), compile() or parse() is not supported.'); @@ -125,28 +123,19 @@ public function register(string $name, callable $compiler, callable $evaluator) $this->functions[$name] = ['compiler' => $compiler, 'evaluator' => $evaluator]; } - /** - * @return void - */ - public function addFunction(ExpressionFunction $function) + public function addFunction(ExpressionFunction $function): void { $this->register($function->getName(), $function->getCompiler(), $function->getEvaluator()); } - /** - * @return void - */ - public function registerProvider(ExpressionFunctionProviderInterface $provider) + public function registerProvider(ExpressionFunctionProviderInterface $provider): void { foreach ($provider->getFunctions() as $function) { $this->addFunction($function); } } - /** - * @return void - */ - protected function registerFunctions() + protected function registerFunctions(): void { $this->addFunction(ExpressionFunction::fromPhp('constant')); diff --git a/Node/FunctionNode.php b/Node/FunctionNode.php index 33323f3..6266ba8 100644 --- a/Node/FunctionNode.php +++ b/Node/FunctionNode.php @@ -50,10 +50,7 @@ public function evaluate(array $functions, array $values): mixed return $functions[$this->attributes['name']]['evaluator'](...$arguments); } - /** - * @return array - */ - public function toArray() + public function toArray(): array { $array = []; $array[] = $this->attributes['name']; diff --git a/Node/Node.php b/Node/Node.php index 91fcc36..77254e1 100644 --- a/Node/Node.php +++ b/Node/Node.php @@ -57,20 +57,14 @@ public function __toString(): string return implode("\n", $repr); } - /** - * @return void - */ - public function compile(Compiler $compiler) + public function compile(Compiler $compiler): void { foreach ($this->nodes as $node) { $node->compile($compiler); } } - /** - * @return mixed - */ - public function evaluate(array $functions, array $values) + public function evaluate(array $functions, array $values): mixed { $results = []; foreach ($this->nodes as $node) { @@ -81,19 +75,14 @@ public function evaluate(array $functions, array $values) } /** - * @return array - * * @throws \BadMethodCallException when this node cannot be transformed to an array */ - public function toArray() + public function toArray(): array { throw new \BadMethodCallException(sprintf('Dumping a "%s" instance is not supported yet.', static::class)); } - /** - * @return string - */ - public function dump() + public function dump(): string { $dump = ''; @@ -104,18 +93,12 @@ public function dump() return $dump; } - /** - * @return string - */ - protected function dumpString(string $value) + protected function dumpString(string $value): string { return sprintf('"%s"', addcslashes($value, "\0\t\"\\")); } - /** - * @return bool - */ - protected function isHash(array $value) + protected function isHash(array $value): bool { $expectedKey = 0; diff --git a/ParsedExpression.php b/ParsedExpression.php index 239624e..e6db735 100644 --- a/ParsedExpression.php +++ b/ParsedExpression.php @@ -29,10 +29,7 @@ public function __construct(string $expression, Node $nodes) $this->nodes = $nodes; } - /** - * @return Node - */ - public function getNodes() + public function getNodes(): Node { return $this->nodes; } diff --git a/Parser.php b/Parser.php index a163a7a..8d4ace6 100644 --- a/Parser.php +++ b/Parser.php @@ -130,10 +130,7 @@ private function doParse(TokenStream $stream, ?array $names = []): Node\Node return $node; } - /** - * @return Node\Node - */ - public function parseExpression(int $precedence = 0) + public function parseExpression(int $precedence = 0): Node\Node { $expr = $this->getPrimary(); $token = $this->stream->current; @@ -154,10 +151,7 @@ public function parseExpression(int $precedence = 0) return $expr; } - /** - * @return Node\Node - */ - protected function getPrimary() + protected function getPrimary(): Node\Node { $token = $this->stream->current; @@ -180,10 +174,7 @@ protected function getPrimary() return $this->parsePrimaryExpression(); } - /** - * @return Node\Node - */ - protected function parseConditionalExpression(Node\Node $expr) + protected function parseConditionalExpression(Node\Node $expr): Node\Node { while ($this->stream->current->test(Token::PUNCTUATION_TYPE, '??')) { $this->stream->next(); @@ -214,10 +205,7 @@ protected function parseConditionalExpression(Node\Node $expr) return $expr; } - /** - * @return Node\Node - */ - public function parsePrimaryExpression() + public function parsePrimaryExpression(): Node\Node { $token = $this->stream->current; switch ($token->type) { @@ -282,10 +270,7 @@ public function parsePrimaryExpression() return $this->parsePostfixExpression($node); } - /** - * @return Node\ArrayNode - */ - public function parseArrayExpression() + public function parseArrayExpression(): Node\ArrayNode { $this->stream->expect(Token::PUNCTUATION_TYPE, '[', 'An array element was expected'); @@ -309,10 +294,7 @@ public function parseArrayExpression() return $node; } - /** - * @return Node\ArrayNode - */ - public function parseHashExpression() + public function parseHashExpression(): Node\ArrayNode { $this->stream->expect(Token::PUNCTUATION_TYPE, '{', 'A hash element was expected'); @@ -356,10 +338,7 @@ public function parseHashExpression() return $node; } - /** - * @return Node\GetAttrNode|Node\Node - */ - public function parsePostfixExpression(Node\Node $node) + public function parsePostfixExpression(Node\Node $node): Node\GetAttrNode|Node\Node { $token = $this->stream->current; while (Token::PUNCTUATION_TYPE == $token->type) { @@ -418,10 +397,8 @@ public function parsePostfixExpression(Node\Node $node) /** * Parses arguments. - * - * @return Node\Node */ - public function parseArguments() + public function parseArguments(): Node\Node { $args = []; $this->stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); diff --git a/SerializedParsedExpression.php b/SerializedParsedExpression.php index 5691907..a3f8e73 100644 --- a/SerializedParsedExpression.php +++ b/SerializedParsedExpression.php @@ -32,10 +32,7 @@ public function __construct(string $expression, string $nodes) $this->nodes = $nodes; } - /** - * @return Node - */ - public function getNodes() + public function getNodes(): Node { return unserialize($this->nodes); } diff --git a/TokenStream.php b/TokenStream.php index 241725b..ab04de4 100644 --- a/TokenStream.php +++ b/TokenStream.php @@ -41,10 +41,8 @@ public function __toString(): string /** * Sets the pointer to the next token and returns the old one. - * - * @return void */ - public function next() + public function next(): void { ++$this->position; @@ -57,10 +55,8 @@ public function next() /** * @param string|null $message The syntax error message - * - * @return void */ - public function expect(string $type, string $value = null, string $message = null) + public function expect(string $type, string $value = null, string $message = null): void { $token = $this->current; if (!$token->test($type, $value)) { From f4145a5fe0e0ff749b7aea2b855dcd073550da9c Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 5 Jul 2023 16:34:47 +0200 Subject: [PATCH 04/27] [ExpressionLanguage] Revert native return types on ExpressionLanguage::registerFunctions() --- ExpressionLanguage.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ExpressionLanguage.php b/ExpressionLanguage.php index 816e319..e8200d9 100644 --- a/ExpressionLanguage.php +++ b/ExpressionLanguage.php @@ -135,7 +135,10 @@ public function registerProvider(ExpressionFunctionProviderInterface $provider): } } - protected function registerFunctions(): void + /** + * @return void + */ + protected function registerFunctions() { $this->addFunction(ExpressionFunction::fromPhp('constant')); From 46520d885b340f44a369967835a4bec178d7ab3d Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 21 Jul 2023 15:36:26 +0200 Subject: [PATCH 05/27] Add types to public and protected properties --- Expression.php | 2 +- Node/Node.php | 4 ++-- Token.php | 6 +++--- TokenStream.php | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Expression.php b/Expression.php index 3cab1df..8bfa1ec 100644 --- a/Expression.php +++ b/Expression.php @@ -18,7 +18,7 @@ */ class Expression { - protected $expression; + protected string $expression; public function __construct(string $expression) { diff --git a/Node/Node.php b/Node/Node.php index 77254e1..4ba5e5c 100644 --- a/Node/Node.php +++ b/Node/Node.php @@ -20,8 +20,8 @@ */ class Node { - public $nodes = []; - public $attributes = []; + public array $nodes = []; + public array $attributes = []; /** * @param array $nodes An array of nodes diff --git a/Token.php b/Token.php index 6eff31e..b15667b 100644 --- a/Token.php +++ b/Token.php @@ -18,9 +18,9 @@ */ class Token { - public $value; - public $type; - public $cursor; + public string $type; + public string|int|float|null $value; + public ?int $cursor; public const EOF_TYPE = 'end of expression'; public const NAME_TYPE = 'name'; diff --git a/TokenStream.php b/TokenStream.php index ab04de4..da9e3ac 100644 --- a/TokenStream.php +++ b/TokenStream.php @@ -18,7 +18,7 @@ */ class TokenStream { - public $current; + public Token $current; private array $tokens; private int $position = 0; From dcd51b36a92f1966478ad0fca1f51a00c2bb0065 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 27 Dec 2023 22:07:59 +0100 Subject: [PATCH 06/27] [ExpressionLanguage] Fix typo --- Parser.php | 2 +- SerializedParsedExpression.php | 2 +- Token.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Parser.php b/Parser.php index 8d4ace6..7ac7e62 100644 --- a/Parser.php +++ b/Parser.php @@ -12,7 +12,7 @@ namespace Symfony\Component\ExpressionLanguage; /** - * Parsers a token stream. + * Parses a token stream. * * This parser implements a "Precedence climbing" algorithm. * diff --git a/SerializedParsedExpression.php b/SerializedParsedExpression.php index a3f8e73..7ccd208 100644 --- a/SerializedParsedExpression.php +++ b/SerializedParsedExpression.php @@ -14,7 +14,7 @@ use Symfony\Component\ExpressionLanguage\Node\Node; /** - * Represents an already parsed expression. + * Represents an already serialized parsed expression. * * @author Fabien Potencier */ diff --git a/Token.php b/Token.php index b15667b..c5196c8 100644 --- a/Token.php +++ b/Token.php @@ -12,7 +12,7 @@ namespace Symfony\Component\ExpressionLanguage; /** - * Represents a Token. + * Represents a token. * * @author Fabien Potencier */ From 8b973ababb2725456d1f64c16417b24169ea2ddd Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Mon, 8 Jan 2024 08:27:24 +0100 Subject: [PATCH 07/27] [Dotenv][ErrorHandler][EventDispatcher] Use CPP --- Compiler.php | 7 +++---- Expression.php | 8 +++----- ExpressionFunction.php | 9 +++++---- ParsedExpression.php | 10 ++++------ Parser.php | 8 +++----- SerializedParsedExpression.php | 9 ++++----- Token.php | 14 +++++--------- 7 files changed, 27 insertions(+), 38 deletions(-) diff --git a/Compiler.php b/Compiler.php index 6ebec06..9f470eb 100644 --- a/Compiler.php +++ b/Compiler.php @@ -21,11 +21,10 @@ class Compiler implements ResetInterface { private string $source = ''; - private array $functions; - public function __construct(array $functions) - { - $this->functions = $functions; + public function __construct( + private array $functions, + ) { } public function getFunction(string $name): array diff --git a/Expression.php b/Expression.php index 8bfa1ec..b101969 100644 --- a/Expression.php +++ b/Expression.php @@ -18,11 +18,9 @@ */ class Expression { - protected string $expression; - - public function __construct(string $expression) - { - $this->expression = $expression; + public function __construct( + protected string $expression, + ) { } /** diff --git a/ExpressionFunction.php b/ExpressionFunction.php index d0ddd10..5bd857d 100644 --- a/ExpressionFunction.php +++ b/ExpressionFunction.php @@ -30,7 +30,6 @@ */ class ExpressionFunction { - private string $name; private \Closure $compiler; private \Closure $evaluator; @@ -39,9 +38,11 @@ class ExpressionFunction * @param callable $compiler A callable able to compile the function * @param callable $evaluator A callable able to evaluate the function */ - public function __construct(string $name, callable $compiler, callable $evaluator) - { - $this->name = $name; + public function __construct( + private string $name, + callable $compiler, + callable $evaluator, + ) { $this->compiler = $compiler(...); $this->evaluator = $evaluator(...); } diff --git a/ParsedExpression.php b/ParsedExpression.php index e6db735..7002b85 100644 --- a/ParsedExpression.php +++ b/ParsedExpression.php @@ -20,13 +20,11 @@ */ class ParsedExpression extends Expression { - private Node $nodes; - - public function __construct(string $expression, Node $nodes) - { + public function __construct( + string $expression, + private Node $nodes, + ) { parent::__construct($expression); - - $this->nodes = $nodes; } public function getNodes(): Node diff --git a/Parser.php b/Parser.php index 7ac7e62..3198a09 100644 --- a/Parser.php +++ b/Parser.php @@ -29,14 +29,12 @@ class Parser private TokenStream $stream; private array $unaryOperators; private array $binaryOperators; - private array $functions; private ?array $names; private bool $lint = false; - public function __construct(array $functions) - { - $this->functions = $functions; - + public function __construct( + private array $functions, + ) { $this->unaryOperators = [ 'not' => ['precedence' => 50], '!' => ['precedence' => 50], diff --git a/SerializedParsedExpression.php b/SerializedParsedExpression.php index 7ccd208..ca7d405 100644 --- a/SerializedParsedExpression.php +++ b/SerializedParsedExpression.php @@ -20,16 +20,15 @@ */ class SerializedParsedExpression extends ParsedExpression { - private string $nodes; - /** * @param string $expression An expression * @param string $nodes The serialized nodes for the expression */ - public function __construct(string $expression, string $nodes) - { + public function __construct( + string $expression, + private string $nodes, + ) { $this->expression = $expression; - $this->nodes = $nodes; } public function getNodes(): Node diff --git a/Token.php b/Token.php index c5196c8..14ed277 100644 --- a/Token.php +++ b/Token.php @@ -18,10 +18,6 @@ */ class Token { - public string $type; - public string|int|float|null $value; - public ?int $cursor; - public const EOF_TYPE = 'end of expression'; public const NAME_TYPE = 'name'; public const NUMBER_TYPE = 'number'; @@ -33,11 +29,11 @@ class Token * @param self::*_TYPE $type * @param int|null $cursor The cursor position in the source */ - public function __construct(string $type, string|int|float|null $value, ?int $cursor) - { - $this->type = $type; - $this->value = $value; - $this->cursor = $cursor; + public function __construct( + public string $type, + public string|int|float|null $value, + public ?int $cursor, + ) { } /** From a1f60dff5f6770df22c469162adf0ce3b0e2a194 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Tue, 28 Nov 2023 13:12:45 +0100 Subject: [PATCH 08/27] [ExpressionLanguage][PasswordHasher][Stopwatch] Improve overall coverage --- Tests/ExpressionLanguageTest.php | 26 ++++++++++++++++++++++++++ Tests/ParserTest.php | 24 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/Tests/ExpressionLanguageTest.php b/Tests/ExpressionLanguageTest.php index ed926ff..bd25f09 100644 --- a/Tests/ExpressionLanguageTest.php +++ b/Tests/ExpressionLanguageTest.php @@ -168,6 +168,14 @@ public function testParseThrowsInsteadOfNotice() $expressionLanguage->parse('node.', ['node']); } + public function testParseReturnsObjectOnAlreadyParsedExpression() + { + $expressionLanguage = new ExpressionLanguage(); + $expression = $expressionLanguage->parse('1 + 1', []); + + $this->assertSame($expression, $expressionLanguage->parse($expression, [])); + } + public static function shortCircuitProviderEvaluate() { $object = new class(static::fail(...)) { @@ -440,6 +448,24 @@ public function testRegisterAfterCompile($registerCallback) $registerCallback($el); } + public function testLintDoesntThrowOnValidExpression() + { + $el = new ExpressionLanguage(); + $el->lint('1 + 1', null); + + $this->expectNotToPerformAssertions(); + } + + public function testLintThrowsOnInvalidExpression() + { + $el = new ExpressionLanguage(); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('Unexpected end of expression around position 6 for expression `node.`.'); + + $el->lint('node.', ['node']); + } + public static function getRegisterCallbacks() { return [ diff --git a/Tests/ParserTest.php b/Tests/ParserTest.php index 58a232e..e109ec2 100644 --- a/Tests/ParserTest.php +++ b/Tests/ParserTest.php @@ -37,6 +37,17 @@ public function testParseWithZeroInNames() $parser->parse($lexer->tokenize('foo'), [0]); } + public function testParseUnknownFunction() + { + $parser = new Parser([]); + $tokenized = (new Lexer())->tokenize('foo()'); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('The function "foo" does not exist around position 1 for expression `foo()`.'); + + $parser->parse($tokenized); + } + /** * @dataProvider getParseData */ @@ -314,6 +325,14 @@ public static function getLintData(): array 'expression' => 'foo.bar', 'names' => null, ], + 'array with trailing comma' => [ + 'expression' => '[value1, value2, value3,]', + 'names' => ['value1', 'value2', 'value3'], + ], + 'hashmap with trailing comma' => [ + 'expression' => '{val1: value1, val2: value2, val3: value3,}', + 'names' => ['value1', 'value2', 'value3'], + ], 'disallow expression without names' => [ 'expression' => 'foo.bar', 'names' => [], @@ -347,6 +366,11 @@ public static function getLintData(): array 'names' => ['foo'], 'exception' => 'Unclosed "[" around position 3 for expression `foo["some_key")`.', ], + 'incorrect hash key' => [ + 'expression' => '{+: value1}', + 'names' => ['value1'], + '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'], From 0c4ed167755fa595e9643f6b3b42d98145aa4797 Mon Sep 17 00:00:00 2001 From: Maximilian Beckers Date: Fri, 2 Feb 2024 07:53:28 +0100 Subject: [PATCH 09/27] [ExpressionLanguage] Add ``min`` and ``max`` php functions --- CHANGELOG.md | 5 +++++ ExpressionLanguage.php | 5 ++++- Tests/ExpressionLanguageTest.php | 18 ++++++++++++++---- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f54f943..9d119be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add support for php `min` and `max` functions + 7.0 --- diff --git a/ExpressionLanguage.php b/ExpressionLanguage.php index ac1b9a2..bd9dbfc 100644 --- a/ExpressionLanguage.php +++ b/ExpressionLanguage.php @@ -140,7 +140,10 @@ public function registerProvider(ExpressionFunctionProviderInterface $provider): */ protected function registerFunctions() { - $this->addFunction(ExpressionFunction::fromPhp('constant')); + $basicPhpFunctions = ['constant', 'min', 'max']; + foreach ($basicPhpFunctions as $function) { + $this->addFunction(ExpressionFunction::fromPhp($function)); + } $this->addFunction(new ExpressionFunction('enum', static fn ($str): string => sprintf("(\constant(\$v = (%s))) instanceof \UnitEnum ? \constant(\$v) : throw new \TypeError(\sprintf('The string \"%%s\" is not the name of a valid enum case.', \$v))", $str), diff --git a/Tests/ExpressionLanguageTest.php b/Tests/ExpressionLanguageTest.php index 515b18d..f7f712d 100644 --- a/Tests/ExpressionLanguageTest.php +++ b/Tests/ExpressionLanguageTest.php @@ -71,13 +71,23 @@ public function testCachedParse() $this->assertSame($savedParsedExpression, $parsedExpression); } - public function testConstantFunction() + /** + * @dataProvider basicPhpFunctionProvider + */ + public function testBasicPhpFunction($expression, $expected, $compiled) { $expressionLanguage = new ExpressionLanguage(); - $this->assertEquals(\PHP_VERSION, $expressionLanguage->evaluate('constant("PHP_VERSION")')); + $this->assertEquals($expected, $expressionLanguage->evaluate($expression)); + $this->assertEquals($compiled, $expressionLanguage->compile($expression)); + } - $expressionLanguage = new ExpressionLanguage(); - $this->assertEquals('\constant("PHP_VERSION")', $expressionLanguage->compile('constant("PHP_VERSION")')); + public static function basicPhpFunctionProvider() + { + return [ + ['constant("PHP_VERSION")', \PHP_VERSION, '\constant("PHP_VERSION")'], + ['min(1,2,3)', 1, '\min(1, 2, 3)'], + ['max(1,2,3)', 3, '\max(1, 2, 3)'], + ]; } public function testEnumFunctionWithConstantThrows() From d19d56c073bfbd1b50f393d651429ed8fcf5eb0a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 3 Feb 2024 13:50:45 +0100 Subject: [PATCH 10/27] Fix typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d119be..f5c26e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ CHANGELOG 7.1 --- - * Add support for php `min` and `max` functions + * Add support for PHP `min` and `max` functions 7.0 --- From 684ac492ec4e86041ae6800ecbd3a4c9883add90 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 6 Feb 2024 09:26:56 +0100 Subject: [PATCH 11/27] [ExpressionLanguage] Add more configurability to the parsing/linting methods --- CHANGELOG.md | 2 ++ ExpressionLanguage.php | 20 ++++++++++++---- Parser.php | 40 +++++++++++++++++++++----------- Tests/ExpressionLanguageTest.php | 2 +- Tests/ParserTest.php | 38 ++++++++++++++++++++++++++---- composer.json | 1 + 6 files changed, 79 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5c26e6..52a8560 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 --- diff --git a/ExpressionLanguage.php b/ExpressionLanguage.php index bd9dbfc..978a068 100644 --- a/ExpressionLanguage.php +++ b/ExpressionLanguage.php @@ -61,8 +61,10 @@ public function evaluate(Expression|string $expression, array $values = []): mix /** * Parses an expression. + * + * @param int-mask-of $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; @@ -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); @@ -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 $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); } /** diff --git a/Parser.php b/Parser.php index 3198a09..1708d18 100644 --- a/Parser.php +++ b/Parser.php @@ -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, @@ -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 $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 $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 $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; @@ -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); } diff --git a/Tests/ExpressionLanguageTest.php b/Tests/ExpressionLanguageTest.php index f7f712d..5fa2318 100644 --- a/Tests/ExpressionLanguageTest.php +++ b/Tests/ExpressionLanguageTest.php @@ -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(); } diff --git a/Tests/ParserTest.php b/Tests/ParserTest.php index e3170ba..7b71dbb 100644 --- a/Tests/ParserTest.php +++ b/Tests/ParserTest.php @@ -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); @@ -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(); @@ -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,]', @@ -333,11 +344,18 @@ 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'], @@ -345,18 +363,21 @@ public static function getLintData(): array '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]`.', @@ -364,26 +385,31 @@ public static function getLintData(): array '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}`.', @@ -391,11 +417,13 @@ public static function getLintData(): array '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]`.', diff --git a/composer.json b/composer.json index b1652e8..e24a315 100644 --- a/composer.json +++ b/composer.json @@ -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": { From 679e0bc385e9d7cf6a25449e68e8b6adb502ae59 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 9 Feb 2024 12:59:26 +0100 Subject: [PATCH 12/27] document deprecation and fix test --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52a8560..1cd4fcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,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. + * Deprecate passing `null` as the allowed variable names to `ExpressionLanguage::lint()` and `Parser::lint()`, + pass the `IGNORE_UNKNOWN_VARIABLES` flag instead to ignore unknown variables during linting 7.0 --- From 93e788339d36d933a181f4430010eae96bd34632 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Thu, 30 May 2024 14:09:55 +0200 Subject: [PATCH 13/27] Add missing UPGRADE notes for 7.1 --- ExpressionLanguage.php | 2 +- Parser.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ExpressionLanguage.php b/ExpressionLanguage.php index 978a068..22112e9 100644 --- a/ExpressionLanguage.php +++ b/ExpressionLanguage.php @@ -101,7 +101,7 @@ public function parse(Expression|string $expression, array $names, int $flags = 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__); + trigger_deprecation('symfony/expression-language', '7.1', 'Passing "null" as the second argument of "%s()" is deprecated, pass "%s\Parser::IGNORE_UNKNOWN_VARIABLES" instead as a third argument.', __METHOD__, __NAMESPACE__); $flags |= Parser::IGNORE_UNKNOWN_VARIABLES; $names = []; diff --git a/Parser.php b/Parser.php index 1708d18..d34e742 100644 --- a/Parser.php +++ b/Parser.php @@ -112,7 +112,7 @@ public function parse(TokenStream $stream, array $names = [], int $flags = 0): N public function lint(TokenStream $stream, ?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__); + trigger_deprecation('symfony/expression-language', '7.1', 'Passing "null" as the second argument of "%s()" is deprecated, pass "%s::IGNORE_UNKNOWN_VARIABLES" instead as a third argument.', __METHOD__, __CLASS__); $flags |= self::IGNORE_UNKNOWN_VARIABLES; $names = []; From 15b4b970d017b2febea8207047a814c83fc72ea4 Mon Sep 17 00:00:00 2001 From: Adam Kiss Date: Sun, 28 Apr 2024 19:51:43 +0200 Subject: [PATCH 14/27] [ExpressionLanguage] Support non-existent names when followed by null coalescing --- CHANGELOG.md | 5 +++ Node/NullCoalescedNameNode.php | 45 ++++++++++++++++++++++++ Parser.php | 4 +++ Tests/ExpressionLanguageTest.php | 1 + Tests/Node/NullCoalescedNameNodeTest.php | 38 ++++++++++++++++++++ 5 files changed, 93 insertions(+) create mode 100644 Node/NullCoalescedNameNode.php create mode 100644 Tests/Node/NullCoalescedNameNodeTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cd4fcb..4331d72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.2 +--- + + * Add support for null-coalescing unknown variables + 7.1 --- diff --git a/Node/NullCoalescedNameNode.php b/Node/NullCoalescedNameNode.php new file mode 100644 index 0000000..e4b4f1d --- /dev/null +++ b/Node/NullCoalescedNameNode.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Node; + +use Symfony\Component\ExpressionLanguage\Compiler; + +/** + * @author Adam Kiss + * + * @internal + */ +class NullCoalescedNameNode extends Node +{ + public function __construct(string $name) + { + parent::__construct( + [], + ['name' => $name] + ); + } + + public function compile(Compiler $compiler): void + { + $compiler->raw('$'.$this->attributes['name'].' ?? null'); + } + + public function evaluate(array $functions, array $values): null + { + return null; + } + + public function toArray(): array + { + return [$this->attributes['name'].' ?? null']; + } +} diff --git a/Parser.php b/Parser.php index 1708d18..6c64813 100644 --- a/Parser.php +++ b/Parser.php @@ -246,6 +246,10 @@ public function parsePrimaryExpression(): Node\Node } else { if (!($this->flags & self::IGNORE_UNKNOWN_VARIABLES)) { if (!\in_array($token->value, $this->names, true)) { + if ($this->stream->current->test(Token::PUNCTUATION_TYPE, '??')) { + return new Node\NullCoalescedNameNode($token->value); + } + throw new SyntaxError(sprintf('Variable "%s" is not valid.', $token->value), $token->cursor, $this->stream->getExpression(), $token->value, $this->names); } diff --git a/Tests/ExpressionLanguageTest.php b/Tests/ExpressionLanguageTest.php index 5fa2318..fbd50c9 100644 --- a/Tests/ExpressionLanguageTest.php +++ b/Tests/ExpressionLanguageTest.php @@ -433,6 +433,7 @@ public function bar() } }; + yield ['bar ?? "default"', null]; yield ['foo.bar ?? "default"', null]; yield ['foo.bar.baz ?? "default"', (object) ['bar' => null]]; yield ['foo.bar ?? foo.baz ?? "default"', null]; diff --git a/Tests/Node/NullCoalescedNameNodeTest.php b/Tests/Node/NullCoalescedNameNodeTest.php new file mode 100644 index 0000000..c5baef9 --- /dev/null +++ b/Tests/Node/NullCoalescedNameNodeTest.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Node\NullCoalescedNameNode; + +class NullCoalescedNameNodeTest extends AbstractNodeTestCase +{ + public static function getEvaluateData(): array + { + return [ + [null, new NullCoalescedNameNode('foo'), []], + ]; + } + + public static function getCompileData(): array + { + return [ + ['$foo ?? null', new NullCoalescedNameNode('foo')], + ]; + } + + public static function getDumpData(): array + { + return [ + ['foo ?? null', new NullCoalescedNameNode('foo')], + ]; + } +} From 0418f90b17d3fb465cf69f0fef75c94df00e89e7 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 3 Jun 2024 15:27:28 +0200 Subject: [PATCH 15/27] use constructor property promotion --- Node/ConstantNode.php | 12 +++++------- TokenStream.php | 10 ++++------ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/Node/ConstantNode.php b/Node/ConstantNode.php index 37beee8..856cd47 100644 --- a/Node/ConstantNode.php +++ b/Node/ConstantNode.php @@ -20,13 +20,11 @@ */ class ConstantNode extends Node { - public readonly bool $isNullSafe; - private bool $isIdentifier; - - public function __construct(mixed $value, bool $isIdentifier = false, bool $isNullSafe = false) - { - $this->isIdentifier = $isIdentifier; - $this->isNullSafe = $isNullSafe; + public function __construct( + mixed $value, + private bool $isIdentifier = false, + public readonly bool $isNullSafe = false, + ) { parent::__construct( [], ['value' => $value] diff --git a/TokenStream.php b/TokenStream.php index a7f6735..67acc44 100644 --- a/TokenStream.php +++ b/TokenStream.php @@ -20,15 +20,13 @@ class TokenStream { public Token $current; - private array $tokens; private int $position = 0; - private string $expression; - public function __construct(array $tokens, string $expression = '') - { - $this->tokens = $tokens; + public function __construct( + private array $tokens, + private string $expression = '', + ) { $this->current = $tokens[0]; - $this->expression = $expression; } /** From 9b89e33d7b3aafc1cc002d12d525a6c2def0491e Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 20 Jun 2024 17:52:34 +0200 Subject: [PATCH 16/27] Prefix all sprintf() calls --- Compiler.php | 2 +- ExpressionFunction.php | 6 +++--- ExpressionLanguage.php | 4 ++-- Lexer.php | 8 ++++---- Node/BinaryNode.php | 4 ++-- Node/GetAttrNode.php | 8 ++++---- Node/Node.php | 6 +++--- Parser.php | 10 +++++----- SyntaxError.php | 6 +++--- Tests/ExpressionLanguageTest.php | 12 ++++++------ Tests/Node/FunctionNodeTest.php | 2 +- Token.php | 2 +- TokenStream.php | 2 +- 13 files changed, 36 insertions(+), 36 deletions(-) diff --git a/Compiler.php b/Compiler.php index 9f470eb..97f6fb5 100644 --- a/Compiler.php +++ b/Compiler.php @@ -94,7 +94,7 @@ public function raw(string $string): static */ public function string(string $value): static { - $this->source .= sprintf('"%s"', addcslashes($value, "\0\t\"\$\\")); + $this->source .= \sprintf('"%s"', addcslashes($value, "\0\t\"\$\\")); return $this; } diff --git a/ExpressionFunction.php b/ExpressionFunction.php index 8950e21..d9a537c 100644 --- a/ExpressionFunction.php +++ b/ExpressionFunction.php @@ -75,15 +75,15 @@ public static function fromPhp(string $phpFunctionName, ?string $expressionFunct { $phpFunctionName = ltrim($phpFunctionName, '\\'); if (!\function_exists($phpFunctionName)) { - throw new \InvalidArgumentException(sprintf('PHP function "%s" does not exist.', $phpFunctionName)); + throw new \InvalidArgumentException(\sprintf('PHP function "%s" does not exist.', $phpFunctionName)); } $parts = explode('\\', $phpFunctionName); if (!$expressionFunctionName && \count($parts) > 1) { - throw new \InvalidArgumentException(sprintf('An expression function name must be defined when PHP function "%s" is namespaced.', $phpFunctionName)); + throw new \InvalidArgumentException(\sprintf('An expression function name must be defined when PHP function "%s" is namespaced.', $phpFunctionName)); } - $compiler = fn (...$args) => sprintf('\%s(%s)', $phpFunctionName, implode(', ', $args)); + $compiler = fn (...$args) => \sprintf('\%s(%s)', $phpFunctionName, implode(', ', $args)); $evaluator = fn ($p, ...$args) => $phpFunctionName(...$args); diff --git a/ExpressionLanguage.php b/ExpressionLanguage.php index 22112e9..055bef2 100644 --- a/ExpressionLanguage.php +++ b/ExpressionLanguage.php @@ -156,12 +156,12 @@ protected function registerFunctions() } $this->addFunction(new ExpressionFunction('enum', - static fn ($str): string => sprintf("(\constant(\$v = (%s))) instanceof \UnitEnum ? \constant(\$v) : throw new \TypeError(\sprintf('The string \"%%s\" is not the name of a valid enum case.', \$v))", $str), + static fn ($str): string => \sprintf("(\constant(\$v = (%s))) instanceof \UnitEnum ? \constant(\$v) : throw new \TypeError(\sprintf('The string \"%%s\" is not the name of a valid enum case.', \$v))", $str), static function ($arguments, $str): \UnitEnum { $value = \constant($str); if (!$value instanceof \UnitEnum) { - throw new \TypeError(sprintf('The string "%s" is not the name of a valid enum case.', $str)); + throw new \TypeError(\sprintf('The string "%s" is not the name of a valid enum case.', $str)); } return $value; diff --git a/Lexer.php b/Lexer.php index ab32342..17092f5 100644 --- a/Lexer.php +++ b/Lexer.php @@ -55,12 +55,12 @@ public function tokenize(string $expression): TokenStream } elseif (str_contains(')]}', $expression[$cursor])) { // closing bracket if (!$brackets) { - throw new SyntaxError(sprintf('Unexpected "%s".', $expression[$cursor]), $cursor, $expression); + throw new SyntaxError(\sprintf('Unexpected "%s".', $expression[$cursor]), $cursor, $expression); } [$expect, $cur] = array_pop($brackets); if ($expression[$cursor] != strtr($expect, '([{', ')]}')) { - throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $cur, $expression); + throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $cur, $expression); } $tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1); @@ -91,7 +91,7 @@ public function tokenize(string $expression): TokenStream $cursor += \strlen($match[0]); } else { // unlexable - throw new SyntaxError(sprintf('Unexpected character "%s".', $expression[$cursor]), $cursor, $expression); + throw new SyntaxError(\sprintf('Unexpected character "%s".', $expression[$cursor]), $cursor, $expression); } } @@ -99,7 +99,7 @@ public function tokenize(string $expression): TokenStream if ($brackets) { [$expect, $cur] = array_pop($brackets); - throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $cur, $expression); + throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $cur, $expression); } return new TokenStream($tokens, $expression); diff --git a/Node/BinaryNode.php b/Node/BinaryNode.php index 5b365c2..4065ed6 100644 --- a/Node/BinaryNode.php +++ b/Node/BinaryNode.php @@ -67,7 +67,7 @@ public function compile(Compiler $compiler): void if (isset(self::FUNCTIONS[$operator])) { $compiler - ->raw(sprintf('%s(', self::FUNCTIONS[$operator])) + ->raw(\sprintf('%s(', self::FUNCTIONS[$operator])) ->compile($this->nodes['left']) ->raw(', ') ->compile($this->nodes['right']) @@ -182,7 +182,7 @@ public function toArray(): array private function evaluateMatches(string $regexp, ?string $str): int { - set_error_handler(static fn ($t, $m) => throw new SyntaxError(sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12))); + set_error_handler(static fn ($t, $m) => throw new SyntaxError(\sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12))); try { return preg_match($regexp, (string) $str); } finally { diff --git a/Node/GetAttrNode.php b/Node/GetAttrNode.php index 984247e..6460744 100644 --- a/Node/GetAttrNode.php +++ b/Node/GetAttrNode.php @@ -83,7 +83,7 @@ public function evaluate(array $functions, array $values): mixed } if (!\is_object($obj)) { - throw new \RuntimeException(sprintf('Unable to get property "%s" of non-object "%s".', $this->nodes['attribute']->dump(), $this->nodes['node']->dump())); + throw new \RuntimeException(\sprintf('Unable to get property "%s" of non-object "%s".', $this->nodes['attribute']->dump(), $this->nodes['node']->dump())); } $property = $this->nodes['attribute']->attributes['value']; @@ -107,10 +107,10 @@ public function evaluate(array $functions, array $values): mixed } if (!\is_object($obj)) { - throw new \RuntimeException(sprintf('Unable to call method "%s" of non-object "%s".', $this->nodes['attribute']->dump(), $this->nodes['node']->dump())); + throw new \RuntimeException(\sprintf('Unable to call method "%s" of non-object "%s".', $this->nodes['attribute']->dump(), $this->nodes['node']->dump())); } if (!\is_callable($toCall = [$obj, $this->nodes['attribute']->attributes['value']])) { - throw new \RuntimeException(sprintf('Unable to call method "%s" of object "%s".', $this->nodes['attribute']->attributes['value'], get_debug_type($obj))); + throw new \RuntimeException(\sprintf('Unable to call method "%s" of object "%s".', $this->nodes['attribute']->attributes['value'], get_debug_type($obj))); } return $toCall(...array_values($this->nodes['arguments']->evaluate($functions, $values))); @@ -123,7 +123,7 @@ public function evaluate(array $functions, array $values): mixed } if (!\is_array($array) && !$array instanceof \ArrayAccess && !(null === $array && $this->attributes['is_null_coalesce'])) { - throw new \RuntimeException(sprintf('Unable to get an item of non-array "%s".', $this->nodes['node']->dump())); + throw new \RuntimeException(\sprintf('Unable to get an item of non-array "%s".', $this->nodes['node']->dump())); } if ($this->attributes['is_null_coalesce']) { diff --git a/Node/Node.php b/Node/Node.php index 4ba5e5c..e14ed45 100644 --- a/Node/Node.php +++ b/Node/Node.php @@ -37,7 +37,7 @@ public function __toString(): string { $attributes = []; foreach ($this->attributes as $name => $value) { - $attributes[] = sprintf('%s: %s', $name, str_replace("\n", '', var_export($value, true))); + $attributes[] = \sprintf('%s: %s', $name, str_replace("\n", '', var_export($value, true))); } $repr = [str_replace('Symfony\Component\ExpressionLanguage\Node\\', '', static::class).'('.implode(', ', $attributes)]; @@ -79,7 +79,7 @@ public function evaluate(array $functions, array $values): mixed */ public function toArray(): array { - throw new \BadMethodCallException(sprintf('Dumping a "%s" instance is not supported yet.', static::class)); + throw new \BadMethodCallException(\sprintf('Dumping a "%s" instance is not supported yet.', static::class)); } public function dump(): string @@ -95,7 +95,7 @@ public function dump(): string protected function dumpString(string $value): string { - return sprintf('"%s"', addcslashes($value, "\0\t\"\\")); + return \sprintf('"%s"', addcslashes($value, "\0\t\"\\")); } protected function isHash(array $value): bool diff --git a/Parser.php b/Parser.php index b122c6e..da36770 100644 --- a/Parser.php +++ b/Parser.php @@ -134,7 +134,7 @@ private function doParse(TokenStream $stream, array $names, int $flags): Node\No $node = $this->parseExpression(); if (!$stream->isEOF()) { - throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s".', $stream->current->type, $stream->current->value), $stream->current->cursor, $stream->getExpression()); + throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', $stream->current->type, $stream->current->value), $stream->current->cursor, $stream->getExpression()); } unset($this->stream, $this->names); @@ -239,7 +239,7 @@ public function parsePrimaryExpression(): Node\Node default: if ('(' === $this->stream->current->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)); + 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()); @@ -250,7 +250,7 @@ public function parsePrimaryExpression(): Node\Node return new Node\NullCoalescedNameNode($token->value); } - throw new SyntaxError(sprintf('Variable "%s" is not valid.', $token->value), $token->cursor, $this->stream->getExpression(), $token->value, $this->names); + 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 @@ -279,7 +279,7 @@ public function parsePrimaryExpression(): Node\Node } elseif ($token->test(Token::PUNCTUATION_TYPE, '{')) { $node = $this->parseHashExpression(); } else { - throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s".', $token->type, $token->value), $token->cursor, $this->stream->getExpression()); + throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', $token->type, $token->value), $token->cursor, $this->stream->getExpression()); } } @@ -341,7 +341,7 @@ public function parseHashExpression(): Node\ArrayNode } else { $current = $this->stream->current; - throw new SyntaxError(sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', $current->type, $current->value), $current->cursor, $this->stream->getExpression()); + throw new SyntaxError(\sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', $current->type, $current->value), $current->cursor, $this->stream->getExpression()); } $this->stream->expect(Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)'); diff --git a/SyntaxError.php b/SyntaxError.php index e165dc2..8ecb341 100644 --- a/SyntaxError.php +++ b/SyntaxError.php @@ -15,9 +15,9 @@ class SyntaxError extends \LogicException { public function __construct(string $message, int $cursor = 0, string $expression = '', ?string $subject = null, ?array $proposals = null) { - $message = sprintf('%s around position %d', rtrim($message, '.'), $cursor); + $message = \sprintf('%s around position %d', rtrim($message, '.'), $cursor); if ($expression) { - $message = sprintf('%s for expression `%s`', $message, $expression); + $message = \sprintf('%s for expression `%s`', $message, $expression); } $message .= '.'; @@ -32,7 +32,7 @@ public function __construct(string $message, int $cursor = 0, string $expression } if (isset($guess) && $minScore < 3) { - $message .= sprintf(' Did you mean "%s"?', $guess); + $message .= \sprintf(' Did you mean "%s"?', $guess); } } diff --git a/Tests/ExpressionLanguageTest.php b/Tests/ExpressionLanguageTest.php index fbd50c9..907ecc4 100644 --- a/Tests/ExpressionLanguageTest.php +++ b/Tests/ExpressionLanguageTest.php @@ -116,7 +116,7 @@ public function testCompiledEnumFunction() { $result = null; $expressionLanguage = new ExpressionLanguage(); - eval(sprintf('$result = %s;', $expressionLanguage->compile('enum("Symfony\\\\Component\\\\ExpressionLanguage\\\\Tests\\\\Fixtures\\\\FooEnum::Foo")'))); + eval(\sprintf('$result = %s;', $expressionLanguage->compile('enum("Symfony\\\\Component\\\\ExpressionLanguage\\\\Tests\\\\Fixtures\\\\FooEnum::Foo")'))); $this->assertSame(FooEnum::Foo, $result); } @@ -132,7 +132,7 @@ public function testCompiledEnumFunctionWithBackedEnum() { $result = null; $expressionLanguage = new ExpressionLanguage(); - eval(sprintf('$result = %s;', $expressionLanguage->compile('enum("Symfony\\\\Component\\\\ExpressionLanguage\\\\Tests\\\\Fixtures\\\\FooBackedEnum::Bar")'))); + eval(\sprintf('$result = %s;', $expressionLanguage->compile('enum("Symfony\\\\Component\\\\ExpressionLanguage\\\\Tests\\\\Fixtures\\\\FooBackedEnum::Bar")'))); $this->assertSame(FooBackedEnum::Bar, $result); } @@ -166,7 +166,7 @@ public function testShortCircuitOperatorsCompile($expression, array $names, $exp { $result = null; $expressionLanguage = new ExpressionLanguage(); - eval(sprintf('$result = %s;', $expressionLanguage->compile($expression, $names))); + eval(\sprintf('$result = %s;', $expressionLanguage->compile($expression, $names))); $this->assertSame($expected, $result); } @@ -330,7 +330,7 @@ public function testNullSafeEvaluate($expression, $foo) public function testNullSafeCompile($expression, $foo) { $expressionLanguage = new ExpressionLanguage(); - $this->assertNull(eval(sprintf('return %s;', $expressionLanguage->compile($expression, ['foo' => 'foo'])))); + $this->assertNull(eval(\sprintf('return %s;', $expressionLanguage->compile($expression, ['foo' => 'foo'])))); } public static function provideNullSafe() @@ -393,7 +393,7 @@ public function testNullSafeCompileFails($expression, $foo) }); try { - eval(sprintf('return %s;', $expressionLanguage->compile($expression, ['foo' => 'foo']))); + eval(\sprintf('return %s;', $expressionLanguage->compile($expression, ['foo' => 'foo']))); } finally { restore_error_handler(); } @@ -421,7 +421,7 @@ public function testNullCoalescingEvaluate($expression, $foo) public function testNullCoalescingCompile($expression, $foo) { $expressionLanguage = new ExpressionLanguage(); - $this->assertSame(eval(sprintf('return %s;', $expressionLanguage->compile($expression, ['foo' => 'foo']))), 'default'); + $this->assertSame(eval(\sprintf('return %s;', $expressionLanguage->compile($expression, ['foo' => 'foo']))), 'default'); } public static function provideNullCoalescing() diff --git a/Tests/Node/FunctionNodeTest.php b/Tests/Node/FunctionNodeTest.php index aa667f7..36bc4e2 100644 --- a/Tests/Node/FunctionNodeTest.php +++ b/Tests/Node/FunctionNodeTest.php @@ -41,7 +41,7 @@ public static function getDumpData(): array protected static function getCallables(): array { return [ - 'compiler' => fn ($arg) => sprintf('foo(%s)', $arg), + 'compiler' => fn ($arg) => \sprintf('foo(%s)', $arg), 'evaluator' => fn ($variables, $arg) => $arg, ]; } diff --git a/Token.php b/Token.php index c1a7c10..5fc7341 100644 --- a/Token.php +++ b/Token.php @@ -41,7 +41,7 @@ public function __construct( */ public function __toString(): string { - return sprintf('%3d %-11s %s', $this->cursor, strtoupper($this->type), $this->value); + return \sprintf('%3d %-11s %s', $this->cursor, strtoupper($this->type), $this->value); } /** diff --git a/TokenStream.php b/TokenStream.php index 67acc44..1280f44 100644 --- a/TokenStream.php +++ b/TokenStream.php @@ -58,7 +58,7 @@ public function expect(string $type, ?string $value = null, ?string $message = n { $token = $this->current; if (!$token->test($type, $value)) { - throw new SyntaxError(sprintf('%sUnexpected token "%s" of value "%s" ("%s" expected%s).', $message ? $message.'. ' : '', $token->type, $token->value, $type, $value ? sprintf(' with value "%s"', $value) : ''), $token->cursor, $this->expression); + throw new SyntaxError(\sprintf('%sUnexpected token "%s" of value "%s" ("%s" expected%s).', $message ? $message.'. ' : '', $token->type, $token->value, $type, $value ? \sprintf(' with value "%s"', $value) : ''), $token->cursor, $this->expression); } $this->next(); } From 8cf0c25290e702b20a4f08baf6bb9b056607f2e5 Mon Sep 17 00:00:00 2001 From: valtzu Date: Fri, 17 May 2024 19:47:00 +0300 Subject: [PATCH 17/27] [ExpressionLanguage] Add comment support to expression language --- CHANGELOG.md | 1 + Lexer.php | 3 +++ Tests/ExpressionLanguageTest.php | 44 ++++++++++++++++++++++++++++++++ Tests/LexerTest.php | 28 ++++++++++++++++++++ 4 files changed, 76 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4331d72..2609004 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Add support for null-coalescing unknown variables + * Add support for comments using `/*` & `*/` 7.1 --- diff --git a/Lexer.php b/Lexer.php index ab32342..4549dc5 100644 --- a/Lexer.php +++ b/Lexer.php @@ -69,6 +69,9 @@ public function tokenize(string $expression): TokenStream // strings $tokens[] = new Token(Token::STRING_TYPE, stripcslashes(substr($match[0], 1, -1)), $cursor + 1); $cursor += \strlen($match[0]); + } elseif (preg_match('{/\*.*?\*/}A', $expression, $match, 0, $cursor)) { + // comments + $cursor += \strlen($match[0]); } elseif (preg_match('/(?<=^|[\s(])starts with(?=[\s(])|(?<=^|[\s(])ends with(?=[\s(])|(?<=^|[\s(])contains(?=[\s(])|(?<=^|[\s(])matches(?=[\s(])|(?<=^|[\s(])not in(?=[\s(])|(?<=^|[\s(])not(?=[\s(])|(?<=^|[\s(])and(?=[\s(])|\=\=\=|\!\=\=|(?<=^|[\s(])or(?=[\s(])|\|\||&&|\=\=|\!\=|\>\=|\<\=|(?<=^|[\s(])in(?=[\s(])|\.\.|\*\*|\!|\||\^|&|\<|\>|\+|\-|~|\*|\/|%/A', $expression, $match, 0, $cursor)) { // operators $tokens[] = new Token(Token::OPERATOR_TYPE, $match[0], $cursor + 1); diff --git a/Tests/ExpressionLanguageTest.php b/Tests/ExpressionLanguageTest.php index fbd50c9..0ab9620 100644 --- a/Tests/ExpressionLanguageTest.php +++ b/Tests/ExpressionLanguageTest.php @@ -459,6 +459,43 @@ public function testRegisterAfterCompile($registerCallback) $registerCallback($el); } + public static function validCommentProvider() + { + yield ['1 /* comment */ + 1']; + yield ['1 /* /* comment with spaces */']; + yield ['1 /** extra stars **/ + 1']; + yield ["/* multi\nline */ 'foo'"]; + } + + /** + * @dataProvider validCommentProvider + */ + public function testLintAllowsComments($expression) + { + $el = new ExpressionLanguage(); + $el->lint($expression, []); + + $this->expectNotToPerformAssertions(); + } + + public static function invalidCommentProvider() + { + yield ['1 + no start */']; + yield ['1 /* no closing']; + yield ['1 /* double closing */ */']; + } + + /** + * @dataProvider invalidCommentProvider + */ + public function testLintThrowsOnInvalidComments($expression) + { + $el = new ExpressionLanguage(); + + $this->expectException(SyntaxError::class); + $el->lint($expression, []); + } + public function testLintDoesntThrowOnValidExpression() { $el = new ExpressionLanguage(); @@ -477,6 +514,13 @@ public function testLintThrowsOnInvalidExpression() $el->lint('node.', ['node']); } + public function testCommentsIgnored() + { + $expressionLanguage = new ExpressionLanguage(); + $this->assertSame(3, $expressionLanguage->evaluate('1 /* foo */ + 2')); + $this->assertSame('(1 + 2)', $expressionLanguage->compile('1 /* foo */ + 2')); + } + public static function getRegisterCallbacks() { return [ diff --git a/Tests/LexerTest.php b/Tests/LexerTest.php index b1962b5..2ffe988 100644 --- a/Tests/LexerTest.php +++ b/Tests/LexerTest.php @@ -151,6 +151,34 @@ public static function getTokenizeData() ], '-.7_189e+10', ], + [ + [ + new Token('number', 65536, 1), + ], + '65536 /* this is 2^16 */', + ], + [ + [ + new Token('number', 2, 1), + new Token('operator', '*', 21), + new Token('number', 4, 23), + ], + '2 /* /* comment1 */ * 4', + ], + [ + [ + new Token('string', '/* this is', 1), + new Token('operator', '~', 14), + new Token('string', 'not a comment */', 16), + ], + '"/* this is" ~ "not a comment */"', + ], + [ + [ + new Token('string', '/* this is not a comment */', 1), + ], + '"/* this is not a comment */"', + ], ]; } } From c91c95019c2c93000521baafd70e6f456fd2fd2e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 6 Jul 2024 09:57:16 +0200 Subject: [PATCH 18/27] Update .gitattributes --- .gitattributes | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitattributes b/.gitattributes index 84c7add..14c3c35 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,3 @@ /Tests export-ignore /phpunit.xml.dist export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore +/.git* export-ignore From efe6bb8448b60d98df377d782c552d0d5a50b875 Mon Sep 17 00:00:00 2001 From: HypeMC Date: Mon, 8 Jul 2024 21:49:37 +0200 Subject: [PATCH 19/27] [ExpressionLanguage] Allow passing any iterable as `$providers` list --- CHANGELOG.md | 1 + ExpressionLanguage.php | 4 ++-- Tests/ExpressionLanguageTest.php | 15 +++++++++++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2609004..e32b66c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Add support for null-coalescing unknown variables * Add support for comments using `/*` & `*/` + * Allow passing any iterable as `$providers` list to `ExpressionLanguage` constructor 7.1 --- diff --git a/ExpressionLanguage.php b/ExpressionLanguage.php index 055bef2..379d386 100644 --- a/ExpressionLanguage.php +++ b/ExpressionLanguage.php @@ -32,9 +32,9 @@ class ExpressionLanguage protected array $functions = []; /** - * @param ExpressionFunctionProviderInterface[] $providers + * @param iterable $providers */ - public function __construct(?CacheItemPoolInterface $cache = null, array $providers = []) + public function __construct(?CacheItemPoolInterface $cache = null, iterable $providers = []) { $this->cache = $cache ?? new ArrayAdapter(); $this->registerFunctions(); diff --git a/Tests/ExpressionLanguageTest.php b/Tests/ExpressionLanguageTest.php index 955c238..e162822 100644 --- a/Tests/ExpressionLanguageTest.php +++ b/Tests/ExpressionLanguageTest.php @@ -137,9 +137,12 @@ public function testCompiledEnumFunctionWithBackedEnum() $this->assertSame(FooBackedEnum::Bar, $result); } - public function testProviders() + /** + * @dataProvider providerTestCases + */ + public function testProviders(iterable $providers) { - $expressionLanguage = new ExpressionLanguage(null, [new TestProvider()]); + $expressionLanguage = new ExpressionLanguage(null, $providers); $this->assertEquals('foo', $expressionLanguage->evaluate('identity("foo")')); $this->assertEquals('"foo"', $expressionLanguage->compile('identity("foo")')); $this->assertEquals('FOO', $expressionLanguage->evaluate('strtoupper("foo")')); @@ -150,6 +153,14 @@ public function testProviders() $this->assertEquals('\Symfony\Component\ExpressionLanguage\Tests\Fixtures\fn_namespaced()', $expressionLanguage->compile('fn_namespaced()')); } + public static function providerTestCases(): iterable + { + yield 'array' => [[new TestProvider()]]; + yield 'Traversable' => [(function () { + yield new TestProvider(); + })()]; + } + /** * @dataProvider shortCircuitProviderEvaluate */ From bb5f8ca5e70dbcd9391e4f2085e9347e66bc3650 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Tue, 9 Jul 2024 18:50:32 +0200 Subject: [PATCH 20/27] [ExpressionLanguage] Use `assertSame()` instead of `assertEquals()` --- Tests/ExpressionLanguageTest.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Tests/ExpressionLanguageTest.php b/Tests/ExpressionLanguageTest.php index e162822..3469aff 100644 --- a/Tests/ExpressionLanguageTest.php +++ b/Tests/ExpressionLanguageTest.php @@ -77,8 +77,8 @@ public function testCachedParse() public function testBasicPhpFunction($expression, $expected, $compiled) { $expressionLanguage = new ExpressionLanguage(); - $this->assertEquals($expected, $expressionLanguage->evaluate($expression)); - $this->assertEquals($compiled, $expressionLanguage->compile($expression)); + $this->assertSame($expected, $expressionLanguage->evaluate($expression)); + $this->assertSame($compiled, $expressionLanguage->compile($expression)); } public static function basicPhpFunctionProvider() @@ -143,14 +143,14 @@ public function testCompiledEnumFunctionWithBackedEnum() public function testProviders(iterable $providers) { $expressionLanguage = new ExpressionLanguage(null, $providers); - $this->assertEquals('foo', $expressionLanguage->evaluate('identity("foo")')); - $this->assertEquals('"foo"', $expressionLanguage->compile('identity("foo")')); - $this->assertEquals('FOO', $expressionLanguage->evaluate('strtoupper("foo")')); - $this->assertEquals('\strtoupper("foo")', $expressionLanguage->compile('strtoupper("foo")')); - $this->assertEquals('foo', $expressionLanguage->evaluate('strtolower("FOO")')); - $this->assertEquals('\strtolower("FOO")', $expressionLanguage->compile('strtolower("FOO")')); + $this->assertSame('foo', $expressionLanguage->evaluate('identity("foo")')); + $this->assertSame('"foo"', $expressionLanguage->compile('identity("foo")')); + $this->assertSame('FOO', $expressionLanguage->evaluate('strtoupper("foo")')); + $this->assertSame('\strtoupper("foo")', $expressionLanguage->compile('strtoupper("foo")')); + $this->assertSame('foo', $expressionLanguage->evaluate('strtolower("FOO")')); + $this->assertSame('\strtolower("FOO")', $expressionLanguage->compile('strtolower("FOO")')); $this->assertTrue($expressionLanguage->evaluate('fn_namespaced()')); - $this->assertEquals('\Symfony\Component\ExpressionLanguage\Tests\Fixtures\fn_namespaced()', $expressionLanguage->compile('fn_namespaced()')); + $this->assertSame('\Symfony\Component\ExpressionLanguage\Tests\Fixtures\fn_namespaced()', $expressionLanguage->compile('fn_namespaced()')); } public static function providerTestCases(): iterable @@ -167,7 +167,7 @@ public static function providerTestCases(): iterable public function testShortCircuitOperatorsEvaluate($expression, array $values, $expected) { $expressionLanguage = new ExpressionLanguage(); - $this->assertEquals($expected, $expressionLanguage->evaluate($expression, $values)); + $this->assertSame($expected, $expressionLanguage->evaluate($expression, $values)); } /** From baef2bca1f27e059bee6e8a6eccd46c335b72ec5 Mon Sep 17 00:00:00 2001 From: Roy de Vos Burchart Date: Thu, 1 Aug 2024 17:21:17 +0200 Subject: [PATCH 21/27] Code style change in `@PER-CS2.0` affecting `@Symfony` (parentheses for anonymous classes) --- Tests/ExpressionLanguageTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/ExpressionLanguageTest.php b/Tests/ExpressionLanguageTest.php index 3469aff..0250732 100644 --- a/Tests/ExpressionLanguageTest.php +++ b/Tests/ExpressionLanguageTest.php @@ -346,7 +346,7 @@ public function testNullSafeCompile($expression, $foo) public static function provideNullSafe() { - $foo = new class() extends \stdClass { + $foo = new class extends \stdClass { public function bar() { return null; @@ -437,7 +437,7 @@ public function testNullCoalescingCompile($expression, $foo) public static function provideNullCoalescing() { - $foo = new class() extends \stdClass { + $foo = new class extends \stdClass { public function bar() { return null; From a3a48b078c71bde59cc0ddfbc3d5aec70cd452ce Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Wed, 21 Aug 2024 22:35:58 +0200 Subject: [PATCH 22/27] [ExpressionLanguage] Add support for `<<`, `>>`, and `~` bitwise operators --- CHANGELOG.md | 1 + Lexer.php | 2 +- Node/BinaryNode.php | 4 ++++ Node/UnaryNode.php | 2 ++ Parser.php | 3 +++ Tests/LexerTest.php | 5 ++++- Tests/Node/BinaryNodeTest.php | 6 ++++++ Tests/Node/UnaryNodeTest.php | 3 +++ 8 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e32b66c..c1daf1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * Add support for null-coalescing unknown variables * Add support for comments using `/*` & `*/` * Allow passing any iterable as `$providers` list to `ExpressionLanguage` constructor + * Add support for `<<`, `>>`, and `~` bitwise operators 7.1 --- diff --git a/Lexer.php b/Lexer.php index d18d34c..44d0609 100644 --- a/Lexer.php +++ b/Lexer.php @@ -72,7 +72,7 @@ public function tokenize(string $expression): TokenStream } elseif (preg_match('{/\*.*?\*/}A', $expression, $match, 0, $cursor)) { // comments $cursor += \strlen($match[0]); - } elseif (preg_match('/(?<=^|[\s(])starts with(?=[\s(])|(?<=^|[\s(])ends with(?=[\s(])|(?<=^|[\s(])contains(?=[\s(])|(?<=^|[\s(])matches(?=[\s(])|(?<=^|[\s(])not in(?=[\s(])|(?<=^|[\s(])not(?=[\s(])|(?<=^|[\s(])and(?=[\s(])|\=\=\=|\!\=\=|(?<=^|[\s(])or(?=[\s(])|\|\||&&|\=\=|\!\=|\>\=|\<\=|(?<=^|[\s(])in(?=[\s(])|\.\.|\*\*|\!|\||\^|&|\<|\>|\+|\-|~|\*|\/|%/A', $expression, $match, 0, $cursor)) { + } elseif (preg_match('/(?<=^|[\s(])starts with(?=[\s(])|(?<=^|[\s(])ends with(?=[\s(])|(?<=^|[\s(])contains(?=[\s(])|(?<=^|[\s(])matches(?=[\s(])|(?<=^|[\s(])not in(?=[\s(])|(?<=^|[\s(])not(?=[\s(])|(?<=^|[\s(])and(?=[\s(])|\=\=\=|\!\=\=|(?<=^|[\s(])or(?=[\s(])|\|\||&&|\=\=|\!\=|\>\=|\<\=|(?<=^|[\s(])in(?=[\s(])|\.\.|\*\*|\!|\||\^|&|<<|>>|\<|\>|\+|\-|~|\*|\/|%/A', $expression, $match, 0, $cursor)) { // operators $tokens[] = new Token(Token::OPERATOR_TYPE, $match[0], $cursor + 1); $cursor += \strlen($match[0]); diff --git a/Node/BinaryNode.php b/Node/BinaryNode.php index 4065ed6..68bce60 100644 --- a/Node/BinaryNode.php +++ b/Node/BinaryNode.php @@ -130,6 +130,10 @@ public function evaluate(array $functions, array $values): mixed return $left ^ $right; case '&': return $left & $right; + case '<<': + return $left << $right; + case '>>': + return $left >> $right; case '==': return $left == $right; case '===': diff --git a/Node/UnaryNode.php b/Node/UnaryNode.php index 55e2121..5a78cfa 100644 --- a/Node/UnaryNode.php +++ b/Node/UnaryNode.php @@ -25,6 +25,7 @@ class UnaryNode extends Node 'not' => '!', '+' => '+', '-' => '-', + '~' => '~', ]; public function __construct(string $operator, Node $node) @@ -53,6 +54,7 @@ public function evaluate(array $functions, array $values): mixed 'not', '!' => !$value, '-' => -$value, + '~' => ~$value, default => $value, }; } diff --git a/Parser.php b/Parser.php index da36770..7305d7a 100644 --- a/Parser.php +++ b/Parser.php @@ -43,6 +43,7 @@ public function __construct( '!' => ['precedence' => 50], '-' => ['precedence' => 500], '+' => ['precedence' => 500], + '~' => ['precedence' => 500], ]; $this->binaryOperators = [ 'or' => ['precedence' => 10, 'associativity' => self::OPERATOR_LEFT], @@ -67,6 +68,8 @@ public function __construct( 'ends with' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT], 'matches' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT], '..' => ['precedence' => 25, 'associativity' => self::OPERATOR_LEFT], + '<<' => ['precedence' => 25, 'associativity' => self::OPERATOR_LEFT], + '>>' => ['precedence' => 25, 'associativity' => self::OPERATOR_LEFT], '+' => ['precedence' => 30, 'associativity' => self::OPERATOR_LEFT], '-' => ['precedence' => 30, 'associativity' => self::OPERATOR_LEFT], '~' => ['precedence' => 40, 'associativity' => self::OPERATOR_LEFT], diff --git a/Tests/LexerTest.php b/Tests/LexerTest.php index 2ffe988..1ba55b7 100644 --- a/Tests/LexerTest.php +++ b/Tests/LexerTest.php @@ -97,8 +97,11 @@ public static function getTokenizeData() new Token('punctuation', ']', 27), new Token('operator', '-', 29), new Token('number', 1990, 31), + new Token('operator', '+', 39), + new Token('operator', '~', 41), + new Token('name', 'qux', 42), ], - '(3 + 5) ~ foo("bar").baz[4] - 1.99E+3', + '(3 + 5) ~ foo("bar").baz[4] - 1.99E+3 + ~qux', ], [ [new Token('operator', '..', 1)], diff --git a/Tests/Node/BinaryNodeTest.php b/Tests/Node/BinaryNodeTest.php index fd06587..36e3b9b 100644 --- a/Tests/Node/BinaryNodeTest.php +++ b/Tests/Node/BinaryNodeTest.php @@ -35,6 +35,8 @@ public static function getEvaluateData(): array [0, new BinaryNode('&', new ConstantNode(2), new ConstantNode(4))], [6, new BinaryNode('|', new ConstantNode(2), new ConstantNode(4))], [6, new BinaryNode('^', new ConstantNode(2), new ConstantNode(4))], + [32, new BinaryNode('<<', new ConstantNode(2), new ConstantNode(4))], + [2, new BinaryNode('>>', new ConstantNode(32), new ConstantNode(4))], [true, new BinaryNode('<', new ConstantNode(1), new ConstantNode(2))], [true, new BinaryNode('<=', new ConstantNode(1), new ConstantNode(2))], @@ -90,6 +92,8 @@ public static function getCompileData(): array ['(2 & 4)', new BinaryNode('&', new ConstantNode(2), new ConstantNode(4))], ['(2 | 4)', new BinaryNode('|', new ConstantNode(2), new ConstantNode(4))], ['(2 ^ 4)', new BinaryNode('^', new ConstantNode(2), new ConstantNode(4))], + ['(2 << 4)', new BinaryNode('<<', new ConstantNode(2), new ConstantNode(4))], + ['(32 >> 4)', new BinaryNode('>>', new ConstantNode(32), new ConstantNode(4))], ['(1 < 2)', new BinaryNode('<', new ConstantNode(1), new ConstantNode(2))], ['(1 <= 2)', new BinaryNode('<=', new ConstantNode(1), new ConstantNode(2))], @@ -142,6 +146,8 @@ public static function getDumpData(): array ['(2 & 4)', new BinaryNode('&', new ConstantNode(2), new ConstantNode(4))], ['(2 | 4)', new BinaryNode('|', new ConstantNode(2), new ConstantNode(4))], ['(2 ^ 4)', new BinaryNode('^', new ConstantNode(2), new ConstantNode(4))], + ['(2 << 4)', new BinaryNode('<<', new ConstantNode(2), new ConstantNode(4))], + ['(32 >> 4)', new BinaryNode('>>', new ConstantNode(32), new ConstantNode(4))], ['(1 < 2)', new BinaryNode('<', new ConstantNode(1), new ConstantNode(2))], ['(1 <= 2)', new BinaryNode('<=', new ConstantNode(1), new ConstantNode(2))], diff --git a/Tests/Node/UnaryNodeTest.php b/Tests/Node/UnaryNodeTest.php index 7da4be7..ec7fb7f 100644 --- a/Tests/Node/UnaryNodeTest.php +++ b/Tests/Node/UnaryNodeTest.php @@ -23,6 +23,7 @@ public static function getEvaluateData(): array [3, new UnaryNode('+', new ConstantNode(3))], [false, new UnaryNode('!', new ConstantNode(true))], [false, new UnaryNode('not', new ConstantNode(true))], + [-6, new UnaryNode('~', new ConstantNode(5))], ]; } @@ -33,6 +34,7 @@ public static function getCompileData(): array ['(+3)', new UnaryNode('+', new ConstantNode(3))], ['(!true)', new UnaryNode('!', new ConstantNode(true))], ['(!true)', new UnaryNode('not', new ConstantNode(true))], + ['(~5)', new UnaryNode('~', new ConstantNode(5))], ]; } @@ -43,6 +45,7 @@ public static function getDumpData(): array ['(+ 3)', new UnaryNode('+', new ConstantNode(3))], ['(! true)', new UnaryNode('!', new ConstantNode(true))], ['(not true)', new UnaryNode('not', new ConstantNode(true))], + ['(~ 5)', new UnaryNode('~', new ConstantNode(5))], ]; } } From 900725f9bf4ebebcc5c273c905a39fd0c0c7bcec Mon Sep 17 00:00:00 2001 From: HypeMC Date: Fri, 20 Sep 2024 17:25:36 +0200 Subject: [PATCH 23/27] [ExpressionLanguage] Use script to generate regex --- Lexer.php | 2 +- Resources/bin/generate_operator_regex.php | 2 +- Tests/LexerTest.php | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Lexer.php b/Lexer.php index 44d0609..38d06ec 100644 --- a/Lexer.php +++ b/Lexer.php @@ -72,7 +72,7 @@ public function tokenize(string $expression): TokenStream } elseif (preg_match('{/\*.*?\*/}A', $expression, $match, 0, $cursor)) { // comments $cursor += \strlen($match[0]); - } elseif (preg_match('/(?<=^|[\s(])starts with(?=[\s(])|(?<=^|[\s(])ends with(?=[\s(])|(?<=^|[\s(])contains(?=[\s(])|(?<=^|[\s(])matches(?=[\s(])|(?<=^|[\s(])not in(?=[\s(])|(?<=^|[\s(])not(?=[\s(])|(?<=^|[\s(])and(?=[\s(])|\=\=\=|\!\=\=|(?<=^|[\s(])or(?=[\s(])|\|\||&&|\=\=|\!\=|\>\=|\<\=|(?<=^|[\s(])in(?=[\s(])|\.\.|\*\*|\!|\||\^|&|<<|>>|\<|\>|\+|\-|~|\*|\/|%/A', $expression, $match, 0, $cursor)) { + } elseif (preg_match('/(?<=^|[\s(])starts with(?=[\s(])|(?<=^|[\s(])ends with(?=[\s(])|(?<=^|[\s(])contains(?=[\s(])|(?<=^|[\s(])matches(?=[\s(])|(?<=^|[\s(])not in(?=[\s(])|(?<=^|[\s(])not(?=[\s(])|(?<=^|[\s(])and(?=[\s(])|\=\=\=|\!\=\=|(?<=^|[\s(])or(?=[\s(])|\|\||&&|\=\=|\!\=|\>\=|\<\=|(?<=^|[\s(])in(?=[\s(])|\.\.|\*\*|\<\<|\>\>|\!|\||\^|&|\<|\>|\+|\-|~|\*|\/|%/A', $expression, $match, 0, $cursor)) { // operators $tokens[] = new Token(Token::OPERATOR_TYPE, $match[0], $cursor + 1); $cursor += \strlen($match[0]); diff --git a/Resources/bin/generate_operator_regex.php b/Resources/bin/generate_operator_regex.php index 179810e..8908552 100644 --- a/Resources/bin/generate_operator_regex.php +++ b/Resources/bin/generate_operator_regex.php @@ -13,7 +13,7 @@ throw new Exception('This script must be run from the command line.'); } -$operators = ['not', '!', 'or', '||', '&&', 'and', '|', '^', '&', '==', '===', '!=', '!==', '<', '>', '>=', '<=', 'not in', 'in', '..', '+', '-', '~', '*', '/', '%', 'contains', 'starts with', 'ends with', 'matches', '**']; +$operators = ['not', '!', 'or', '||', '&&', 'and', '|', '^', '&', '==', '===', '!=', '!==', '<', '>', '>=', '<=', 'not in', 'in', '..', '+', '-', '~', '*', '/', '%', 'contains', 'starts with', 'ends with', 'matches', '**', '<<', '>>']; $operators = array_combine($operators, array_map('strlen', $operators)); arsort($operators); diff --git a/Tests/LexerTest.php b/Tests/LexerTest.php index 1ba55b7..7c501fc 100644 --- a/Tests/LexerTest.php +++ b/Tests/LexerTest.php @@ -184,4 +184,20 @@ public static function getTokenizeData() ], ]; } + + public function testOperatorRegexWasGeneratedWithScript() + { + ob_start(); + try { + require $script = \dirname(__DIR__).'/Resources/bin/generate_operator_regex.php'; + } finally { + $output = ob_get_clean(); + } + + self::assertStringContainsString( + $output, + file_get_contents((new \ReflectionClass(Lexer::class))->getFileName()), + \sprintf('You need to run "%s" to generate the operator regex.', $script), + ); + } } From a30e5fdbe530ab5c5f17a7e7d8544cfb67a0fd6f Mon Sep 17 00:00:00 2001 From: HypeMC Date: Fri, 20 Sep 2024 22:04:46 +0200 Subject: [PATCH 24/27] [ExpressionLanguage] Add support for logical `xor` operator --- CHANGELOG.md | 1 + Lexer.php | 2 +- Node/BinaryNode.php | 2 ++ Parser.php | 1 + Resources/bin/generate_operator_regex.php | 2 +- Tests/LexerTest.php | 8 ++++++++ Tests/Node/BinaryNodeTest.php | 3 +++ Tests/ParserTest.php | 9 +++++++++ 8 files changed, 26 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1daf1d..a85455b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * Add support for comments using `/*` & `*/` * Allow passing any iterable as `$providers` list to `ExpressionLanguage` constructor * Add support for `<<`, `>>`, and `~` bitwise operators + * Add support for logical `xor` operator 7.1 --- diff --git a/Lexer.php b/Lexer.php index 38d06ec..f7a3c16 100644 --- a/Lexer.php +++ b/Lexer.php @@ -72,7 +72,7 @@ public function tokenize(string $expression): TokenStream } elseif (preg_match('{/\*.*?\*/}A', $expression, $match, 0, $cursor)) { // comments $cursor += \strlen($match[0]); - } elseif (preg_match('/(?<=^|[\s(])starts with(?=[\s(])|(?<=^|[\s(])ends with(?=[\s(])|(?<=^|[\s(])contains(?=[\s(])|(?<=^|[\s(])matches(?=[\s(])|(?<=^|[\s(])not in(?=[\s(])|(?<=^|[\s(])not(?=[\s(])|(?<=^|[\s(])and(?=[\s(])|\=\=\=|\!\=\=|(?<=^|[\s(])or(?=[\s(])|\|\||&&|\=\=|\!\=|\>\=|\<\=|(?<=^|[\s(])in(?=[\s(])|\.\.|\*\*|\<\<|\>\>|\!|\||\^|&|\<|\>|\+|\-|~|\*|\/|%/A', $expression, $match, 0, $cursor)) { + } elseif (preg_match('/(?<=^|[\s(])starts with(?=[\s(])|(?<=^|[\s(])ends with(?=[\s(])|(?<=^|[\s(])contains(?=[\s(])|(?<=^|[\s(])matches(?=[\s(])|(?<=^|[\s(])not in(?=[\s(])|(?<=^|[\s(])not(?=[\s(])|(?<=^|[\s(])xor(?=[\s(])|(?<=^|[\s(])and(?=[\s(])|\=\=\=|\!\=\=|(?<=^|[\s(])or(?=[\s(])|\|\||&&|\=\=|\!\=|\>\=|\<\=|(?<=^|[\s(])in(?=[\s(])|\.\.|\*\*|\<\<|\>\>|\!|\||\^|&|\<|\>|\+|\-|~|\*|\/|%/A', $expression, $match, 0, $cursor)) { // operators $tokens[] = new Token(Token::OPERATOR_TYPE, $match[0], $cursor + 1); $cursor += \strlen($match[0]); diff --git a/Node/BinaryNode.php b/Node/BinaryNode.php index 68bce60..2a3d52a 100644 --- a/Node/BinaryNode.php +++ b/Node/BinaryNode.php @@ -116,6 +116,8 @@ public function evaluate(array $functions, array $values): mixed case 'or': case '||': return $left || $this->nodes['right']->evaluate($functions, $values); + case 'xor': + return $left xor $this->nodes['right']->evaluate($functions, $values); case 'and': case '&&': return $left && $this->nodes['right']->evaluate($functions, $values); diff --git a/Parser.php b/Parser.php index 7305d7a..32254cd 100644 --- a/Parser.php +++ b/Parser.php @@ -48,6 +48,7 @@ public function __construct( $this->binaryOperators = [ 'or' => ['precedence' => 10, 'associativity' => self::OPERATOR_LEFT], '||' => ['precedence' => 10, 'associativity' => self::OPERATOR_LEFT], + 'xor' => ['precedence' => 12, 'associativity' => self::OPERATOR_LEFT], 'and' => ['precedence' => 15, 'associativity' => self::OPERATOR_LEFT], '&&' => ['precedence' => 15, 'associativity' => self::OPERATOR_LEFT], '|' => ['precedence' => 16, 'associativity' => self::OPERATOR_LEFT], diff --git a/Resources/bin/generate_operator_regex.php b/Resources/bin/generate_operator_regex.php index 8908552..3803549 100644 --- a/Resources/bin/generate_operator_regex.php +++ b/Resources/bin/generate_operator_regex.php @@ -13,7 +13,7 @@ throw new Exception('This script must be run from the command line.'); } -$operators = ['not', '!', 'or', '||', '&&', 'and', '|', '^', '&', '==', '===', '!=', '!==', '<', '>', '>=', '<=', 'not in', 'in', '..', '+', '-', '~', '*', '/', '%', 'contains', 'starts with', 'ends with', 'matches', '**', '<<', '>>']; +$operators = ['not', '!', 'or', '||', 'xor', '&&', 'and', '|', '^', '&', '==', '===', '!=', '!==', '<', '>', '>=', '<=', 'not in', 'in', '..', '+', '-', '~', '*', '/', '%', 'contains', 'starts with', 'ends with', 'matches', '**', '<<', '>>']; $operators = array_combine($operators, array_map('strlen', $operators)); arsort($operators); diff --git a/Tests/LexerTest.php b/Tests/LexerTest.php index 7c501fc..9e6f8b4 100644 --- a/Tests/LexerTest.php +++ b/Tests/LexerTest.php @@ -182,6 +182,14 @@ public static function getTokenizeData() ], '"/* this is not a comment */"', ], + [ + [ + new Token('name', 'foo', 1), + new Token('operator', 'xor', 5), + new Token('name', 'bar', 9), + ], + 'foo xor bar', + ], ]; } diff --git a/Tests/Node/BinaryNodeTest.php b/Tests/Node/BinaryNodeTest.php index 36e3b9b..e75a3d4 100644 --- a/Tests/Node/BinaryNodeTest.php +++ b/Tests/Node/BinaryNodeTest.php @@ -29,6 +29,7 @@ public static function getEvaluateData(): array return [ [true, new BinaryNode('or', new ConstantNode(true), new ConstantNode(false))], [true, new BinaryNode('||', new ConstantNode(true), new ConstantNode(false))], + [false, new BinaryNode('xor', new ConstantNode(true), new ConstantNode(true))], [false, new BinaryNode('and', new ConstantNode(true), new ConstantNode(false))], [false, new BinaryNode('&&', new ConstantNode(true), new ConstantNode(false))], @@ -86,6 +87,7 @@ public static function getCompileData(): array return [ ['(true || false)', new BinaryNode('or', new ConstantNode(true), new ConstantNode(false))], ['(true || false)', new BinaryNode('||', new ConstantNode(true), new ConstantNode(false))], + ['(true xor true)', new BinaryNode('xor', new ConstantNode(true), new ConstantNode(true))], ['(true && false)', new BinaryNode('and', new ConstantNode(true), new ConstantNode(false))], ['(true && false)', new BinaryNode('&&', new ConstantNode(true), new ConstantNode(false))], @@ -140,6 +142,7 @@ public static function getDumpData(): array return [ ['(true or false)', new BinaryNode('or', new ConstantNode(true), new ConstantNode(false))], ['(true || false)', new BinaryNode('||', new ConstantNode(true), new ConstantNode(false))], + ['(true xor true)', new BinaryNode('xor', new ConstantNode(true), new ConstantNode(true))], ['(true and false)', new BinaryNode('and', new ConstantNode(true), new ConstantNode(false))], ['(true && false)', new BinaryNode('&&', new ConstantNode(true), new ConstantNode(false))], diff --git a/Tests/ParserTest.php b/Tests/ParserTest.php index 31a8658..0f1c893 100644 --- a/Tests/ParserTest.php +++ b/Tests/ParserTest.php @@ -244,6 +244,15 @@ public static function getParseData() 'not foo or foo.not', ['foo'], ], + [ + new Node\BinaryNode( + 'xor', + new Node\NameNode('foo'), + new Node\NameNode('bar'), + ), + 'foo xor bar', + ['foo', 'bar'], + ], [ new Node\BinaryNode('..', new Node\ConstantNode(0), new Node\ConstantNode(3)), '0..3', From 529f749af4d919175887325acfc47063b8fdfb9f Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Fri, 4 Oct 2024 16:49:39 +0200 Subject: [PATCH 25/27] [ExpressionLanguage] Improve tests on `BinaryNode` --- Node/BinaryNode.php | 6 ++---- Tests/Node/BinaryNodeTest.php | 10 ++++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Node/BinaryNode.php b/Node/BinaryNode.php index 2a3d52a..a4212c1 100644 --- a/Node/BinaryNode.php +++ b/Node/BinaryNode.php @@ -152,10 +152,6 @@ public function evaluate(array $functions, array $values): mixed return $left >= $right; case '<=': return $left <= $right; - case 'not in': - return !\in_array($left, $right, true); - case 'in': - return \in_array($left, $right, true); case '+': return $left + $right; case '-': @@ -179,6 +175,8 @@ public function evaluate(array $functions, array $values): mixed case 'matches': return $this->evaluateMatches($right, $left); } + + throw new \LogicException(\sprintf('"%s" does not support the "%s" operator.', __CLASS__, $operator)); } public function toArray(): array diff --git a/Tests/Node/BinaryNodeTest.php b/Tests/Node/BinaryNodeTest.php index e75a3d4..239d952 100644 --- a/Tests/Node/BinaryNodeTest.php +++ b/Tests/Node/BinaryNodeTest.php @@ -258,4 +258,14 @@ public function testInOperatorStrictness(mixed $value) $this->assertFalse($node->evaluate([], [])); } + + public function testEvaluateUnsupportedOperator() + { + $node = new BinaryNode('unsupported', new ConstantNode(1), new ConstantNode(2)); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('"Symfony\Component\ExpressionLanguage\Node\BinaryNode" does not support the "unsupported" operator.'); + + $node->evaluate([], []); + } } From 0e7ebebda24146ca6f80b0b5c8ed6ad5ce14ee0c Mon Sep 17 00:00:00 2001 From: Ivan Tse Date: Fri, 13 Sep 2024 14:02:49 -0400 Subject: [PATCH 26/27] [ExpressionLanguage] Fix matches to handle booleans being used as regexp --- Node/BinaryNode.php | 2 ++ Tests/Node/BinaryNodeTest.php | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/Node/BinaryNode.php b/Node/BinaryNode.php index 68bce60..8890d96 100644 --- a/Node/BinaryNode.php +++ b/Node/BinaryNode.php @@ -52,6 +52,8 @@ public function compile(Compiler $compiler): void if ('matches' == $operator) { if ($this->nodes['right'] instanceof ConstantNode) { $this->evaluateMatches($this->nodes['right']->evaluate([], []), ''); + } elseif ($this->nodes['right'] instanceof self && '~' !== $this->nodes['right']->attributes['operator']) { + throw new SyntaxError('The regex passed to "matches" must be a string.'); } $compiler diff --git a/Tests/Node/BinaryNodeTest.php b/Tests/Node/BinaryNodeTest.php index 36e3b9b..454a181 100644 --- a/Tests/Node/BinaryNodeTest.php +++ b/Tests/Node/BinaryNodeTest.php @@ -221,6 +221,27 @@ public function testCompileMatchesWithInvalidRegexpAsExpression() eval('$regexp = "this is not a regexp"; '.$compiler->getSource().';'); } + public function testCompileMatchesWithBooleanBinaryNode() + { + $binaryNode = new BinaryNode('||', new ConstantNode(true), new ConstantNode(false)); + $node = new BinaryNode('matches', new ConstantNode('abc'), $binaryNode); + + $this->expectException(SyntaxError::class); + $this->expectExceptionMessage('The regex passed to "matches" must be a string'); + $compiler = new Compiler([]); + $node->compile($compiler); + } + + public function testCompileMatchesWithStringBinaryNode() + { + $binaryNode = new BinaryNode('~', new ConstantNode('a'), new ConstantNode('b')); + $node = new BinaryNode('matches', new ConstantNode('abc'), $binaryNode); + + $compiler = new Compiler([]); + $node->compile($compiler); + $this->expectNotToPerformAssertions(); + } + public function testDivisionByZero() { $node = new BinaryNode('/', new ConstantNode(1), new ConstantNode(0)); From 26f4884a455e755e630a5fc372df124a3578da2e Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Tue, 15 Oct 2024 13:39:51 +0200 Subject: [PATCH 27/27] [ExpressionLanguage] Cover multiline comments --- Tests/LexerTest.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Tests/LexerTest.php b/Tests/LexerTest.php index 96c41f7..2827cf6 100644 --- a/Tests/LexerTest.php +++ b/Tests/LexerTest.php @@ -35,6 +35,32 @@ public function testTokenize($tokens, $expression) $this->assertEquals(new TokenStream($tokens, $expression), $this->lexer->tokenize($expression)); } + public function testTokenizeMultilineComment() + { + $expression = <<assertEquals(new TokenStream($tokens, str_replace("\n", ' ', $expression)), $this->lexer->tokenize($expression)); + } + public function testTokenizeThrowsErrorWithMessage() { $this->expectException(SyntaxError::class); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy