From a512a8ea4c566705790b196dc409bb5f9e8e5a02 Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Wed, 13 Nov 2024 18:03:50 +0100 Subject: [PATCH] First set of CS fixes --- src/Query/Query.php | 2 +- src/Toolkit/Query/BaseParser.php | 19 ++-- src/Toolkit/Query/Parser.php | 63 +++++++------- src/Toolkit/Query/Runners/Interpreted.php | 15 ++-- src/Toolkit/Query/Runners/Transpiled.php | 44 +++++++--- .../Query/Runners/Visitors/CodeGen.php | 68 ++++++++++----- .../Query/Runners/Visitors/Interpreter.php | 52 +++++++---- src/Toolkit/Query/Runtime.php | 36 +++++--- src/Toolkit/Query/Tokenizer.php | 86 ++++++++++++------- src/Toolkit/Query/Visitor.php | 34 +++++--- 10 files changed, 262 insertions(+), 157 deletions(-) diff --git a/src/Query/Query.php b/src/Query/Query.php index 6e77d1746a..043c0a9875 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -109,7 +109,7 @@ public function resolve(array|object $data = []): mixed $mode = App::instance()->option('query.runner', 'transpiled'); - if($mode === 'legacy') { + if ($mode === 'legacy') { return $this->resolve_legacy($data); } diff --git a/src/Toolkit/Query/BaseParser.php b/src/Toolkit/Query/BaseParser.php index b597cf391a..7e3fc459c4 100644 --- a/src/Toolkit/Query/BaseParser.php +++ b/src/Toolkit/Query/BaseParser.php @@ -19,13 +19,12 @@ abstract class BaseParser public function __construct( Tokenizer|Iterator $source, ) { - if($source instanceof Tokenizer) { - $this->tokens = $source->tokenize(); - } else { - $this->tokens = $source; + if ($source instanceof Tokenizer) { + $source = $source->tokenize(); } - $first = $this->tokens->current(); + $this->tokens = $source; + $first = $this->tokens->current(); if ($first === null) { throw new Exception('No tokens found.'); @@ -36,7 +35,7 @@ public function __construct( protected function consume(TokenType $type, string $message): Token { - if ($this->check($type)) { + if ($this->check($type) === true) { return $this->advance(); } @@ -45,7 +44,7 @@ protected function consume(TokenType $type, string $message): Token protected function check(TokenType $type): bool { - if ($this->isAtEnd()) { + if ($this->isAtEnd() === true) { return false; } @@ -54,7 +53,7 @@ protected function check(TokenType $type): bool protected function advance(): Token|null { - if (!$this->isAtEnd()) { + if ($this->isAtEnd() === false) { $this->previous = $this->current; $this->tokens->next(); $this->current = $this->tokens->current(); @@ -71,7 +70,7 @@ protected function isAtEnd(): bool protected function match(TokenType $type): Token|false { - if ($this->check($type)) { + if ($this->check($type) === true) { return $this->advance(); } @@ -81,7 +80,7 @@ protected function match(TokenType $type): Token|false protected function matchAny(array $types): Token|false { foreach ($types as $type) { - if ($this->check($type)) { + if ($this->check($type) === true) { return $this->advance(); } } diff --git a/src/Toolkit/Query/Parser.php b/src/Toolkit/Query/Parser.php index 8be677f1bf..f5b4801120 100644 --- a/src/Toolkit/Query/Parser.php +++ b/src/Toolkit/Query/Parser.php @@ -3,7 +3,6 @@ namespace Kirby\Toolkit\Query; use Exception; -use Iterator; use Kirby\Toolkit\Query\AST\ArgumentListNode; use Kirby\Toolkit\Query\AST\ArrayListNode; use Kirby\Toolkit\Query\AST\ClosureNode; @@ -17,18 +16,12 @@ class Parser extends BaseParser { - public function __construct( - Tokenizer|Iterator $source, - ) { - parent::__construct($source); - } - public function parse(): Node { $expression = $this->expression(); // ensure that we consumed all tokens - if(!$this->isAtEnd()) { + if ($this->isAtEnd() === false) { $this->consume(TokenType::T_EOF, 'Expect end of expression.'); } @@ -46,7 +39,7 @@ private function coalesce(): Node while ($this->match(TokenType::T_COALESCE)) { $right = $this->ternary(); - $left = new CoalesceNode($left, $right); + $left = new CoalesceNode($left, $right); } return $left; @@ -57,18 +50,23 @@ private function ternary(): Node $left = $this->memberAccess(); if ($tok = $this->matchAny([TokenType::T_QUESTION_MARK, TokenType::T_TERNARY_DEFAULT])) { - if($tok->type === TokenType::T_TERNARY_DEFAULT) { + if ($tok->type === TokenType::T_TERNARY_DEFAULT) { $trueIsDefault = true; - $trueBranch = null; - $falseBranch = $this->expression(); + $trueBranch = null; } else { $trueIsDefault = false; - $trueBranch = $this->expression(); + $trueBranch = $this->expression(); $this->consume(TokenType::T_COLON, 'Expect ":" after true branch.'); - $falseBranch = $this->expression(); } - return new TernaryNode($left, $trueBranch, $falseBranch, $trueIsDefault); + $falseBranch = $this->expression(); + + return new TernaryNode( + $left, + $trueBranch, + $falseBranch, + $trueIsDefault + ); } return $left; @@ -81,20 +79,24 @@ private function memberAccess(): Node while ($tok = $this->matchAny([TokenType::T_DOT, TokenType::T_NULLSAFE])) { $nullSafe = $tok->type === TokenType::T_NULLSAFE; - if($right = $this->match(TokenType::T_IDENTIFIER)) { + if ($right = $this->match(TokenType::T_IDENTIFIER)) { $right = $right->lexeme; - } elseif($right = $this->match(TokenType::T_INTEGER)) { + } elseif ($right = $this->match(TokenType::T_INTEGER)) { $right = $right->literal; } else { throw new Exception('Expect property name after ".".'); } - if($this->match(TokenType::T_OPEN_PAREN)) { + if ($this->match(TokenType::T_OPEN_PAREN)) { $arguments = $this->argumentList(); - $left = new MemberAccessNode($left, $right, $arguments, $nullSafe); - } else { - $left = new MemberAccessNode($left, $right, null, $nullSafe); } + + $left = new MemberAccessNode( + $left, + $right, + $arguments ?? null, + $nullSafe + ); } return $left; @@ -104,10 +106,10 @@ private function listUntil(TokenType $until): array { $elements = []; - while (!$this->isAtEnd() && !$this->check($until)) { + while ($this->isAtEnd() === false && $this->check($until) === false) { $elements[] = $this->expression(); - if (!$this->match(TokenType::T_COMMA)) { + if ($this->match(TokenType::T_COMMA) == false) { break; } } @@ -129,10 +131,11 @@ private function atomic(): Node { // float numbers if ($integer = $this->match(TokenType::T_INTEGER)) { - if($this->match(TokenType::T_DOT)) { + if ($this->match(TokenType::T_DOT)) { $fractional = $this->match(TokenType::T_INTEGER); return new LiteralNode((float)($integer->literal . '.' . $fractional->literal)); } + return new LiteralNode($integer->literal); } @@ -155,7 +158,7 @@ private function atomic(): Node // global functions and variables if ($token = $this->match(TokenType::T_IDENTIFIER)) { - if($this->match(TokenType::T_OPEN_PAREN)) { + if ($this->match(TokenType::T_OPEN_PAREN)) { $arguments = $this->argumentList(); return new GlobalFunctionNode($token->lexeme, $arguments); } @@ -167,7 +170,7 @@ private function atomic(): Node if ($token = $this->match(TokenType::T_OPEN_PAREN)) { $list = $this->listUntil(TokenType::T_CLOSE_PAREN); - if($this->match(TokenType::T_ARROW)) { + if ($this->match(TokenType::T_ARROW)) { $expression = $this->expression(); /** @@ -175,7 +178,7 @@ private function atomic(): Node * @var VariableNode[] $list */ foreach($list as $element) { - if(!$element instanceof VariableNode) { + if ($element instanceof VariableNode === false) { throw new Exception('Expecting only variables in closure argument list.'); } } @@ -183,13 +186,13 @@ private function atomic(): Node $arguments = array_map(fn ($element) => $element->name, $list); return new ClosureNode($arguments, $expression); } - if(count($list) > 1) { + + if (count($list) > 1) { throw new Exception('Expecting \"=>\" after closure argument list.'); } + // this is just a grouping return $list[0]; - - } throw new Exception('Expect expression.'); diff --git a/src/Toolkit/Query/Runners/Interpreted.php b/src/Toolkit/Query/Runners/Interpreted.php index 9ecf15aa03..139ed9ba77 100644 --- a/src/Toolkit/Query/Runners/Interpreted.php +++ b/src/Toolkit/Query/Runners/Interpreted.php @@ -5,6 +5,7 @@ use Closure; use Kirby\Toolkit\Query\Parser; use Kirby\Toolkit\Query\Runner; +use Kirby\Toolkit\Query\Runners\Visitors\Interpreter; use Kirby\Toolkit\Query\Tokenizer; class Interpreted extends Runner @@ -20,22 +21,24 @@ public function __construct( protected function getResolver(string $query): Closure { // load closure from process cache - if(isset(self::$cache[$query])) { + if (isset(self::$cache[$query])) { return self::$cache[$query]; } // on cache miss, parse query and generate closure - $t = new Tokenizer($query); - $parser = new Parser($t); - $node = $parser->parse(); + $tokenizer = new Tokenizer($query); + $parser = new Parser($tokenizer); + $node = $parser->parse(); $self = $this; return self::$cache[$query] = function (array $binding) use ($node, $self) { - $interpreter = new Visitors\Interpreter($self->allowedFunctions, $binding); - if($self->interceptor !== null) { + $interpreter = new Interpreter($self->allowedFunctions, $binding); + + if ($self->interceptor !== null) { $interpreter->setInterceptor($self->interceptor); } + return $node->accept($interpreter); }; } diff --git a/src/Toolkit/Query/Runners/Transpiled.php b/src/Toolkit/Query/Runners/Transpiled.php index 1d88e2042f..09a3d5fbfd 100644 --- a/src/Toolkit/Query/Runners/Transpiled.php +++ b/src/Toolkit/Query/Runners/Transpiled.php @@ -6,6 +6,7 @@ use Exception; use Kirby\Toolkit\Query\Parser; use Kirby\Toolkit\Query\Runner; +use Kirby\Toolkit\Query\Runners\Visitors\CodeGen; use Kirby\Toolkit\Query\Tokenizer; class Transpiled extends Runner @@ -24,7 +25,6 @@ public function __construct( ) { } - /** * Retrieves the executor closure for a given query. * If the closure is not already cached, it will be generated and stored in `Runner::$cacheFolder`. @@ -35,33 +35,43 @@ public function __construct( protected function getResolver(string $query): Closure { // load closure from process memory - if(isset(self::$cache[$query])) { + if (isset(self::$cache[$query])) { return self::$cache[$query]; } // load closure from file-cache / opcache - $hash = crc32($query); + $hash = crc32($query); $filename = self::$cacheFolder . '/' . $hash . '.php'; - if(file_exists($filename)) { + + if (file_exists($filename)) { return self::$cache[$query] = include $filename; } // on cache miss, parse query and generate closure - $t = new Tokenizer($query); - $parser = new Parser($t); - $node = $parser->parse(); - $codeGen = new Visitors\CodeGen($this->allowedFunctions); + $tokenizer = new Tokenizer($query); + $parser = new Parser($tokenizer); + $node = $parser->parse(); + $codeGen = new CodeGen($this->allowedFunctions); $functionBody = $node->accept($codeGen); - $mappings = join("\n", array_map(fn ($k, $v) => "$k = $v;", array_keys($codeGen->mappings), $codeGen->mappings)) . "\n"; - $comment = join("\n", array_map(fn ($l) => "// $l", explode("\n", $query))); + $mappings = array_map( + fn ($k, $v) => "$k = $v;", + array_keys($codeGen->mappings), + $codeGen->mappings + ); + $mappings = join("\n", $mappings) . "\n"; + + $comment = array_map(fn ($l) => "// $l", explode("\n", $query)); + $comment = join("\n", $comment); + + $uses = array_map(fn ($k) => "use $k;", array_keys($codeGen->uses)); + $uses = join("\n", $uses) . "\n"; - $uses = join("\n", array_map(fn ($k) => "use $k;", array_keys($codeGen->uses))) . "\n"; $function = "getResolver($query); - if(!is_callable($function)) { + + if (is_callable($function) === false) { throw new Exception('Query is not valid'); } - return $function($context, $this->allowedFunctions, $this->interceptor ?? fn ($v) => $v); + + return $function( + $context, + $this->allowedFunctions, + $this->interceptor ?? fn ($v) => $v + ); } } diff --git a/src/Toolkit/Query/Runners/Visitors/CodeGen.php b/src/Toolkit/Query/Runners/Visitors/CodeGen.php index 8b8c44defe..d28a766779 100644 --- a/src/Toolkit/Query/Runners/Visitors/CodeGen.php +++ b/src/Toolkit/Query/Runners/Visitors/CodeGen.php @@ -13,6 +13,7 @@ use Kirby\Toolkit\Query\AST\MemberAccessNode; use Kirby\Toolkit\Query\AST\TernaryNode; use Kirby\Toolkit\Query\AST\VariableNode; +use Kirby\Toolkit\Query\Runtime; use Kirby\Toolkit\Query\Visitor; /** @@ -51,8 +52,10 @@ private static function phpName(string $name): string * * @param array $validGlobalFunctions An array of valid global function closures. */ - public function __construct(public array $validGlobalFunctions = [], public array $directAccessFor = []) - { + public function __construct( + public array $validGlobalFunctions = [], + public array $directAccessFor = [] + ) { } private function intercept(string $value): string @@ -62,19 +65,27 @@ private function intercept(string $value): string public function visitArgumentList(ArgumentListNode $node): string { - $arguments = array_map(fn ($argument) => $argument->accept($this), $node->arguments); + $arguments = array_map( + fn ($argument) => $argument->accept($this), + $node->arguments + ); + return join(', ', $arguments); } public function visitArrayList(ArrayListNode $node): string { - $elements = array_map(fn ($element) => $element->accept($this), $node->elements); + $elements = array_map( + fn ($element) => $element->accept($this), + $node->elements + ); + return '[' . join(', ', $elements) . ']'; } public function visitCoalesce(CoalesceNode $node): string { - $left = $node->left->accept($this); + $left = $node->left->accept($this); $right = $node->right->accept($this); return "($left ?? $right)"; } @@ -89,28 +100,33 @@ public function visitMemberAccess(MemberAccessNode $node): string $object = $node->object->accept($this); $member = $node->member; - $this->uses['Kirby\\Toolkit\\Query\\Runtime'] = true; + $this->uses[Runtime::class] = true; $memberStr = var_export($member, true); - $nullSafe = $node->nullSafe ? 'true' : 'false'; + $nullSafe = $node->nullSafe ? 'true' : 'false'; - if($node->arguments) { + if ($node->arguments) { $arguments = $node->arguments->accept($this); - $member = var_export($member, true); + $member = var_export($member, true); - return $this->intercept("Runtime::access($object, $memberStr, $nullSafe, $arguments)"); + return $this->intercept( + "Runtime::access($object, $memberStr, $nullSafe, $arguments)" + ); } - return $this->intercept("Runtime::access($object, $memberStr, $nullSafe)"); + return $this->intercept( + "Runtime::access($object, $memberStr, $nullSafe)" + ); } public function visitTernary(TernaryNode $node): string { - $left = $node->condition->accept($this); + $left = $node->condition->accept($this); $falseBranch = $node->falseBranch->accept($this); - if($node->trueBranchIsDefault) { + if ($node->trueBranchIsDefault === true) { return "($left ?: $falseBranch)"; } + $trueBranch = $node->trueBranch->accept($this); return "($left ? $trueBranch : $falseBranch)"; @@ -118,15 +134,15 @@ public function visitTernary(TernaryNode $node): string public function visitVariable(VariableNode $node): string { - $name = $node->name(); + $name = $node->name(); $namestr = var_export($name, true); + $key = static::phpName($name); - $key = self::phpName($name); - if(isset($this->directAccessFor[$name])) { + if (isset($this->directAccessFor[$name])) { return $this->intercept($key); } - if(!isset($this->mappings[$key])) { + if (isset($this->mappings[$key]) === false) { $this->mappings[$key] = $this->intercept("match(true) { isset(\$context[$namestr]) && \$context[$namestr] instanceof Closure => \$context[$namestr](), isset(\$context[$namestr]) => \$context[$namestr], isset(\$functions[$namestr]) => \$functions[$namestr](), default => null }"); } @@ -139,25 +155,31 @@ public function visitVariable(VariableNode $node): string public function visitGlobalFunction(GlobalFunctionNode $node): string { $name = $node->name(); - if(!isset($this->validGlobalFunctions[$name])) { + + if (isset($this->validGlobalFunctions[$name])) { throw new Exception("Invalid global function $name"); } $arguments = $node->arguments->accept($this); - $name = var_export($name, true); + $name = var_export($name, true); return $this->intercept($this->intercept("\$functions[$name]") . "($arguments)"); } public function visitClosure(ClosureNode $node): mixed { - $this->uses['Kirby\\Toolkit\\Query\\Runtime'] = true; + $this->uses[Runtime::class] = true; - $args = array_map(self::phpName(...), $node->arguments); + $args = array_map(static::phpName(...), $node->arguments); $args = join(', ', $args); - $newDirectAccessFor = array_merge($this->directAccessFor, array_fill_keys($node->arguments, true)); + $newDirectAccessFor = [ + ...$this->directAccessFor, + ...array_fill_keys($node->arguments, true) + ]; - return "fn($args) => " . $node->body->accept(new self($this->validGlobalFunctions, $newDirectAccessFor)); + return "fn($args) => " . $node->body->accept( + new static($this->validGlobalFunctions, $newDirectAccessFor) + ); } } diff --git a/src/Toolkit/Query/Runners/Visitors/Interpreter.php b/src/Toolkit/Query/Runners/Visitors/Interpreter.php index 484c1fa02e..01c1e74507 100644 --- a/src/Toolkit/Query/Runners/Visitors/Interpreter.php +++ b/src/Toolkit/Query/Runners/Visitors/Interpreter.php @@ -33,12 +33,18 @@ public function __construct( public function visitArgumentList(ArgumentListNode $node): array { - return array_map(fn ($argument) => $argument->accept($this), $node->arguments); + return array_map( + fn ($argument) => $argument->accept($this), + $node->arguments + ); } public function visitArrayList(ArrayListNode $node): mixed { - return array_map(fn ($element) => $element->accept($this), $node->elements); + return array_map( + fn ($element) => $element->accept($this), + $node->elements + ); } public function visitCoalesce(CoalesceNode $node): mixed @@ -50,7 +56,7 @@ public function visitLiteral(LiteralNode $node): mixed { $val = $node->value; - if($this->interceptor !== null) { + if ($this->interceptor !== null) { $val = ($this->interceptor)($val); } @@ -60,15 +66,20 @@ public function visitLiteral(LiteralNode $node): mixed public function visitMemberAccess(MemberAccessNode $node): mixed { $left = $node->object->accept($this); - $item = null; - if($node->arguments !== null) { - $item = Runtime::access($left, $node->member, $node->nullSafe, ...$node->arguments->accept($this)); + + if ($node->arguments !== null) { + $item = Runtime::access( + $left, + $node->member, + $node->nullSafe, + ...$node->arguments->accept($this) + ); } else { $item = Runtime::access($left, $node->member, $node->nullSafe); } - if($this->interceptor !== null) { + if ($this->interceptor !== null) { $item = ($this->interceptor)($item); } @@ -77,11 +88,16 @@ public function visitMemberAccess(MemberAccessNode $node): mixed public function visitTernary(TernaryNode $node): mixed { - if($node->trueBranchIsDefault) { - return $node->condition->accept($this) ?: $node->trueBranch->accept($this); + if ($node->trueBranchIsDefault === true) { + return + $node->condition->accept($this) ?: + $node->trueBranch->accept($this); } - return $node->condition->accept($this) ? $node->trueBranch->accept($this) : $node->falseBranch->accept($this); + return + $node->condition->accept($this) ? + $node->trueBranch->accept($this) : + $node->falseBranch->accept($this); } public function visitVariable(VariableNode $node): mixed @@ -97,7 +113,7 @@ public function visitVariable(VariableNode $node): mixed default => null, }; - if($this->interceptor !== null) { + if ($this->interceptor !== null) { $item = ($this->interceptor)($item); } @@ -108,18 +124,19 @@ public function visitGlobalFunction(GlobalFunctionNode $node): mixed { $name = $node->name(); - if(!isset($this->validGlobalFunctions[$name])) { + if (isset($this->validGlobalFunctions[$name]) === false) { throw new Exception("Invalid global function $name"); } $function = $this->validGlobalFunctions[$name]; - if($this->interceptor !== null) { + + if ($this->interceptor !== null) { $function = ($this->interceptor)($function); } $result = $function(...$node->arguments->accept($this)); - if($this->interceptor !== null) { + if ($this->interceptor !== null) { $result = ($this->interceptor)($result); } @@ -131,7 +148,7 @@ public function visitClosure(ClosureNode $node): mixed $self = $this; return function (...$params) use ($self, $node) { - $context = $self->context; + $context = $self->context; $functions = $self->validGlobalFunctions; // [key1, key2] + [value1, value2] => [key1 => value1, key2 => value2] @@ -140,8 +157,9 @@ public function visitClosure(ClosureNode $node): mixed $params ); - $visitor = new self($functions, [...$context, ...$arguments]); - if($self->interceptor !== null) { + $visitor = new static($functions, [...$context, ...$arguments]); + + if ($self->interceptor !== null) { $visitor->setInterceptor($self->interceptor); } diff --git a/src/Toolkit/Query/Runtime.php b/src/Toolkit/Query/Runtime.php index 9c102b741f..66be6a59cf 100644 --- a/src/Toolkit/Query/Runtime.php +++ b/src/Toolkit/Query/Runtime.php @@ -2,40 +2,50 @@ namespace Kirby\Toolkit\Query; +use Closure; use Exception; class Runtime { - public static function access(array|object|null $object, string|int $key, bool $nullSafe = false, ...$arguments): mixed - { - if($nullSafe && $object === null) { + public static function access( + array|object|null $object, + string|int $key, + bool $nullSafe = false, + ...$arguments + ): mixed { + if ($nullSafe === true && $object === null) { return null; } - if(is_array($object)) { - $item = ($object[$key] ?? $object[(string)$key] ?? null); - - if($item) { - if($arguments) { + if (is_array($object)) { + if ($item = $object[$key] ?? $object[(string)$key] ?? null) { + if ($arguments) { return $item(...$arguments); } - if($item instanceof \Closure) { + + if ($item instanceof Closure) { return $item(); } } return $item; } - if(is_object($object)) { - if(is_int($key)) { + + if (is_object($object)) { + if (is_int($key)) { $key = (string)$key; } - if(method_exists($object, $key) || method_exists($object, '__call')) { + + if ( + method_exists($object, $key) || + method_exists($object, '__call') + ) { return $object->$key(...$arguments); } + return $object->$key ?? null; } - throw new Exception("Cannot access \"$key\" on " . gettype($object)); + throw new Exception("Cannot access \"$key\" on " . gettype($object)); } } diff --git a/src/Toolkit/Query/Tokenizer.php b/src/Toolkit/Query/Tokenizer.php index 993be68de0..26f76772e5 100644 --- a/src/Toolkit/Query/Tokenizer.php +++ b/src/Toolkit/Query/Tokenizer.php @@ -42,12 +42,14 @@ public function tokenize(): Generator $current = 0; while ($current < $this->length) { - $t = self::scanToken($this->source, $current); + $token = static::scanToken($this->source, $current); + // don't yield whitespace tokens (ignore them) - if($t->type !== TokenType::T_WHITESPACE) { - yield $t; + if ($token->type !== TokenType::T_WHITESPACE) { + yield $token; } - $current += mb_strlen($t->lexeme); + + $current += mb_strlen($token->lexeme); } yield new Token(TokenType::T_EOF, '', null); @@ -63,37 +65,50 @@ public function tokenize(): Generator */ protected static function scanToken(string $source, int $current): Token { - $l = ''; - $c = $source[$current]; + $lex = ''; + $char = $source[$current]; return match(true) { // single character tokens - $c === '.' => new Token(TokenType::T_DOT, '.'), - $c === '(' => new Token(TokenType::T_OPEN_PAREN, '('), - $c === ')' => new Token(TokenType::T_CLOSE_PAREN, ')'), - $c === '[' => new Token(TokenType::T_OPEN_BRACKET, '['), - $c === ']' => new Token(TokenType::T_CLOSE_BRACKET, ']'), - $c === ',' => new Token(TokenType::T_COMMA, ','), - $c === ':' => new Token(TokenType::T_COLON, ':'), + $char === '.' => new Token(TokenType::T_DOT, '.'), + $char === '(' => new Token(TokenType::T_OPEN_PAREN, '('), + $char === ')' => new Token(TokenType::T_CLOSE_PAREN, ')'), + $char === '[' => new Token(TokenType::T_OPEN_BRACKET, '['), + $char === ']' => new Token(TokenType::T_CLOSE_BRACKET, ']'), + $char === ',' => new Token(TokenType::T_COMMA, ','), + $char === ':' => new Token(TokenType::T_COLON, ':'), // two character tokens - self::match($source, $current, '\?\?', $l) => new Token(TokenType::T_COALESCE, $l), - self::match($source, $current, '\?\s*\.', $l) => new Token(TokenType::T_NULLSAFE, $l), - self::match($source, $current, '\?\s*:', $l) => new Token(TokenType::T_TERNARY_DEFAULT, $l), - self::match($source, $current, '=>', $l) => new Token(TokenType::T_ARROW, $l), - - // make sure this check comes after the two above that check for '?' in the beginning - $c === '?' => new Token(TokenType::T_QUESTION_MARK, '?'), + static::match($source, $current, '\?\?', $lex) + => new Token(TokenType::T_COALESCE, $lex), + static::match($source, $current, '\?\s*\.', $lex) + => new Token(TokenType::T_NULLSAFE, $lex), + static::match($source, $current, '\?\s*:', $lex) + => new Token(TokenType::T_TERNARY_DEFAULT, $lex), + static::match($source, $current, '=>', $lex) + => new Token(TokenType::T_ARROW, $lex), + + // make sure this check comes after the two above + // that check for '?' in the beginning + $char === '?' => new Token(TokenType::T_QUESTION_MARK, '?'), // multi character tokens - self::match($source, $current, '\s+', $l) => new Token(TokenType::T_WHITESPACE, $l), - self::match($source, $current, 'true', $l, true) => new Token(TokenType::T_TRUE, $l, true), - self::match($source, $current, 'false', $l, true) => new Token(TokenType::T_FALSE, $l, false), - self::match($source, $current, 'null', $l, true) => new Token(TokenType::T_NULL, $l, null), - self::match($source, $current, self::DOUBLEQUOTE_STRING_REGEX, $l) => new Token(TokenType::T_STRING, $l, stripcslashes(substr($l, 1, -1))), - self::match($source, $current, self::SINGLEQUOTE_STRING_REGEX, $l) => new Token(TokenType::T_STRING, $l, stripcslashes(substr($l, 1, -1))), - self::match($source, $current, '\d+\b', $l) => new Token(TokenType::T_INTEGER, $l, (int)$l), - self::match($source, $current, self::IDENTIFIER_REGEX, $l) => new Token(TokenType::T_IDENTIFIER, $l), + static::match($source, $current, '\s+', $lex) + => new Token(TokenType::T_WHITESPACE, $lex), + static::match($source, $current, 'true', $lex, true) + => new Token(TokenType::T_TRUE, $lex, true), + static::match($source, $current, 'false', $lex, true) + => new Token(TokenType::T_FALSE, $lex, false), + static::match($source, $current, 'null', $lex, true) + => new Token(TokenType::T_NULL, $lex, null), + static::match($source, $current, static::DOUBLEQUOTE_STRING_REGEX, $lex) + => new Token(TokenType::T_STRING, $lex, stripcslashes(substr($lex, 1, -1))), + static::match($source, $current, static::SINGLEQUOTE_STRING_REGEX, $lex) + => new Token(TokenType::T_STRING, $lex, stripcslashes(substr($lex, 1, -1))), + static::match($source, $current, '\d+\b', $lex) + => new Token(TokenType::T_INTEGER, $lex, (int)$lex), + static::match($source, $current, static::IDENTIFIER_REGEX, $lex) + => new Token(TokenType::T_IDENTIFIER, $lex), // unknown token default => throw new Exception("Unexpected character: {$source[$current]}"), @@ -111,17 +126,26 @@ protected static function scanToken(string $source, int $current): Token * @param bool $caseIgnore Whether to ignore case while matching * @return bool Whether the regex pattern was matched */ - protected static function match(string $source, int $current, string $regex, string &$lexeme, bool $caseIgnore = false): bool - { + protected static function match( + string $source, + int $current, + string $regex, + string &$lexeme, + bool $caseIgnore = false + ): bool { $regex = '/\G' . $regex . '/u'; - if($caseIgnore) { + + if ($caseIgnore) { $regex .= 'i'; } + $matches = []; preg_match($regex, $source, $matches, 0, $current); + if (empty($matches[0])) { return false; } + $lexeme = $matches[0]; return true; } diff --git a/src/Toolkit/Query/Visitor.php b/src/Toolkit/Query/Visitor.php index 41db64ddc3..fc58257799 100644 --- a/src/Toolkit/Query/Visitor.php +++ b/src/Toolkit/Query/Visitor.php @@ -4,36 +4,46 @@ use Closure; use Exception; +use Kirby\Toolkit\Query\AST\ArgumentListNode; +use Kirby\Toolkit\Query\AST\ArrayListNode; +use Kirby\Toolkit\Query\AST\ClosureNode; +use Kirby\Toolkit\Query\AST\CoalesceNode; +use Kirby\Toolkit\Query\AST\GlobalFunctionNode; +use Kirby\Toolkit\Query\AST\LiteralNode; +use Kirby\Toolkit\Query\AST\MemberAccessNode; +use Kirby\Toolkit\Query\AST\Node; +use Kirby\Toolkit\Query\AST\TernaryNode; +use Kirby\Toolkit\Query\AST\VariableNode; use ReflectionClass; abstract class Visitor { protected Closure|null $interceptor = null; - public function visitNode(AST\Node $node): mixed + public function visitNode(Node $node): mixed { $shortName = (new ReflectionClass($node))->getShortName(); // remove the "Node" suffix $shortName = substr($shortName, 0, -4); + $method = 'visit' . $shortName; - $method = 'visit' . $shortName; - if(method_exists($this, $method)) { + if (method_exists($this, $method)) { return $this->$method($node); } throw new Exception('No visitor method for ' . $node::class); } - abstract public function visitArgumentList(AST\ArgumentListNode $node): mixed; - abstract public function visitArrayList(AST\ArrayListNode $node): mixed; - abstract public function visitCoalesce(AST\CoalesceNode $node): mixed; - abstract public function visitLiteral(AST\LiteralNode $node): mixed; - abstract public function visitMemberAccess(AST\MemberAccessNode $node): mixed; - abstract public function visitTernary(AST\TernaryNode $node): mixed; - abstract public function visitVariable(AST\VariableNode $node): mixed; - abstract public function visitGlobalFunction(AST\GlobalFunctionNode $node): mixed; - abstract public function visitClosure(AST\ClosureNode $node): mixed; + abstract public function visitArgumentList(ArgumentListNode $node): mixed; + abstract public function visitArrayList(ArrayListNode $node): mixed; + abstract public function visitCoalesce(CoalesceNode $node): mixed; + abstract public function visitLiteral(LiteralNode $node): mixed; + abstract public function visitMemberAccess(MemberAccessNode $node): mixed; + abstract public function visitTernary(TernaryNode $node): mixed; + abstract public function visitVariable(VariableNode $node): mixed; + abstract public function visitGlobalFunction(GlobalFunctionNode $node): mixed; + abstract public function visitClosure(ClosureNode $node): mixed; /** * Sets and activates an interceptor closure that is called for each resolved value.