From 6e0a613616898f7af033978a5217828bb95c6ec5 Mon Sep 17 00:00:00 2001 From: HypeMC Date: Fri, 20 Sep 2024 22:04:46 +0200 Subject: [PATCH] [ExpressionLanguage] Add support for logical `xor` operator --- src/Symfony/Component/ExpressionLanguage/CHANGELOG.md | 1 + src/Symfony/Component/ExpressionLanguage/Lexer.php | 2 +- .../Component/ExpressionLanguage/Node/BinaryNode.php | 2 ++ src/Symfony/Component/ExpressionLanguage/Parser.php | 1 + .../Resources/bin/generate_operator_regex.php | 2 +- .../Component/ExpressionLanguage/Tests/LexerTest.php | 8 ++++++++ .../ExpressionLanguage/Tests/Node/BinaryNodeTest.php | 3 +++ .../Component/ExpressionLanguage/Tests/ParserTest.php | 9 +++++++++ 8 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/ExpressionLanguage/CHANGELOG.md b/src/Symfony/Component/ExpressionLanguage/CHANGELOG.md index c1daf1de9b89e..a85455b92b6a2 100644 --- a/src/Symfony/Component/ExpressionLanguage/CHANGELOG.md +++ b/src/Symfony/Component/ExpressionLanguage/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/src/Symfony/Component/ExpressionLanguage/Lexer.php b/src/Symfony/Component/ExpressionLanguage/Lexer.php index 38d06ec7b4dbe..f7a3c16996191 100644 --- a/src/Symfony/Component/ExpressionLanguage/Lexer.php +++ b/src/Symfony/Component/ExpressionLanguage/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/src/Symfony/Component/ExpressionLanguage/Node/BinaryNode.php b/src/Symfony/Component/ExpressionLanguage/Node/BinaryNode.php index 68bce60c62862..2a3d52a448d10 100644 --- a/src/Symfony/Component/ExpressionLanguage/Node/BinaryNode.php +++ b/src/Symfony/Component/ExpressionLanguage/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/src/Symfony/Component/ExpressionLanguage/Parser.php b/src/Symfony/Component/ExpressionLanguage/Parser.php index 7305d7a0d4ade..32254cd5e355f 100644 --- a/src/Symfony/Component/ExpressionLanguage/Parser.php +++ b/src/Symfony/Component/ExpressionLanguage/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/src/Symfony/Component/ExpressionLanguage/Resources/bin/generate_operator_regex.php b/src/Symfony/Component/ExpressionLanguage/Resources/bin/generate_operator_regex.php index 890855228ac7a..3803549877c75 100644 --- a/src/Symfony/Component/ExpressionLanguage/Resources/bin/generate_operator_regex.php +++ b/src/Symfony/Component/ExpressionLanguage/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/src/Symfony/Component/ExpressionLanguage/Tests/LexerTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/LexerTest.php index 7c501fcce4164..9e6f8b462982a 100644 --- a/src/Symfony/Component/ExpressionLanguage/Tests/LexerTest.php +++ b/src/Symfony/Component/ExpressionLanguage/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/src/Symfony/Component/ExpressionLanguage/Tests/Node/BinaryNodeTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/Node/BinaryNodeTest.php index 36e3b9b4dee81..e75a3d4fbb3b7 100644 --- a/src/Symfony/Component/ExpressionLanguage/Tests/Node/BinaryNodeTest.php +++ b/src/Symfony/Component/ExpressionLanguage/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/src/Symfony/Component/ExpressionLanguage/Tests/ParserTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/ParserTest.php index 31a8658449573..0f1c893ca038e 100644 --- a/src/Symfony/Component/ExpressionLanguage/Tests/ParserTest.php +++ b/src/Symfony/Component/ExpressionLanguage/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',