From 15b4b970d017b2febea8207047a814c83fc72ea4 Mon Sep 17 00:00:00 2001 From: Adam Kiss Date: Sun, 28 Apr 2024 19:51:43 +0200 Subject: [PATCH 01/16] [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 02/16] 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 03/16] 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 04/16] [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 05/16] 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 06/16] [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 07/16] [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 08/16] 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 09/16] [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 10/16] [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 11/16] [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 12/16] [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 13/16] [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 14/16] [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); From cbf40de4dd360a5358368ae5618dfce4045cd02c Mon Sep 17 00:00:00 2001 From: Ivan Tse Date: Wed, 9 Jul 2025 09:28:51 +0000 Subject: [PATCH 15/16] [ExpressionLanguage] Fix dumping of null safe operator --- Node/GetAttrNode.php | 5 +++-- Tests/ExpressionLanguageTest.php | 6 +++--- Tests/Node/FunctionNodeTest.php | 2 +- Tests/Node/GetAttrNodeTest.php | 7 +++++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Node/GetAttrNode.php b/Node/GetAttrNode.php index 984247e..57f4aa2 100644 --- a/Node/GetAttrNode.php +++ b/Node/GetAttrNode.php @@ -141,12 +141,13 @@ private function isShortCircuited(): bool public function toArray(): array { + $nullSafe = $this->nodes['attribute'] instanceof ConstantNode && $this->nodes['attribute']->isNullSafe; switch ($this->attributes['type']) { case self::PROPERTY_CALL: - return [$this->nodes['node'], '.', $this->nodes['attribute']]; + return [$this->nodes['node'], $nullSafe ? '?.' : '.', $this->nodes['attribute']]; case self::METHOD_CALL: - return [$this->nodes['node'], '.', $this->nodes['attribute'], '(', $this->nodes['arguments'], ')']; + return [$this->nodes['node'], $nullSafe ? '?.' : '.', $this->nodes['attribute'], '(', $this->nodes['arguments'], ')']; case self::ARRAY_CALL: return [$this->nodes['node'], '[', $this->nodes['attribute'], ']']; diff --git a/Tests/ExpressionLanguageTest.php b/Tests/ExpressionLanguageTest.php index af53599..0289afb 100644 --- a/Tests/ExpressionLanguageTest.php +++ b/Tests/ExpressionLanguageTest.php @@ -383,9 +383,9 @@ public function testNullSafeCompileFails($expression, $foo) public static function provideInvalidNullSafe() { - yield ['foo?.bar.baz', (object) ['bar' => null], 'Unable to get property "baz" of non-object "foo.bar".']; - yield ['foo?.bar["baz"]', (object) ['bar' => null], 'Unable to get an item of non-array "foo.bar".']; - yield ['foo?.bar["baz"].qux.quux', (object) ['bar' => ['baz' => null]], 'Unable to get property "qux" of non-object "foo.bar["baz"]".']; + yield ['foo?.bar.baz', (object) ['bar' => null], 'Unable to get property "baz" of non-object "foo?.bar".']; + yield ['foo?.bar["baz"]', (object) ['bar' => null], 'Unable to get an item of non-array "foo?.bar".']; + yield ['foo?.bar["baz"].qux.quux', (object) ['bar' => ['baz' => null]], 'Unable to get property "qux" of non-object "foo?.bar["baz"]".']; } /** diff --git a/Tests/Node/FunctionNodeTest.php b/Tests/Node/FunctionNodeTest.php index aa667f7..2fa6abe 100644 --- a/Tests/Node/FunctionNodeTest.php +++ b/Tests/Node/FunctionNodeTest.php @@ -34,7 +34,7 @@ public static function getCompileData(): array public static function getDumpData(): array { return [ - ['foo("bar")', new FunctionNode('foo', new Node([new ConstantNode('bar')])), ['foo' => static::getCallables()]], + ['foo("bar")', new FunctionNode('foo', new Node([new ConstantNode('bar')]))], ]; } diff --git a/Tests/Node/GetAttrNodeTest.php b/Tests/Node/GetAttrNodeTest.php index 6d81a2b..dcc9811 100644 --- a/Tests/Node/GetAttrNodeTest.php +++ b/Tests/Node/GetAttrNodeTest.php @@ -15,6 +15,7 @@ use Symfony\Component\ExpressionLanguage\Node\ConstantNode; use Symfony\Component\ExpressionLanguage\Node\GetAttrNode; use Symfony\Component\ExpressionLanguage\Node\NameNode; +use Symfony\Component\ExpressionLanguage\Node\ArgumentsNode; class GetAttrNodeTest extends AbstractNodeTestCase { @@ -50,10 +51,12 @@ public static function getDumpData(): array ['foo[0]', new GetAttrNode(new NameNode('foo'), new ConstantNode(0), self::getArrayNode(), GetAttrNode::ARRAY_CALL)], ['foo["b"]', new GetAttrNode(new NameNode('foo'), new ConstantNode('b'), self::getArrayNode(), GetAttrNode::ARRAY_CALL)], - ['foo.foo', new GetAttrNode(new NameNode('foo'), new NameNode('foo'), self::getArrayNode(), GetAttrNode::PROPERTY_CALL), ['foo' => new Obj()]], + ['foo.foo', new GetAttrNode(new NameNode('foo'), new NameNode('foo'), self::getArrayNode(), GetAttrNode::PROPERTY_CALL)], - ['foo.foo({"b": "a", 0: "b"})', new GetAttrNode(new NameNode('foo'), new NameNode('foo'), self::getArrayNode(), GetAttrNode::METHOD_CALL), ['foo' => new Obj()]], + ['foo.foo({"b": "a", 0: "b"})', new GetAttrNode(new NameNode('foo'), new NameNode('foo'), self::getArrayNode(), GetAttrNode::METHOD_CALL)], ['foo[index]', new GetAttrNode(new NameNode('foo'), new NameNode('index'), self::getArrayNode(), GetAttrNode::ARRAY_CALL)], + + ['foo?.foo()', new GetAttrNode(new NameNode('foo'), new ConstantNode('foo', true, true), new ArgumentsNode(), GetAttrNode::METHOD_CALL)], ]; } From 1ea0adaa53539ea7e70821ae9de49ebe03ae7091 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 10 Jul 2025 09:12:18 +0200 Subject: [PATCH 16/16] CS fixes --- 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 | 16 ++++++++-------- Tests/Node/FunctionNodeTest.php | 2 +- Tests/Node/GetAttrNodeTest.php | 2 +- Token.php | 2 +- TokenStream.php | 2 +- 14 files changed, 39 insertions(+), 39 deletions(-) diff --git a/Compiler.php b/Compiler.php index ab50d36..419eef8 100644 --- a/Compiler.php +++ b/Compiler.php @@ -101,7 +101,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 0b3d6c4..2ed664c 100644 --- a/ExpressionFunction.php +++ b/ExpressionFunction.php @@ -74,15 +74,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 a7f249f..6350cbf 100644 --- a/ExpressionLanguage.php +++ b/ExpressionLanguage.php @@ -151,12 +151,12 @@ protected function registerFunctions() $this->addFunction(ExpressionFunction::fromPhp('constant')); $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 0cacfc6..455523e 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']) @@ -194,7 +194,7 @@ public static function inArray($value, array $array): bool 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 57f4aa2..416c094 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 91fcc36..33cdf24 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)]; @@ -87,7 +87,7 @@ public function evaluate(array $functions, array $values) */ public function toArray() { - 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)); } /** @@ -109,7 +109,7 @@ public function dump() */ protected function dumpString(string $value) { - return sprintf('"%s"', addcslashes($value, "\0\t\"\\")); + return \sprintf('"%s"', addcslashes($value, "\0\t\"\\")); } /** diff --git a/Parser.php b/Parser.php index a163a7a..8b5f4ad 100644 --- a/Parser.php +++ b/Parser.php @@ -122,7 +122,7 @@ private function doParse(TokenStream $stream, ?array $names = []): Node\Node $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,14 +239,14 @@ public function parsePrimaryExpression() default: if ('(' === $this->stream->current->value) { if (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()); } else { if (!$this->lint || \is_array($this->names)) { 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); + 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 @@ -275,7 +275,7 @@ public function parsePrimaryExpression() } 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()); } } @@ -343,7 +343,7 @@ public function parseHashExpression() } 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 0289afb..df05548 100644 --- a/Tests/ExpressionLanguageTest.php +++ b/Tests/ExpressionLanguageTest.php @@ -106,7 +106,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); } @@ -122,7 +122,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); } @@ -156,7 +156,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); } @@ -312,12 +312,12 @@ 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() { - $foo = new class() extends \stdClass { + $foo = new class extends \stdClass { public function bar() { return null; @@ -375,7 +375,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(); } @@ -403,12 +403,12 @@ 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() { - $foo = new class() extends \stdClass { + $foo = new class extends \stdClass { public function bar() { return null; diff --git a/Tests/Node/FunctionNodeTest.php b/Tests/Node/FunctionNodeTest.php index 2fa6abe..9999482 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/Tests/Node/GetAttrNodeTest.php b/Tests/Node/GetAttrNodeTest.php index dcc9811..d939afc 100644 --- a/Tests/Node/GetAttrNodeTest.php +++ b/Tests/Node/GetAttrNodeTest.php @@ -11,11 +11,11 @@ namespace Symfony\Component\ExpressionLanguage\Tests\Node; +use Symfony\Component\ExpressionLanguage\Node\ArgumentsNode; use Symfony\Component\ExpressionLanguage\Node\ArrayNode; use Symfony\Component\ExpressionLanguage\Node\ConstantNode; use Symfony\Component\ExpressionLanguage\Node\GetAttrNode; use Symfony\Component\ExpressionLanguage\Node\NameNode; -use Symfony\Component\ExpressionLanguage\Node\ArgumentsNode; class GetAttrNodeTest extends AbstractNodeTestCase { diff --git a/Token.php b/Token.php index 99f721f..4693904 100644 --- a/Token.php +++ b/Token.php @@ -45,7 +45,7 @@ public function __construct(string $type, string|int|float|null $value, ?int $cu */ 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 9512a10..dd72518 100644 --- a/TokenStream.php +++ b/TokenStream.php @@ -64,7 +64,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(); } 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