diff --git a/.gitignore b/.gitignore
index 3e04865..e4898ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,8 @@
/tests/log
/composer.lock
/coverage.clover
-/.phpunit.result.cache
+/.phpunit.*
/vendor/
/.idea
/.composer
+.DS_Store
diff --git a/.styleci.yml b/.styleci.yml
index 93c2ae9..bab8d79 100644
--- a/.styleci.yml
+++ b/.styleci.yml
@@ -1,5 +1,5 @@
---
-preset: psr2
+preset: psr12
risky: true
version: 8.4
finder:
diff --git a/README.md b/README.md
index 49b26ab..6159b14 100644
--- a/README.md
+++ b/README.md
@@ -6,9 +6,9 @@
[![Code Quality][Master quality image]][Master quality]
[](https://styleci.io/repos/39503126)
-You're looking at a standalone PHP library to parse and evaluate text based rules with a Javascript-like syntax. This project was born out of the necessity to evaluate hundreds of rules that were originally written and evaluated in JavaScript, and now needed to be evaluated on the server-side, using PHP.
+A standalone PHP library to parse and evaluate text-based rules using a JavaScript-like syntax. This project was born out of the need to evaluate hundreds of rules originally written in JavaScript on the server side, using PHP.
-This library has initially been used to change and configure the behavior of certain "Workflows" (without changing actual code) in an intranet application, but it may serve a purpose elsewhere.
+The library was initially used to configure the behavior of "Workflows" in an intranet application without changing actual code, but it may serve a purpose elsewhere.
## Install
@@ -44,7 +44,7 @@ $variables = [
'coupon_code' => (string) $_POST['coupon_code'],
];
-$rule = new Rule('coupon_code.test(/^summer20[0-9]{2}$/) == true', $variables);
+$rule = new Rule('coupon_code.test(/^summer20[0-9]{2}$/)', $variables);
var_dump($rule->isTrue()); // bool(true)
```
@@ -56,6 +56,60 @@ $rule = new Rule('points >= 50 && points <= 100', $variables);
var_dump($rule->isTrue()); // bool(true)
```
+Perform arithmetic operations
+```php
+$variables = ['price' => 100, 'quantity' => 3];
+
+$rule = new Rule('price * quantity > 250', $variables);
+var_dump($rule->isTrue()); // bool(true)
+```
+
+Arithmetic operators follow standard precedence rules: `*`, `/`, `%` bind tighter than `+`, `-`. Parentheses can be used to override precedence.
+```php
+$rule = new Rule('2 + 3 * 4 == 14');
+var_dump($rule->isTrue()); // bool(true) - multiplication before addition
+
+$rule = new Rule('(2 + 3) * 4 == 20');
+var_dump($rule->isTrue()); // bool(true) - parentheses override precedence
+```
+
+Unary minus and logical NOT operators are also supported:
+```php
+$rule = new Rule('-5 * 3 == -15');
+var_dump($rule->isTrue()); // bool(true) - unary minus binds tighter than multiplication
+
+$rule = new Rule('--5 == 5');
+var_dump($rule->isTrue()); // bool(true) - double negation
+
+$rule = new Rule('!false');
+var_dump($rule->isTrue()); // bool(true) - logical NOT
+
+$rule = new Rule('!(1 == 2)');
+var_dump($rule->isTrue()); // bool(true) - NOT with parenthesized comparison
+
+$rule = new Rule('!foo', ['foo' => false]);
+var_dump($rule->isTrue()); // bool(true) - NOT with variable
+```
+
+Get the actual computed result of an expression (not just true/false)
+```php
+$rule = new Rule('5 * 3');
+var_dump($rule->result()); // int(15)
+
+$rule = new Rule('"hello " + "world"');
+var_dump($rule->result()); // string("hello world")
+
+$rule = new Rule('parseInt("42")');
+var_dump($rule->result()); // int(42)
+
+$rule = new Rule('price * quantity', ['price' => 100, 'quantity' => 3]);
+var_dump($rule->result()); // int(300)
+
+// Comparison and logical expressions still return bool
+$rule = new Rule('foo > 5', ['foo' => 10]);
+var_dump($rule->result()); // bool(true)
+```
+
Call methods on objects from within rules
```php
class User
@@ -73,54 +127,62 @@ $variables = [
$rule = new Rule('user.points() > 300', $variables);
var_dump($rule->isTrue()); // bool(true)
```
+
For security reasons, PHP's magic methods like `__construct` and `__destruct` cannot be
called from within rules. However, `__call` will be invoked automatically if available,
-unless the called method is defined.
+unless the called method is defined.
## Built-in Methods
-Name | Example
------------ | ------------------------
-charAt | `"foo".charAt(2) === "o"`
-concat | `"foo".concat("bar", "baz") === "foobarbaz"`
-endsWith | `"foo".endsWith("oo") === true`
-startsWith | `"foo".startsWith("fo") === true`
-indexOf | `"foo".indexOf("oo") === 1`
-join | `["foo", "bar"].join(",") === "foo,bar"`
-replace | `"foo".replace("oo", "aa") === "faa"`
-split | `"foo-bar".split("-") === ["foo", "bar"]`
-substr | `"foo".substr(1) === "oo"`
-test | `"foo".test(/oo$/) === true`
-toLowerCase | `"FOO".toLowerCase() === "foo"`
-toUpperCase | `"foo".toUpperCase() === "FOO"`
+| Name | Example |
+| ----------- | ------------------------ |
+| charAt | `"foo".charAt(2) === "o"` |
+| concat | `"foo".concat("bar", "baz") === "foobarbaz"` |
+| endsWith | `"foo".endsWith("oo")` |
+| startsWith | `"foo".startsWith("fo")` |
+| indexOf | `"foo".indexOf("oo") === 1` |
+| join | `["foo", "bar"].join(",") === "foo,bar"` |
+| replace | `"foo".replace("oo", "aa") === "faa"` |
+| split | `"foo-bar".split("-") === ["foo", "bar"]` |
+| substr | `"foo".substr(1) === "oo"` |
+| test | `"foo".test(/oo$/)` |
+| toLowerCase | `"FOO".toLowerCase() === "foo"` |
+| toUpperCase | `"foo".toUpperCase() === "FOO"` |
## Built-in Functions
-Name | Example
------------ | ------------------------
-parseInt | `parseInt("22aa") === 22`
-parseFloat | `parseFloat("3.1") === 3.1`
+| Name | Example |
+| ----------- | ------------------------ |
+| parseInt | `parseInt("22aa") === 22` |
+| parseFloat | `parseFloat("3.1") === 3.1` |
## Supported Operators
-Type | Description | Operator
------------ | ------------------------ | ----------
-Comparison | greater than | >
-Comparison | greater than or equal to | >=
-Comparison | less than | <
-Comparison | less or equal to | <=
-Comparison | equal to | ==
-Comparison | not equal to | !=
-Comparison | identical | ===
-Comparison | not identical | !==
-Containment | contains | in
-Containment | does not contain | not in
-Logical | and | &&
-Logical | or | \|\|
+| Type | Description | Operator |
+| ----------- | ------------------------ | ---------- |
+| Comparison | greater than | > |
+| Comparison | greater than or equal to | >= |
+| Comparison | less than | < |
+| Comparison | less or equal to | <= |
+| Comparison | equal to | == |
+| Comparison | not equal to | != |
+| Comparison | identical | === |
+| Comparison | not identical | !== |
+| Containment | contains | in |
+| Containment | does not contain | not in |
+| Logical | and | && |
+| Logical | or | \|\| |
+| Arithmetic | addition | + |
+| Arithmetic | subtraction | - |
+| Arithmetic | multiplication | * |
+| Arithmetic | division | / |
+| Arithmetic | modulo | % |
+| Unary | negation | - |
+| Unary | logical NOT | ! |
## Error Handling
-Both, `$rule->isTrue()` and `$rule->isFalse()` will throw an exception if the syntax is invalid. These calls can either be placed inside a `try` / `catch` block, or it can be checked prior using `$rule->isValid()`.
+Both `$rule->isTrue()` and `$rule->isFalse()` will throw an exception if the syntax is invalid. These calls can either be placed inside a `try` / `catch` block, or validity can be checked beforehand using `$rule->isValid()`.
```php
$ruleStr = '
@@ -143,18 +205,19 @@ Or alternatively:
```php
if (!$rule->isValid()) {
- echo $rule->getError();
+ echo $rule->error;
}
```
-Both will output: `Unexpected token "(" at position 25 on line 3`
+Both will output: `Unexpected "(" at position 28`
## Syntax Highlighting
-
+
A custom syntax highlighter is also provided.
```php
-use nicoSWD\Rule;
+use nicoSWD\Rule\Highlighter\Highlighter;
+use nicoSWD\Rule\TokenStream\Token\TokenType;
$ruleStr = '
// This is true
@@ -170,7 +233,7 @@ $ruleStr = '
bar > 6
)';
-$highlighter = new Rule\Highlighter\Highlighter(new Rule\Tokenizer());
+$highlighter = new Highlighter();
// Optional custom styles
$highlighter->setStyle(
@@ -185,11 +248,6 @@ Outputs:

-## Notes
-
-- Parentheses can be nested, and will be evaluated from right to left.
-- Only value/variable comparison expressions with optional logical ANDs/ORs, are supported.
-
## Security
If you discover any security related issues, please email security@nic0.me instead of using the issue tracker.
@@ -202,26 +260,24 @@ $ composer test
## Contributing
-Pull requests are very welcome! If they include tests, even better. This project follows PSR-2 coding standards, please make sure your pull requests do too.
+Pull requests are very welcome! If they include tests, even better. This project follows [PSR-12](https://www.php-fig.org/psr/psr-12/) coding standards, please make sure your pull requests do too.
## To Do
- Support for object properties (foo.length)
-- Support for returning actual results, other than true or false
- Support for array / string dereferencing: "foo"[1]
-- Don't force boolean comparison for tokens that are already booleans. `my_func() && 2 > 1` should work
-- Change regex and implementation for method calls. ".split(" should not be the token
-- Add / implement missing methods
-- Add "typeof" construct
-- Do math (?)
-- Allow string concatenating with "+"
-- Invalid regex modifiers should not result in an unknown token
-- Duplicate regex modifiers should throw an error
+- ~~Support for returning actual results, other than true or false~~
+- ~~Don't force boolean comparison for tokens that are already booleans. `my_func() && 2 > 1` should work~~
+- ~~Allow string concatenating with "+"~~
+- ~~Duplicate regex modifiers should throw an error~~
- ~~Add support for function calls~~
- ~~Support for regular expressions~~
- ~~Fix build on PHP 7 / Nightly~~
- ~~Allow variables in arrays~~
- ~~Verify function and method name spelling (.tOuPpErCAse() is currently valid)~~
+- ~~Change regex and implementation for method calls~~
+- ~~Add / implement missing methods~~
+- ~~Invalid regex modifiers should not result in an unknown token~~
- ...
## License
diff --git a/composer.json b/composer.json
index bbcab55..cf26d85 100644
--- a/composer.json
+++ b/composer.json
@@ -33,11 +33,13 @@
}
},
"require": {
- "php": ">=8.4"
+ "php": ">=8.5",
+ "ext-ctype": "*"
},
"require-dev": {
- "phpunit/phpunit": "^12.3",
- "mockery/mockery": "^1.6"
+ "phpunit/phpunit": "^13.1",
+ "mockery/mockery": "^1.6",
+ "squizlabs/php_codesniffer": "^4.0"
},
"scripts": {
"test": "vendor/bin/phpunit"
diff --git a/phpunit.xml.dist.bak b/phpunit.xml.dist.bak
new file mode 100644
index 0000000..db6f484
--- /dev/null
+++ b/phpunit.xml.dist.bak
@@ -0,0 +1,15 @@
+
+
+
+
+ src
+
+
+
+
+ tests/
+
+
+
diff --git a/src/AST/AdditionNode.php b/src/AST/AdditionNode.php
new file mode 100644
index 0000000..c9dbd1c
--- /dev/null
+++ b/src/AST/AdditionNode.php
@@ -0,0 +1,32 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+final class AdditionNode extends Node
+{
+ public function __construct(
+ public readonly Node $left,
+ public readonly Node $right,
+ ) {
+ }
+
+ public function evaluate(EvaluationContext $context): mixed
+ {
+ $left = $this->left->evaluate($context);
+ $right = $this->right->evaluate($context);
+
+ if (is_string($left) || is_string($right)) {
+ return (string) $left . (string) $right;
+ }
+
+ return $left + $right;
+ }
+}
diff --git a/src/AST/ArrayNode.php b/src/AST/ArrayNode.php
new file mode 100644
index 0000000..1d5f59e
--- /dev/null
+++ b/src/AST/ArrayNode.php
@@ -0,0 +1,34 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+final class ArrayNode extends Node
+{
+ /** @param Node[] $items */
+ public function __construct(
+ public readonly array $items,
+ ) {
+ }
+
+ /**
+ * @throws \RuntimeException
+ */
+ public function evaluate(EvaluationContext $context): array
+ {
+ $items = [];
+
+ foreach ($this->items as $item) {
+ $items[] = $item->evaluate($context);
+ }
+
+ return $items;
+ }
+}
diff --git a/src/AST/AstEvaluator.php b/src/AST/AstEvaluator.php
new file mode 100644
index 0000000..0809fc9
--- /dev/null
+++ b/src/AST/AstEvaluator.php
@@ -0,0 +1,49 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+use nicoSWD\Rule\Parser\Exception\ParserException;
+use nicoSWD\Rule\TokenStream\FunctionRegistry;
+use nicoSWD\Rule\TokenStream\MethodRegistry;
+use nicoSWD\Rule\TokenStream\Token\TokenFactory;
+use nicoSWD\Rule\TokenStream\VariableRegistry;
+
+final readonly class AstEvaluator
+{
+ private EvaluationContext $context;
+
+ public function __construct(
+ VariableRegistry $variableRegistry,
+ FunctionRegistry $functionRegistry,
+ MethodRegistry $methodRegistry,
+ TokenFactory $tokenFactory,
+ ) {
+ $this->context = new EvaluationContext($variableRegistry, $functionRegistry, $methodRegistry, $tokenFactory);
+ }
+
+ /**
+ * @throws ParserException
+ */
+ public function evaluate(Node $node): bool
+ {
+ return (bool) $this->resolve($node);
+ }
+
+ /**
+ * Resolve a node to its actual computed value, without casting to bool.
+ *
+ * @throws ParserException
+ */
+ public function resolve(Node $node): mixed
+ {
+ return $node->evaluate($this->context);
+ }
+}
diff --git a/src/AST/BoolNode.php b/src/AST/BoolNode.php
new file mode 100644
index 0000000..34ca10a
--- /dev/null
+++ b/src/AST/BoolNode.php
@@ -0,0 +1,24 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+final class BoolNode extends ValueNode
+{
+ public function __construct(
+ public readonly bool $value,
+ ) {
+ }
+
+ public function evaluate(EvaluationContext $context): bool
+ {
+ return $this->value;
+ }
+}
diff --git a/src/AST/ComparisonNode.php b/src/AST/ComparisonNode.php
new file mode 100644
index 0000000..88c60ae
--- /dev/null
+++ b/src/AST/ComparisonNode.php
@@ -0,0 +1,57 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+use nicoSWD\Rule\Parser\Exception\ParserException;
+
+final class ComparisonNode extends Node
+{
+ public function __construct(
+ public readonly Node $left,
+ public readonly Node $right,
+ public readonly ComparisonOperator $operator,
+ ) {
+ }
+
+ /**
+ * @throws ParserException
+ */
+ public function evaluate(EvaluationContext $context): bool
+ {
+ $leftValue = $this->left->evaluate($context);
+ $rightValue = $this->right->evaluate($context);
+
+ return match ($this->operator) {
+ ComparisonOperator::EQUAL => $leftValue == $rightValue,
+ ComparisonOperator::EQUAL_STRICT => $leftValue === $rightValue,
+ ComparisonOperator::NOT_EQUAL => $leftValue != $rightValue,
+ ComparisonOperator::NOT_EQUAL_STRICT => $leftValue !== $rightValue,
+ ComparisonOperator::LESS_THAN => $leftValue < $rightValue,
+ ComparisonOperator::GREATER_THAN => $leftValue > $rightValue,
+ ComparisonOperator::LESS_THAN_EQUAL => $leftValue <= $rightValue,
+ ComparisonOperator::GREATER_THAN_EQUAL => $leftValue >= $rightValue,
+ ComparisonOperator::IN => $this->evaluateIn($leftValue, $rightValue),
+ ComparisonOperator::NOT_IN => !$this->evaluateIn($leftValue, $rightValue),
+ };
+ }
+
+ /**
+ * @throws ParserException
+ */
+ private function evaluateIn(mixed $leftValue, mixed $rightValue): bool
+ {
+ if (!is_array($rightValue)) {
+ throw ParserException::expectedArray(gettype($rightValue));
+ }
+
+ return in_array($leftValue, $rightValue, strict: true);
+ }
+}
diff --git a/src/AST/ComparisonOperator.php b/src/AST/ComparisonOperator.php
new file mode 100644
index 0000000..512e357
--- /dev/null
+++ b/src/AST/ComparisonOperator.php
@@ -0,0 +1,25 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+enum ComparisonOperator: string
+{
+ case EQUAL = '==';
+ case EQUAL_STRICT = '===';
+ case NOT_EQUAL = '!=';
+ case NOT_EQUAL_STRICT = '!==';
+ case LESS_THAN = '<';
+ case GREATER_THAN = '>';
+ case LESS_THAN_EQUAL = '<=';
+ case GREATER_THAN_EQUAL = '>=';
+ case IN = 'in';
+ case NOT_IN = 'not in';
+}
diff --git a/src/AST/DivisionNode.php b/src/AST/DivisionNode.php
new file mode 100644
index 0000000..ca2080d
--- /dev/null
+++ b/src/AST/DivisionNode.php
@@ -0,0 +1,36 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+final class DivisionNode extends Node
+{
+ public function __construct(
+ public readonly Node $left,
+ public readonly Node $right,
+ ) {
+ }
+
+ public function evaluate(EvaluationContext $context): mixed
+ {
+ $left = $this->left->evaluate($context);
+ $right = $this->right->evaluate($context);
+
+ if ($right == 0) {
+ if ($left == 0) {
+ return NAN;
+ }
+
+ return $left > 0 ? INF : -INF;
+ }
+
+ return $left / $right;
+ }
+}
diff --git a/src/AST/EvaluationContext.php b/src/AST/EvaluationContext.php
new file mode 100644
index 0000000..3c5f684
--- /dev/null
+++ b/src/AST/EvaluationContext.php
@@ -0,0 +1,27 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+use nicoSWD\Rule\TokenStream\FunctionRegistry;
+use nicoSWD\Rule\TokenStream\MethodRegistry;
+use nicoSWD\Rule\TokenStream\Token\TokenFactory;
+use nicoSWD\Rule\TokenStream\VariableRegistry;
+
+final readonly class EvaluationContext
+{
+ public function __construct(
+ public VariableRegistry $variableRegistry,
+ public FunctionRegistry $functionRegistry,
+ public MethodRegistry $methodRegistry,
+ public TokenFactory $tokenFactory,
+ ) {
+ }
+}
diff --git a/src/AST/FloatNode.php b/src/AST/FloatNode.php
new file mode 100644
index 0000000..fac9361
--- /dev/null
+++ b/src/AST/FloatNode.php
@@ -0,0 +1,24 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+final class FloatNode extends ValueNode
+{
+ public function __construct(
+ public readonly float $value,
+ ) {
+ }
+
+ public function evaluate(EvaluationContext $context): float
+ {
+ return $this->value;
+ }
+}
diff --git a/src/AST/FunctionCallNode.php b/src/AST/FunctionCallNode.php
new file mode 100644
index 0000000..18c7fd3
--- /dev/null
+++ b/src/AST/FunctionCallNode.php
@@ -0,0 +1,56 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+use nicoSWD\Rule\Parser\Exception\ParserException;
+use nicoSWD\Rule\TokenStream\Exception\UndefinedFunctionException;
+
+final class FunctionCallNode extends Node
+{
+ /** @param Node[] $arguments */
+ public function __construct(
+ public readonly string $name,
+ public readonly array $arguments,
+ public readonly int $offset = 0,
+ ) {
+ }
+
+ /**
+ * @throws ParserException
+ */
+ public function evaluate(EvaluationContext $context): mixed
+ {
+ $args = $this->resolveArguments($context);
+
+ try {
+ $function = $context->functionRegistry->get($this->name);
+ } catch (UndefinedFunctionException) {
+ throw ParserException::undefinedFunction($this->name, $this->offset);
+ }
+
+ return $function->call(...$args)->getValue();
+ }
+
+ /**
+ * @throws ParserException
+ */
+ private function resolveArguments(EvaluationContext $context): array
+ {
+ $resolved = [];
+
+ foreach ($this->arguments as $argument) {
+ $value = $argument->evaluate($context);
+ $resolved[] = $value;
+ }
+
+ return $resolved;
+ }
+}
diff --git a/src/AST/IntegerNode.php b/src/AST/IntegerNode.php
new file mode 100644
index 0000000..3ab421c
--- /dev/null
+++ b/src/AST/IntegerNode.php
@@ -0,0 +1,24 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+final class IntegerNode extends ValueNode
+{
+ public function __construct(
+ public readonly int $value,
+ ) {
+ }
+
+ public function evaluate(EvaluationContext $context): int
+ {
+ return $this->value;
+ }
+}
diff --git a/src/AST/LogicalNode.php b/src/AST/LogicalNode.php
new file mode 100644
index 0000000..00c0928
--- /dev/null
+++ b/src/AST/LogicalNode.php
@@ -0,0 +1,37 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+use nicoSWD\Rule\Parser\Exception\ParserException;
+
+final class LogicalNode extends Node
+{
+ public function __construct(
+ public readonly Node $left,
+ public readonly Node $right,
+ public readonly LogicalOperator $operator,
+ ) {
+ }
+
+ /**
+ * @throws ParserException
+ */
+ public function evaluate(EvaluationContext $context): bool
+ {
+ $left = (bool) $this->left->evaluate($context);
+
+ // Short-circuit evaluation
+ return match ($this->operator) {
+ LogicalOperator::AND => $left && (bool) $this->right->evaluate($context),
+ LogicalOperator::OR => $left || (bool) $this->right->evaluate($context),
+ };
+ }
+}
diff --git a/src/Compiler/CompilerFactoryInterface.php b/src/AST/LogicalOperator.php
similarity index 58%
rename from src/Compiler/CompilerFactoryInterface.php
rename to src/AST/LogicalOperator.php
index 69a2ed6..1a3bd20 100644
--- a/src/Compiler/CompilerFactoryInterface.php
+++ b/src/AST/LogicalOperator.php
@@ -5,9 +5,13 @@
* @link https://github.com/nicoSWD
* @author Nicolas Oelgart
*/
-namespace nicoSWD\Rule\Compiler;
-interface CompilerFactoryInterface
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+enum LogicalOperator: string
{
- public function create(): CompilerInterface;
+ case AND = '&&';
+ case OR = '||';
}
diff --git a/src/AST/MethodCallNode.php b/src/AST/MethodCallNode.php
new file mode 100644
index 0000000..6b3b64d
--- /dev/null
+++ b/src/AST/MethodCallNode.php
@@ -0,0 +1,76 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+use nicoSWD\Rule\Parser\Exception\ParserException;
+use nicoSWD\Rule\TokenStream\Exception\ForbiddenMethodException;
+use nicoSWD\Rule\TokenStream\Exception\UndefinedMethodException;
+use nicoSWD\Rule\TokenStream\Token\TokenKind;
+
+final class MethodCallNode extends Node
+{
+ /** @param Node[] $arguments */
+ public function __construct(
+ public readonly Node $object,
+ public readonly string $name,
+ public readonly array $arguments,
+ public readonly int $offset = 0,
+ ) {
+ }
+
+ /**
+ * @throws ParserException
+ */
+ public function evaluate(EvaluationContext $context): mixed
+ {
+ $objectValue = $this->object->evaluate($context);
+
+ // For regex nodes, preserve the original token for type detection
+ if ($this->object instanceof RegexNode) {
+ $objectToken = $this->object->originalToken;
+ } else {
+ $objectToken = $context->tokenFactory->createFromPHPType($objectValue);
+ }
+
+ $args = $this->resolveArguments($context);
+
+ try {
+ $method = $context->methodRegistry->get($this->name, $objectToken, $objectValue);
+ } catch (UndefinedMethodException) {
+ throw ParserException::undefinedMethod($this->name, $this->offset);
+ } catch (ForbiddenMethodException) {
+ throw ParserException::forbiddenMethod($this->name, $objectToken);
+ }
+
+ $result = $method->call(...$args);
+
+ if ($result->isOfKind(TokenKind::ARRAY)) {
+ return $result->toArray();
+ }
+
+ return $result->getValue();
+ }
+
+ /**
+ * @throws ParserException
+ */
+ private function resolveArguments(EvaluationContext $context): array
+ {
+ $resolved = [];
+
+ foreach ($this->arguments as $argument) {
+ $value = $argument->evaluate($context);
+ $resolved[] = $value;
+ }
+
+ return $resolved;
+ }
+}
diff --git a/src/AST/ModuloNode.php b/src/AST/ModuloNode.php
new file mode 100644
index 0000000..21b0b8c
--- /dev/null
+++ b/src/AST/ModuloNode.php
@@ -0,0 +1,32 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+final class ModuloNode extends Node
+{
+ public function __construct(
+ public readonly Node $left,
+ public readonly Node $right,
+ ) {
+ }
+
+ public function evaluate(EvaluationContext $context): mixed
+ {
+ $left = $this->left->evaluate($context);
+ $right = $this->right->evaluate($context);
+
+ if ($right == 0) {
+ return NAN;
+ }
+
+ return $left % $right;
+ }
+}
diff --git a/src/AST/MultiplicationNode.php b/src/AST/MultiplicationNode.php
new file mode 100644
index 0000000..0feed42
--- /dev/null
+++ b/src/AST/MultiplicationNode.php
@@ -0,0 +1,28 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+final class MultiplicationNode extends Node
+{
+ public function __construct(
+ public readonly Node $left,
+ public readonly Node $right,
+ ) {
+ }
+
+ public function evaluate(EvaluationContext $context): mixed
+ {
+ $left = $this->left->evaluate($context);
+ $right = $this->right->evaluate($context);
+
+ return $left * $right;
+ }
+}
diff --git a/src/AST/Node.php b/src/AST/Node.php
new file mode 100644
index 0000000..6a5f7db
--- /dev/null
+++ b/src/AST/Node.php
@@ -0,0 +1,24 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+abstract class Node
+{
+ /**
+ * Evaluate this node and return its computed value.
+ *
+ * @throws \RuntimeException
+ */
+ public function evaluate(EvaluationContext $context): mixed
+ {
+ throw new \RuntimeException('Node type ' . static::class . ' does not support evaluation');
+ }
+}
diff --git a/src/AST/NotNode.php b/src/AST/NotNode.php
new file mode 100644
index 0000000..f43913a
--- /dev/null
+++ b/src/AST/NotNode.php
@@ -0,0 +1,30 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+final class NotNode extends Node
+{
+ public function __construct(
+ public readonly Node $node,
+ public readonly int $offset = 0,
+ ) {
+ }
+
+ /**
+ * @throws \RuntimeException
+ */
+ public function evaluate(EvaluationContext $context): mixed
+ {
+ $value = $this->node->evaluate($context);
+
+ return !$value;
+ }
+}
diff --git a/src/TokenStream/Token/TokenBoolTrue.php b/src/AST/NullNode.php
similarity index 50%
rename from src/TokenStream/Token/TokenBoolTrue.php
rename to src/AST/NullNode.php
index e6cbf38..cc38d50 100644
--- a/src/TokenStream/Token/TokenBoolTrue.php
+++ b/src/AST/NullNode.php
@@ -1,16 +1,19 @@
-
*/
-namespace nicoSWD\Rule\TokenStream\Token;
-final class TokenBoolTrue extends TokenBool
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+final class NullNode extends ValueNode
{
- public function getValue(): bool
+ public function evaluate(EvaluationContext $context): null
{
- return true;
+ return null;
}
}
diff --git a/src/AST/RegexNode.php b/src/AST/RegexNode.php
new file mode 100644
index 0000000..0b27ccd
--- /dev/null
+++ b/src/AST/RegexNode.php
@@ -0,0 +1,30 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+use nicoSWD\Rule\TokenStream\Token\BaseToken;
+
+final class RegexNode extends ValueNode
+{
+ public readonly string $pattern;
+ public readonly BaseToken $originalToken;
+
+ public function __construct(string $pattern, BaseToken $originalToken)
+ {
+ $this->pattern = $pattern;
+ $this->originalToken = $originalToken;
+ }
+
+ public function evaluate(EvaluationContext $context): string
+ {
+ return $this->pattern;
+ }
+}
diff --git a/src/AST/StringNode.php b/src/AST/StringNode.php
new file mode 100644
index 0000000..4636489
--- /dev/null
+++ b/src/AST/StringNode.php
@@ -0,0 +1,24 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+final class StringNode extends ValueNode
+{
+ public function __construct(
+ public readonly string $value,
+ ) {
+ }
+
+ public function evaluate(EvaluationContext $context): string
+ {
+ return $this->value;
+ }
+}
diff --git a/src/AST/SubtractionNode.php b/src/AST/SubtractionNode.php
new file mode 100644
index 0000000..9bc6d3a
--- /dev/null
+++ b/src/AST/SubtractionNode.php
@@ -0,0 +1,28 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+final class SubtractionNode extends Node
+{
+ public function __construct(
+ public readonly Node $left,
+ public readonly Node $right,
+ ) {
+ }
+
+ public function evaluate(EvaluationContext $context): mixed
+ {
+ $left = $this->left->evaluate($context);
+ $right = $this->right->evaluate($context);
+
+ return $left - $right;
+ }
+}
diff --git a/src/AST/UnaryMinusNode.php b/src/AST/UnaryMinusNode.php
new file mode 100644
index 0000000..a187690
--- /dev/null
+++ b/src/AST/UnaryMinusNode.php
@@ -0,0 +1,30 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+final class UnaryMinusNode extends Node
+{
+ public function __construct(
+ public readonly Node $node,
+ public readonly int $offset = 0,
+ ) {
+ }
+
+ /**
+ * @throws \RuntimeException
+ */
+ public function evaluate(EvaluationContext $context): mixed
+ {
+ $value = $this->node->evaluate($context);
+
+ return -$value;
+ }
+}
diff --git a/src/TokenStream/Token/Type/Method.php b/src/AST/ValueNode.php
similarity index 65%
rename from src/TokenStream/Token/Type/Method.php
rename to src/AST/ValueNode.php
index dabe45a..0c944ce 100644
--- a/src/TokenStream/Token/Type/Method.php
+++ b/src/AST/ValueNode.php
@@ -5,8 +5,11 @@
* @link https://github.com/nicoSWD
* @author Nicolas Oelgart
*/
-namespace nicoSWD\Rule\TokenStream\Token\Type;
-interface Method
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+abstract class ValueNode extends Node
{
}
diff --git a/src/AST/VariableNode.php b/src/AST/VariableNode.php
new file mode 100644
index 0000000..d798d3b
--- /dev/null
+++ b/src/AST/VariableNode.php
@@ -0,0 +1,42 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\AST;
+
+use nicoSWD\Rule\Parser\Exception\ParserException;
+use nicoSWD\Rule\TokenStream\Exception\UndefinedVariableException;
+use nicoSWD\Rule\TokenStream\Token\TokenKind;
+
+final class VariableNode extends Node
+{
+ public function __construct(
+ public readonly string $name,
+ public readonly int $offset = 0,
+ ) {
+ }
+
+ /**
+ * @throws ParserException
+ */
+ public function evaluate(EvaluationContext $context): mixed
+ {
+ try {
+ $token = $context->variableRegistry->get($this->name);
+ } catch (UndefinedVariableException) {
+ throw ParserException::undefinedVariable($this->name, $this->offset);
+ }
+
+ if ($token->isOfKind(TokenKind::ARRAY)) {
+ return $token->toArray();
+ }
+
+ return $token->getValue();
+ }
+}
diff --git a/src/Compiler/CompilerFactory.php b/src/Compiler/CompilerFactory.php
deleted file mode 100644
index 0be47bc..0000000
--- a/src/Compiler/CompilerFactory.php
+++ /dev/null
@@ -1,16 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Compiler;
-
-class CompilerFactory implements CompilerFactoryInterface
-{
- public function create(): CompilerInterface
- {
- return new StandardCompiler();
- }
-}
diff --git a/src/Compiler/CompilerInterface.php b/src/Compiler/CompilerInterface.php
deleted file mode 100644
index 832cce6..0000000
--- a/src/Compiler/CompilerInterface.php
+++ /dev/null
@@ -1,23 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Compiler;
-
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\Type\Logical;
-use nicoSWD\Rule\TokenStream\Token\Type\Parenthesis;
-
-interface CompilerInterface
-{
- public function getCompiledRule(): string;
-
- public function addParentheses(BaseToken & Parenthesis $token): void;
-
- public function addLogical(BaseToken & Logical $token): void;
-
- public function addBoolean(bool $bool): void;
-}
diff --git a/src/Compiler/Exception/MissingOperatorException.php b/src/Compiler/Exception/MissingOperatorException.php
deleted file mode 100644
index e551ffd..0000000
--- a/src/Compiler/Exception/MissingOperatorException.php
+++ /dev/null
@@ -1,14 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Compiler\Exception;
-
-final class MissingOperatorException extends \Exception
-{
- /** @var string */
- protected $message = 'Missing operator';
-}
diff --git a/src/Compiler/StandardCompiler.php b/src/Compiler/StandardCompiler.php
deleted file mode 100644
index 4006d39..0000000
--- a/src/Compiler/StandardCompiler.php
+++ /dev/null
@@ -1,119 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Compiler;
-
-use nicoSWD\Rule\Compiler\Exception\MissingOperatorException;
-use nicoSWD\Rule\Evaluator\Boolean;
-use nicoSWD\Rule\Evaluator\Operator;
-use nicoSWD\Rule\Parser\Exception\ParserException;
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\TokenAnd;
-use nicoSWD\Rule\TokenStream\Token\TokenOpeningParenthesis;
-use nicoSWD\Rule\TokenStream\Token\Type\Logical;
-use nicoSWD\Rule\TokenStream\Token\Type\Parenthesis;
-
-final class StandardCompiler implements CompilerInterface
-{
- private const string OPENING_PARENTHESIS = '(';
- private const string CLOSING_PARENTHESIS = ')';
-
- private string $output = '';
- private int $openParenthesis = 0;
- private int $closedParenthesis = 0;
-
- /** @throws ParserException */
- public function getCompiledRule(): string
- {
- if ($this->isIncompleteCondition()) {
- throw new ParserException('Incomplete condition');
- } elseif (!$this->numParenthesesMatch()) {
- throw new ParserException('Missing closing parenthesis');
- }
-
- return $this->output;
- }
-
- private function openParenthesis(): void
- {
- $this->openParenthesis++;
- $this->output .= self::OPENING_PARENTHESIS;
- }
-
- /** @throws ParserException */
- private function closeParenthesis(): void
- {
- if ($this->openParenthesis < 1) {
- throw new ParserException('Missing opening parenthesis');
- }
-
- $this->closedParenthesis++;
- $this->output .= self::CLOSING_PARENTHESIS;
- }
-
- /** @throws ParserException */
- public function addParentheses(BaseToken & Parenthesis $token): void
- {
- if ($token instanceof TokenOpeningParenthesis) {
- if (!$this->expectOpeningParenthesis()) {
- throw ParserException::unexpectedToken($token);
- }
- $this->openParenthesis();
- } else {
- $this->closeParenthesis();
- }
- }
-
- /** @throws ParserException */
- public function addLogical(BaseToken & Logical $token): void
- {
- if (Operator::tryFrom($this->getLastChar()) !== null) {
- throw ParserException::unexpectedToken($token);
- }
-
- if ($token instanceof TokenAnd) {
- $this->output .= Operator::LOGICAL_AND->value;
- } else {
- $this->output .= Operator::LOGICAL_OR->value;
- }
- }
-
- /** @throws MissingOperatorException */
- public function addBoolean(bool $bool): void
- {
- if (Boolean::tryFrom($this->getLastChar()) !== null) {
- throw new MissingOperatorException();
- }
-
- $this->output .= Boolean::fromBool($bool)->value;
- }
-
- private function numParenthesesMatch(): bool
- {
- return $this->openParenthesis === $this->closedParenthesis;
- }
-
- private function isIncompleteCondition(): bool
- {
- return Operator::tryFrom($this->getLastChar()) !== null;
- }
-
- private function expectOpeningParenthesis(): bool
- {
- $lastChar = $this->getLastChar();
-
- return
- $lastChar === '' ||
- $lastChar === self::OPENING_PARENTHESIS ||
- Operator::tryFrom($lastChar) !== null;
- }
-
- private function getLastChar(): string
- {
- return substr($this->output, offset: -1);
- }
-}
diff --git a/src/Evaluator/Boolean.php b/src/Evaluator/Boolean.php
deleted file mode 100644
index 529ee71..0000000
--- a/src/Evaluator/Boolean.php
+++ /dev/null
@@ -1,19 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Evaluator;
-
-enum Boolean: string
-{
- case TRUE = '1';
- case FALSE = '0';
-
- final public static function fromBool(bool $bool): self
- {
- return $bool ? self::TRUE : self::FALSE;
- }
-}
diff --git a/src/Evaluator/Evaluator.php b/src/Evaluator/Evaluator.php
deleted file mode 100644
index e9672ec..0000000
--- a/src/Evaluator/Evaluator.php
+++ /dev/null
@@ -1,68 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Evaluator;
-
-use Closure;
-
-final class Evaluator implements EvaluatorInterface
-{
- public function evaluate(string $group): bool
- {
- $evalGroup = $this->evalGroup();
- $count = 0;
-
- do {
- $group = preg_replace_callback(
- '~\((?[^()]+)\)~',
- $evalGroup,
- $group,
- limit: -1,
- count: $count
- );
- } while ($count > 0);
-
- return (bool) $evalGroup(['match' => $group]);
- }
-
- private function evalGroup(): Closure
- {
- return function (array $group): ?int {
- $result = null;
- $operator = null;
- $offset = 0;
-
- while (isset($group['match'][$offset])) {
- $value = $group['match'][$offset++];
- $possibleOperator = Operator::tryFrom($value);
-
- if ($possibleOperator) {
- $operator = $possibleOperator;
- } elseif (Boolean::tryFrom($value)) {
- $result = $this->setResult($result, (int) $value, $operator);
- } else {
- throw new Exception\UnknownSymbolException(sprintf('Unexpected "%s"', $value));
- }
- }
-
- return $result;
- };
- }
-
- private function setResult(?int $result, int $value, ?Operator $operator): int
- {
- if (!isset($result)) {
- $result = $value;
- } elseif (Operator::isAnd($operator)) {
- $result &= $value;
- } elseif (Operator::isOr($operator)) {
- $result |= $value;
- }
-
- return $result;
- }
-}
diff --git a/src/Evaluator/EvaluatorInterface.php b/src/Evaluator/EvaluatorInterface.php
deleted file mode 100644
index dee7ecd..0000000
--- a/src/Evaluator/EvaluatorInterface.php
+++ /dev/null
@@ -1,13 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Evaluator;
-
-interface EvaluatorInterface
-{
- public function evaluate(string $group): bool;
-}
diff --git a/src/Evaluator/Exception/UnknownSymbolException.php b/src/Evaluator/Exception/UnknownSymbolException.php
deleted file mode 100644
index 85ddec5..0000000
--- a/src/Evaluator/Exception/UnknownSymbolException.php
+++ /dev/null
@@ -1,12 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Evaluator\Exception;
-
-final class UnknownSymbolException extends \Exception
-{
-}
diff --git a/src/Evaluator/Operator.php b/src/Evaluator/Operator.php
deleted file mode 100644
index 651fa30..0000000
--- a/src/Evaluator/Operator.php
+++ /dev/null
@@ -1,24 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Evaluator;
-
-enum Operator: string
-{
- case LOGICAL_AND = '&';
- case LOGICAL_OR = '|';
-
- public static function isAnd(self $operator): bool
- {
- return $operator === self::LOGICAL_AND;
- }
-
- public static function isOr(self $operator): bool
- {
- return $operator === self::LOGICAL_OR;
- }
-}
diff --git a/src/Expression/BaseExpression.php b/src/Expression/BaseExpression.php
deleted file mode 100644
index 5cff5a4..0000000
--- a/src/Expression/BaseExpression.php
+++ /dev/null
@@ -1,16 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Expression;
-
-use nicoSWD\Rule\Parser\Exception\ParserException;
-
-abstract class BaseExpression
-{
- /** @throws ParserException */
- abstract public function evaluate(mixed $leftValue, mixed $rightValue): bool;
-}
diff --git a/src/Expression/EqualExpression.php b/src/Expression/EqualExpression.php
deleted file mode 100644
index 89b2eb6..0000000
--- a/src/Expression/EqualExpression.php
+++ /dev/null
@@ -1,16 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Expression;
-
-final class EqualExpression extends BaseExpression
-{
- public function evaluate(mixed $leftValue, mixed $rightValue): bool
- {
- return $leftValue == $rightValue;
- }
-}
diff --git a/src/Expression/EqualStrictExpression.php b/src/Expression/EqualStrictExpression.php
deleted file mode 100644
index de98cbe..0000000
--- a/src/Expression/EqualStrictExpression.php
+++ /dev/null
@@ -1,26 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Expression;
-
-use nicoSWD\Rule\TokenStream\TokenCollection;
-
-final class EqualStrictExpression extends BaseExpression
-{
- public function evaluate(mixed $leftValue, mixed $rightValue): bool
- {
- if ($leftValue instanceof TokenCollection) {
- $leftValue = $leftValue->toArray();
- }
-
- if ($rightValue instanceof TokenCollection) {
- $rightValue = $rightValue->toArray();
- }
-
- return $leftValue === $rightValue;
- }
-}
diff --git a/src/Expression/ExpressionFactory.php b/src/Expression/ExpressionFactory.php
deleted file mode 100644
index c87dab4..0000000
--- a/src/Expression/ExpressionFactory.php
+++ /dev/null
@@ -1,34 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Expression;
-
-use nicoSWD\Rule\Parser\Exception\ParserException;
-use nicoSWD\Rule\TokenStream\Token;
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\Type\Operator;
-
-final class ExpressionFactory implements ExpressionFactoryInterface
-{
- /** @throws ParserException */
- public function createFromOperator(BaseToken & Operator $operator): BaseExpression
- {
- return match ($operator::class) {
- Token\TokenEqual::class => new EqualExpression(),
- Token\TokenEqualStrict::class => new EqualStrictExpression(),
- Token\TokenNotEqual::class => new NotEqualExpression(),
- Token\TokenNotEqualStrict::class => new NotEqualStrictExpression(),
- Token\TokenGreater::class => new GreaterThanExpression(),
- Token\TokenSmaller::class => new LessThanExpression(),
- Token\TokenSmallerEqual::class => new LessThanEqualExpression(),
- Token\TokenGreaterEqual::class => new GreaterThanEqualExpression(),
- Token\TokenIn::class => new InExpression(),
- Token\TokenNotIn::class => new NotInExpression(),
- default => throw ParserException::unknownOperator($operator),
- };
- }
-}
diff --git a/src/Expression/ExpressionFactoryInterface.php b/src/Expression/ExpressionFactoryInterface.php
deleted file mode 100644
index 488b437..0000000
--- a/src/Expression/ExpressionFactoryInterface.php
+++ /dev/null
@@ -1,16 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Expression;
-
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\Type\Operator;
-
-interface ExpressionFactoryInterface
-{
- public function createFromOperator(BaseToken & Operator $operator): BaseExpression;
-}
diff --git a/src/Expression/GreaterThanEqualExpression.php b/src/Expression/GreaterThanEqualExpression.php
deleted file mode 100644
index 5f413d9..0000000
--- a/src/Expression/GreaterThanEqualExpression.php
+++ /dev/null
@@ -1,16 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Expression;
-
-final class GreaterThanEqualExpression extends BaseExpression
-{
- public function evaluate(mixed $leftValue, mixed $rightValue): bool
- {
- return $leftValue >= $rightValue;
- }
-}
diff --git a/src/Expression/GreaterThanExpression.php b/src/Expression/GreaterThanExpression.php
deleted file mode 100644
index d9dedb5..0000000
--- a/src/Expression/GreaterThanExpression.php
+++ /dev/null
@@ -1,16 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Expression;
-
-final class GreaterThanExpression extends BaseExpression
-{
- public function evaluate(mixed $leftValue, mixed $rightValue): bool
- {
- return $leftValue > $rightValue;
- }
-}
diff --git a/src/Expression/InExpression.php b/src/Expression/InExpression.php
deleted file mode 100644
index 837e7ce..0000000
--- a/src/Expression/InExpression.php
+++ /dev/null
@@ -1,30 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Expression;
-
-use nicoSWD\Rule\TokenStream\TokenCollection;
-use nicoSWD\Rule\Parser\Exception\ParserException;
-
-final class InExpression extends BaseExpression
-{
- public function evaluate(mixed $leftValue, mixed $rightValue): bool
- {
- if ($rightValue instanceof TokenCollection) {
- $rightValue = $rightValue->toArray();
- }
-
- if (!is_array($rightValue)) {
- throw new ParserException(sprintf(
- 'Expected array, got "%s"',
- gettype($rightValue)
- ));
- }
-
- return in_array($leftValue, $rightValue, strict: true);
- }
-}
diff --git a/src/Expression/LessThanEqualExpression.php b/src/Expression/LessThanEqualExpression.php
deleted file mode 100644
index 5ca7959..0000000
--- a/src/Expression/LessThanEqualExpression.php
+++ /dev/null
@@ -1,16 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Expression;
-
-final class LessThanEqualExpression extends BaseExpression
-{
- public function evaluate(mixed $leftValue, mixed $rightValue): bool
- {
- return $leftValue <= $rightValue;
- }
-}
diff --git a/src/Expression/LessThanExpression.php b/src/Expression/LessThanExpression.php
deleted file mode 100644
index 89d9f5f..0000000
--- a/src/Expression/LessThanExpression.php
+++ /dev/null
@@ -1,16 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Expression;
-
-final class LessThanExpression extends BaseExpression
-{
- public function evaluate(mixed $leftValue, mixed $rightValue): bool
- {
- return $leftValue < $rightValue;
- }
-}
diff --git a/src/Expression/NotEqualExpression.php b/src/Expression/NotEqualExpression.php
deleted file mode 100644
index fba4703..0000000
--- a/src/Expression/NotEqualExpression.php
+++ /dev/null
@@ -1,16 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Expression;
-
-final class NotEqualExpression extends BaseExpression
-{
- public function evaluate(mixed $leftValue, mixed $rightValue): bool
- {
- return $leftValue != $rightValue;
- }
-}
diff --git a/src/Expression/NotEqualStrictExpression.php b/src/Expression/NotEqualStrictExpression.php
deleted file mode 100644
index 162f973..0000000
--- a/src/Expression/NotEqualStrictExpression.php
+++ /dev/null
@@ -1,16 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Expression;
-
-final class NotEqualStrictExpression extends BaseExpression
-{
- public function evaluate(mixed $leftValue, mixed $rightValue): bool
- {
- return $leftValue !== $rightValue;
- }
-}
diff --git a/src/Expression/NotInExpression.php b/src/Expression/NotInExpression.php
deleted file mode 100644
index 518ba9d..0000000
--- a/src/Expression/NotInExpression.php
+++ /dev/null
@@ -1,30 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Expression;
-
-use nicoSWD\Rule\TokenStream\TokenCollection;
-use nicoSWD\Rule\Parser\Exception\ParserException;
-
-final class NotInExpression extends BaseExpression
-{
- public function evaluate(mixed $leftValue, mixed $rightValue): bool
- {
- if ($rightValue instanceof TokenCollection) {
- $rightValue = $rightValue->toArray();
- }
-
- if (!is_array($rightValue)) {
- throw new ParserException(sprintf(
- 'Expected array, got "%s"',
- gettype($rightValue)
- ));
- }
-
- return !in_array($leftValue, $rightValue, strict: true);
- }
-}
diff --git a/src/Grammar/CallableFunction.php b/src/Grammar/CallableFunction.php
index 4694ab8..5941d21 100644
--- a/src/Grammar/CallableFunction.php
+++ b/src/Grammar/CallableFunction.php
@@ -1,22 +1,23 @@
-
*/
-namespace nicoSWD\Rule\Grammar;
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\Grammar;
-abstract class CallableFunction implements CallableUserFunctionInterface
+abstract class CallableFunction implements CallableInterface
{
public function __construct(
- protected readonly ?BaseToken $token = null,
+ protected readonly mixed $token = null,
) {
}
- protected function parseParameter(array $parameters, int $numParam): ?BaseToken
+ protected function parseParameter(array $parameters, int $numParam): mixed
{
return $parameters[$numParam] ?? null;
}
diff --git a/src/Grammar/CallableUserFunctionInterface.php b/src/Grammar/CallableInterface.php
similarity index 77%
rename from src/Grammar/CallableUserFunctionInterface.php
rename to src/Grammar/CallableInterface.php
index 55ef2a4..6887ab1 100644
--- a/src/Grammar/CallableUserFunctionInterface.php
+++ b/src/Grammar/CallableInterface.php
@@ -5,13 +5,14 @@
* @link https://github.com/nicoSWD
* @author Nicolas Oelgart
*/
+
namespace nicoSWD\Rule\Grammar;
use nicoSWD\Rule\Parser\Exception\ParserException;
use nicoSWD\Rule\TokenStream\Token\BaseToken;
-interface CallableUserFunctionInterface
+interface CallableInterface
{
/** @throws ParserException */
- public function call(?BaseToken ...$param): BaseToken;
+ public function call(mixed ...$param): BaseToken;
}
diff --git a/src/Grammar/Definition.php b/src/Grammar/Definition.php
deleted file mode 100644
index 324922a..0000000
--- a/src/Grammar/Definition.php
+++ /dev/null
@@ -1,20 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Grammar;
-
-use nicoSWD\Rule\TokenStream\Token\Token;
-
-final readonly class Definition
-{
- public function __construct(
- public Token $token,
- public string $regex,
- public int $priority,
- ) {
- }
-}
diff --git a/src/Grammar/Grammar.php b/src/Grammar/Grammar.php
index 1ce1820..40d6c87 100644
--- a/src/Grammar/Grammar.php
+++ b/src/Grammar/Grammar.php
@@ -1,60 +1,20 @@
-
*/
-namespace nicoSWD\Rule\Grammar;
-use SplPriorityQueue;
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\Grammar;
abstract class Grammar
{
- private string $compiledRegex = '';
- /** @var Definition[] */
- private array $tokens = [];
-
- /** @return Definition[] */
- abstract public function getDefinition(): array;
-
- /** @return InternalFunction[] */
+ /** @return InternalCallable[] */
abstract public function getInternalFunctions(): array;
- /** @return InternalMethod[] */
+ /** @return InternalCallable[] */
abstract public function getInternalMethods(): array;
-
- public function buildRegex(): string
- {
- if (!$this->compiledRegex) {
- $this->registerTokens();
- $regex = [];
-
- foreach ($this->getQueue() as $token) {
- $regex[] = "(?<{$token->token->value}>{$token->regex})";
- }
-
- $this->compiledRegex = '~(' . implode('|', $regex) . ')~As';
- }
-
- return $this->compiledRegex;
- }
-
- private function getQueue(): SplPriorityQueue
- {
- $queue = new SplPriorityQueue();
-
- foreach ($this->tokens as $token) {
- $queue->insert($token, $token->priority);
- }
-
- return $queue;
- }
-
- private function registerTokens(): void
- {
- foreach ($this->getDefinition() as $definition) {
- $this->tokens[$definition->token->value] = $definition;
- }
- }
}
diff --git a/src/Grammar/InternalFunction.php b/src/Grammar/InternalCallable.php
similarity index 80%
rename from src/Grammar/InternalFunction.php
rename to src/Grammar/InternalCallable.php
index fbfdf58..b87d77e 100644
--- a/src/Grammar/InternalFunction.php
+++ b/src/Grammar/InternalCallable.php
@@ -1,13 +1,16 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\Grammar;
-final readonly class InternalFunction
+final readonly class InternalCallable
{
public function __construct(
public string $name,
diff --git a/src/Grammar/InternalMethod.php b/src/Grammar/InternalMethod.php
deleted file mode 100644
index 8bec86d..0000000
--- a/src/Grammar/InternalMethod.php
+++ /dev/null
@@ -1,17 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Grammar;
-
-final readonly class InternalMethod
-{
- public function __construct(
- public string $name,
- public string $class,
- ) {
- }
-}
diff --git a/src/Grammar/JavaScript/Functions/ParseFloat.php b/src/Grammar/JavaScript/Functions/ParseFloat.php
index e627879..d62a5d9 100644
--- a/src/Grammar/JavaScript/Functions/ParseFloat.php
+++ b/src/Grammar/JavaScript/Functions/ParseFloat.php
@@ -1,27 +1,30 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\Grammar\JavaScript\Functions;
use nicoSWD\Rule\Grammar\CallableFunction;
-use nicoSWD\Rule\Grammar\CallableUserFunctionInterface;
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\TokenFloat;
+use nicoSWD\Rule\Grammar\CallableInterface;
+use nicoSWD\Rule\TokenStream\Token\GenericToken;
+use nicoSWD\Rule\TokenStream\Token\TokenKind;
-final class ParseFloat extends CallableFunction implements CallableUserFunctionInterface
+final class ParseFloat extends CallableFunction implements CallableInterface
{
- public function call(?BaseToken ...$parameters): BaseToken
+ public function call(mixed ...$parameters): GenericToken
{
$value = $this->parseParameter($parameters, numParam: 0);
- if (!isset($value)) {
- return new TokenFloat(NAN);
+ if ($value === null) {
+ return new GenericToken(TokenKind::FLOAT, NAN);
}
- return new TokenFloat((float) $value->getValue());
+ return new GenericToken(TokenKind::FLOAT, (float) $value);
}
}
diff --git a/src/Grammar/JavaScript/Functions/ParseInt.php b/src/Grammar/JavaScript/Functions/ParseInt.php
index b693162..968ec68 100644
--- a/src/Grammar/JavaScript/Functions/ParseInt.php
+++ b/src/Grammar/JavaScript/Functions/ParseInt.php
@@ -1,27 +1,30 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\Grammar\JavaScript\Functions;
use nicoSWD\Rule\Grammar\CallableFunction;
-use nicoSWD\Rule\Grammar\CallableUserFunctionInterface;
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\TokenInteger;
+use nicoSWD\Rule\Grammar\CallableInterface;
+use nicoSWD\Rule\TokenStream\Token\GenericToken;
+use nicoSWD\Rule\TokenStream\Token\TokenKind;
-final class ParseInt extends CallableFunction implements CallableUserFunctionInterface
+final class ParseInt extends CallableFunction implements CallableInterface
{
- public function call(?BaseToken ...$parameters): BaseToken
+ public function call(mixed ...$parameters): GenericToken
{
$value = $this->parseParameter($parameters, numParam: 0);
- if (!isset($value)) {
- return new TokenInteger(NAN);
+ if ($value === null) {
+ return new GenericToken(TokenKind::FLOAT, NAN);
}
- return new TokenInteger((int) $value->getValue());
+ return new GenericToken(TokenKind::INTEGER, (int) $value);
}
}
diff --git a/src/Grammar/JavaScript/JavaScript.php b/src/Grammar/JavaScript/JavaScript.php
index 84e84aa..8bdf951 100644
--- a/src/Grammar/JavaScript/JavaScript.php
+++ b/src/Grammar/JavaScript/JavaScript.php
@@ -1,80 +1,43 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\Grammar\JavaScript;
-use nicoSWD\Rule\Grammar\Definition;
use nicoSWD\Rule\Grammar\Grammar;
-use nicoSWD\Rule\Grammar\InternalFunction;
-use nicoSWD\Rule\Grammar\InternalMethod;
-use nicoSWD\Rule\TokenStream\Token\Token;
+use nicoSWD\Rule\Grammar\InternalCallable;
final class JavaScript extends Grammar
{
- public function getDefinition(): array
- {
- return [
- new Definition(Token::AND, '&&', 145),
- new Definition(Token::OR, '\|\|', 140),
- new Definition(Token::NOT_EQUAL_STRICT, '!==', 135),
- new Definition(Token::NOT_EQUAL, '<>|!=', 130),
- new Definition(Token::EQUAL_STRICT, '===', 125),
- new Definition(Token::EQUAL, '==', 120),
- new Definition(Token::IN, '\bin\b', 115),
- new Definition(Token::NOT_IN, '\bnot\s+in\b', 116),
- new Definition(Token::BOOL_TRUE, '\btrue\b', 110),
- new Definition(Token::BOOL_FALSE, '\bfalse\b', 111),
- new Definition(Token::NULL, '\bnull\b', 105),
- new Definition(Token::METHOD, '\.\s*[a-zA-Z_]\w*\s*\(', 100),
- new Definition(Token::FUNCTION, '[a-zA-Z_]\w*\s*\(', 95),
- new Definition(Token::FLOAT, '-?\d+(?:\.\d+)', 90),
- new Definition(Token::INTEGER, '-?\d+', 85),
- new Definition(Token::ENCAPSED_STRING, '"[^"]*"|\'[^\']*\'', 80),
- new Definition(Token::SMALLER_EQUAL, '<=', 75),
- new Definition(Token::GREATER_EQUAL, '>=', 70),
- new Definition(Token::SMALLER, '<', 65),
- new Definition(Token::GREATER, '>', 60),
- new Definition(Token::OPENING_PARENTHESIS, '\(', 55),
- new Definition(Token::CLOSING_PARENTHESIS, '\)', 50),
- new Definition(Token::OPENING_ARRAY, '\[', 45),
- new Definition(Token::CLOSING_ARRAY, '\]', 40),
- new Definition(Token::COMMA, ',', 35),
- new Definition(Token::REGEX, '/[^/\*].*/[igm]{0,3}', 30),
- new Definition(Token::COMMENT, '//[^\r\n]*|/\*.*?\*/', 25),
- new Definition(Token::NEWLINE, '\r?\n', 20),
- new Definition(Token::SPACE, '\s+', 15),
- new Definition(Token::VARIABLE, '[a-zA-Z_]\w*', 10),
- new Definition(Token::UNKNOWN, '.', 5),
- ];
- }
-
public function getInternalFunctions(): array
{
return [
- new InternalFunction('parseInt', Functions\ParseInt::class),
- new InternalFunction('parseFloat', Functions\ParseFloat::class),
+ new InternalCallable('parseInt', Functions\ParseInt::class),
+ new InternalCallable('parseFloat', Functions\ParseFloat::class),
];
}
public function getInternalMethods(): array
{
return [
- new InternalMethod('charAt', Methods\CharAt::class),
- new InternalMethod('concat', Methods\Concat::class),
- new InternalMethod('indexOf', Methods\IndexOf::class),
- new InternalMethod('join', Methods\Join::class),
- new InternalMethod('replace', Methods\Replace::class),
- new InternalMethod('split', Methods\Split::class),
- new InternalMethod('substr', Methods\Substr::class),
- new InternalMethod('test', Methods\Test::class),
- new InternalMethod('toLowerCase', Methods\ToLowerCase::class),
- new InternalMethod('toUpperCase', Methods\ToUpperCase::class),
- new InternalMethod('startsWith', Methods\StartsWith::class),
- new InternalMethod('endsWith', Methods\EndsWith::class),
+ new InternalCallable('charAt', Methods\CharAt::class),
+ new InternalCallable('concat', Methods\Concat::class),
+ new InternalCallable('indexOf', Methods\IndexOf::class),
+ new InternalCallable('join', Methods\Join::class),
+ new InternalCallable('replace', Methods\Replace::class),
+ new InternalCallable('split', Methods\Split::class),
+ new InternalCallable('substr', Methods\Substr::class),
+ new InternalCallable('test', Methods\Test::class),
+ new InternalCallable('toLowerCase', Methods\ToLowerCase::class),
+ new InternalCallable('toUpperCase', Methods\ToUpperCase::class),
+ new InternalCallable('startsWith', Methods\StartsWith::class),
+ new InternalCallable('endsWith', Methods\EndsWith::class),
];
}
}
diff --git a/src/Grammar/JavaScript/Methods/CharAt.php b/src/Grammar/JavaScript/Methods/CharAt.php
index ee0063b..46f2be9 100644
--- a/src/Grammar/JavaScript/Methods/CharAt.php
+++ b/src/Grammar/JavaScript/Methods/CharAt.php
@@ -1,34 +1,33 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\Grammar\JavaScript\Methods;
use nicoSWD\Rule\Grammar\CallableFunction;
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\TokenInteger;
-use nicoSWD\Rule\TokenStream\Token\TokenString;
+use nicoSWD\Rule\TokenStream\Token\GenericToken;
+use nicoSWD\Rule\TokenStream\Token\TokenKind;
final class CharAt extends CallableFunction
{
- public function call(?BaseToken ...$parameters): BaseToken
+ public function call(mixed ...$parameters): GenericToken
{
- $tokenValue = $this->token->getValue();
$offset = $this->parseParameter($parameters, numParam: 0);
- if (!$offset) {
+ if ($offset === null) {
$offset = 0;
- } elseif (!$offset instanceof TokenInteger) {
- $offset = (int) $offset->getValue();
} else {
- $offset = $offset->getValue();
+ $offset = (int) $offset;
}
- $char = $tokenValue[$offset] ?? '';
+ $char = ($this->token)[$offset] ?? '';
- return new TokenString($char);
+ return new GenericToken(TokenKind::STRING, $char);
}
}
diff --git a/src/Grammar/JavaScript/Methods/Concat.php b/src/Grammar/JavaScript/Methods/Concat.php
index 5d9678f..1508f3d 100644
--- a/src/Grammar/JavaScript/Methods/Concat.php
+++ b/src/Grammar/JavaScript/Methods/Concat.php
@@ -1,31 +1,33 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\Grammar\JavaScript\Methods;
use nicoSWD\Rule\Grammar\CallableFunction;
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\TokenArray;
-use nicoSWD\Rule\TokenStream\Token\TokenString;
+use nicoSWD\Rule\TokenStream\Token\GenericToken;
+use nicoSWD\Rule\TokenStream\Token\TokenKind;
final class Concat extends CallableFunction
{
- public function call(?BaseToken ...$parameters): BaseToken
+ public function call(mixed ...$parameters): GenericToken
{
- $value = $this->token->getValue();
+ $value = $this->token;
foreach ($parameters as $parameter) {
- if ($parameter instanceof TokenArray) {
- $value .= implode(',', $parameter->toArray());
+ if (is_array($parameter)) {
+ $value .= implode(',', $parameter);
} else {
- $value .= $parameter->getValue();
+ $value .= $parameter;
}
}
- return new TokenString($value);
+ return new GenericToken(TokenKind::STRING, $value);
}
}
diff --git a/src/Grammar/JavaScript/Methods/EndsWith.php b/src/Grammar/JavaScript/Methods/EndsWith.php
index eddab8f..de296c2 100644
--- a/src/Grammar/JavaScript/Methods/EndsWith.php
+++ b/src/Grammar/JavaScript/Methods/EndsWith.php
@@ -1,36 +1,33 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\Grammar\JavaScript\Methods;
use nicoSWD\Rule\Grammar\CallableFunction;
use nicoSWD\Rule\Parser\Exception\ParserException;
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
use nicoSWD\Rule\TokenStream\Token\TokenBool;
-use nicoSWD\Rule\TokenStream\Token\TokenString;
final class EndsWith extends CallableFunction
{
- public function call(?BaseToken ...$parameters): BaseToken
+ public function call(mixed ...$parameters): TokenBool
{
- if (!$this->token instanceof TokenString) {
+ if (!is_string($this->token)) {
throw new ParserException('Call to undefined method "endsWith" on non-string');
}
$needle = $this->parseParameter($parameters, numParam: 0);
- $haystack = $this->token->getValue();
- if (!$needle) {
- $result = false;
- } else {
- $needle = $needle->getValue();
- $result = str_ends_with($haystack, $needle);
+ if ($needle === null) {
+ return TokenBool::fromBool(false);
}
- return TokenBool::fromBool($result);
+ return TokenBool::fromBool(str_ends_with($this->token, $needle));
}
}
diff --git a/src/Grammar/JavaScript/Methods/IndexOf.php b/src/Grammar/JavaScript/Methods/IndexOf.php
index 695e20d..dc531bb 100644
--- a/src/Grammar/JavaScript/Methods/IndexOf.php
+++ b/src/Grammar/JavaScript/Methods/IndexOf.php
@@ -1,32 +1,35 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\Grammar\JavaScript\Methods;
use nicoSWD\Rule\Grammar\CallableFunction;
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\TokenInteger;
+use nicoSWD\Rule\TokenStream\Token\GenericToken;
+use nicoSWD\Rule\TokenStream\Token\TokenKind;
final class IndexOf extends CallableFunction
{
- public function call(?BaseToken ...$parameters): BaseToken
+ public function call(mixed ...$parameters): GenericToken
{
$needle = $this->parseParameter($parameters, numParam: 0);
- if (!$needle) {
+ if ($needle === null) {
$value = -1;
} else {
- $value = strpos($this->token->getValue(), $needle->getValue());
+ $value = strpos($this->token, $needle);
if ($value === false) {
$value = -1;
}
}
- return new TokenInteger($value);
+ return new GenericToken(TokenKind::INTEGER, $value);
}
}
diff --git a/src/Grammar/JavaScript/Methods/Join.php b/src/Grammar/JavaScript/Methods/Join.php
index 348c9ad..6bbfadf 100644
--- a/src/Grammar/JavaScript/Methods/Join.php
+++ b/src/Grammar/JavaScript/Methods/Join.php
@@ -1,41 +1,36 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\Grammar\JavaScript\Methods;
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\TokenArray;
-use nicoSWD\Rule\TokenStream\Token\TokenString;
-use nicoSWD\Rule\TokenStream\TokenCollection;
+use nicoSWD\Rule\TokenStream\Token\GenericToken;
+use nicoSWD\Rule\TokenStream\Token\TokenKind;
use nicoSWD\Rule\Parser\Exception\ParserException;
use nicoSWD\Rule\Grammar\CallableFunction;
final class Join extends CallableFunction
{
- public function call(?BaseToken ...$parameters): BaseToken
+ public function call(mixed ...$parameters): GenericToken
{
- if (!$this->token instanceof TokenArray) {
- throw new ParserException(sprintf('%s.join is not a function', $this->token->getValue()));
+ if (!is_array($this->token)) {
+ throw new ParserException(sprintf('%s.join is not a function', $this->token));
}
$glue = $this->parseParameter($parameters, numParam: 0);
- if ($glue) {
- $glue = $glue->getValue();
+ if ($glue !== null) {
+ $glue = (string) $glue;
} else {
$glue = ',';
}
- $array = $this->token->getValue();
-
- if ($array instanceof TokenCollection) {
- $array = $array->toArray();
- }
-
- return new TokenString(implode($glue, $array));
+ return new GenericToken(TokenKind::STRING, implode($glue, $this->token));
}
}
diff --git a/src/Grammar/JavaScript/Methods/Replace.php b/src/Grammar/JavaScript/Methods/Replace.php
index 209aea5..4d282c9 100644
--- a/src/Grammar/JavaScript/Methods/Replace.php
+++ b/src/Grammar/JavaScript/Methods/Replace.php
@@ -1,46 +1,50 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\Grammar\JavaScript\Methods;
use nicoSWD\Rule\Grammar\CallableFunction;
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\TokenRegex;
-use nicoSWD\Rule\TokenStream\Token\TokenString;
+use nicoSWD\Rule\TokenStream\Token\GenericToken;
+use nicoSWD\Rule\TokenStream\Token\TokenKind;
final class Replace extends CallableFunction
{
- public function call(?BaseToken ...$parameters): BaseToken
+ public function call(mixed ...$parameters): GenericToken
{
- $isRegExpr = false;
$search = $this->parseParameter($parameters, numParam: 0);
- if (!$search) {
+ if ($search === null || $search === '') {
$search = '';
+ $isRegExpr = false;
} else {
- $isRegExpr = ($search instanceof TokenRegex);
- $search = $search->getValue();
+ $isRegExpr = $this->isRegex($search);
}
$replace = $this->parseParameter($parameters, numParam: 1);
- if (!$replace) {
+ if ($replace === null) {
$replace = 'undefined';
- } else {
- $replace = $replace->getValue();
}
if ($isRegExpr) {
$value = $this->doRegexReplace($search, $replace);
} else {
- $value = str_replace($search, $replace, $this->token->getValue());
+ $value = str_replace($search, $replace, $this->token);
}
- return new TokenString($value);
+ return new GenericToken(TokenKind::STRING, $value);
+ }
+
+ private function isRegex(string $value): bool
+ {
+ return (bool) preg_match('~^/.+/[img]{0,3}$~', $value);
}
private function doRegexReplace(string $search, string $replace): string
@@ -53,7 +57,7 @@ private function doRegexReplace(string $search, string $replace): string
return preg_replace(
$expression . $modifiers,
$replace,
- $this->token->getValue(),
+ $this->token,
$limit
);
}
diff --git a/src/Grammar/JavaScript/Methods/Split.php b/src/Grammar/JavaScript/Methods/Split.php
index c255049..165b9a5 100644
--- a/src/Grammar/JavaScript/Methods/Split.php
+++ b/src/Grammar/JavaScript/Methods/Split.php
@@ -1,34 +1,36 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\Grammar\JavaScript\Methods;
use nicoSWD\Rule\Grammar\CallableFunction;
use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\TokenArray;
-use nicoSWD\Rule\TokenStream\Token\TokenRegex;
+use nicoSWD\Rule\TokenStream\Token\TokenFactory;
final class Split extends CallableFunction
{
- public function call(?BaseToken ...$parameters): BaseToken
+ public function call(mixed ...$parameters): BaseToken
{
$separator = $this->parseParameter($parameters, numParam: 0);
- if (!$separator || !is_string($separator->getValue())) {
- $newValue = [$this->token->getValue()];
+ if (!is_string($separator)) {
+ $newValue = [$this->token];
} else {
- $params = [$separator->getValue(), $this->token->getValue()];
+ $params = [$separator, $this->token];
$limit = $this->parseParameter($parameters, numParam: 1);
if ($limit !== null) {
- $params[] = (int) $limit->getValue();
+ $params[] = (int) $limit;
}
- if ($separator instanceof TokenRegex) {
+ if ($this->isRegex($separator)) {
$func = 'preg_split';
} else {
$func = 'explode';
@@ -37,6 +39,11 @@ public function call(?BaseToken ...$parameters): BaseToken
$newValue = $func(...$params);
}
- return new TokenArray($newValue);
+ return new TokenFactory()->createFromPHPType($newValue);
+ }
+
+ private function isRegex(string $value): bool
+ {
+ return (bool) preg_match('~^/.+/[img]{0,3}$~', $value);
}
}
diff --git a/src/Grammar/JavaScript/Methods/StartsWith.php b/src/Grammar/JavaScript/Methods/StartsWith.php
index de47339..8b2ad95 100644
--- a/src/Grammar/JavaScript/Methods/StartsWith.php
+++ b/src/Grammar/JavaScript/Methods/StartsWith.php
@@ -1,42 +1,40 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\Grammar\JavaScript\Methods;
use nicoSWD\Rule\Grammar\CallableFunction;
use nicoSWD\Rule\Parser\Exception\ParserException;
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
use nicoSWD\Rule\TokenStream\Token\TokenBool;
-use nicoSWD\Rule\TokenStream\Token\TokenInteger;
-use nicoSWD\Rule\TokenStream\Token\TokenString;
final class StartsWith extends CallableFunction
{
- public function call(?BaseToken ...$parameters): BaseToken
+ public function call(mixed ...$parameters): TokenBool
{
- if (!$this->token instanceof TokenString) {
+ if (!is_string($this->token)) {
throw new ParserException('Call to undefined method "startsWith" on non-string');
}
$needle = $this->parseParameter($parameters, numParam: 0);
$offset = $this->getOffset($this->parseParameter($parameters, numParam: 1));
- $position = strpos($this->token->getValue(), $needle->getValue(), $offset);
+ $position = strpos($this->token, $needle, $offset);
return TokenBool::fromBool($position === $offset);
}
- private function getOffset(?BaseToken $offset): int
+ private function getOffset(mixed $offset): int
{
- if ($offset instanceof TokenInteger) {
- $offset = $offset->getValue();
- } else {
- $offset = 0;
+ if ($offset !== null) {
+ return (int) $offset;
}
- return $offset;
+ return 0;
}
}
diff --git a/src/Grammar/JavaScript/Methods/Substr.php b/src/Grammar/JavaScript/Methods/Substr.php
index 2a4fe73..cdc8eea 100644
--- a/src/Grammar/JavaScript/Methods/Substr.php
+++ b/src/Grammar/JavaScript/Methods/Substr.php
@@ -1,31 +1,34 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\Grammar\JavaScript\Methods;
use nicoSWD\Rule\Grammar\CallableFunction;
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\TokenString;
+use nicoSWD\Rule\TokenStream\Token\GenericToken;
+use nicoSWD\Rule\TokenStream\Token\TokenKind;
final class Substr extends CallableFunction
{
- public function call(?BaseToken ...$parameters): BaseToken
+ public function call(mixed ...$parameters): GenericToken
{
$start = $this->parseParameter($parameters, numParam: 0);
- $params = [(int) $start?->getValue()];
+ $params = [(int) $start];
$offset = $this->parseParameter($parameters, numParam: 1);
- if ($offset) {
- $params[] = (int) $offset->getValue();
+ if ($offset !== null) {
+ $params[] = (int) $offset;
}
- $value = substr($this->token->getValue(), ...$params);
+ $value = substr($this->token, ...$params);
- return new TokenString($value);
+ return new GenericToken(TokenKind::STRING, $value);
}
}
diff --git a/src/Grammar/JavaScript/Methods/Test.php b/src/Grammar/JavaScript/Methods/Test.php
index 52bcf0b..88d66ab 100644
--- a/src/Grammar/JavaScript/Methods/Test.php
+++ b/src/Grammar/JavaScript/Methods/Test.php
@@ -1,49 +1,71 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\Grammar\JavaScript\Methods;
use nicoSWD\Rule\TokenStream\Token\BaseToken;
use nicoSWD\Rule\TokenStream\Token\TokenBool;
-use nicoSWD\Rule\TokenStream\Token\TokenRegex;
-use nicoSWD\Rule\TokenStream\TokenCollection;
+use nicoSWD\Rule\TokenStream\Token\TokenKind;
use nicoSWD\Rule\Parser\Exception\ParserException;
use nicoSWD\Rule\Grammar\CallableFunction;
final class Test extends CallableFunction
{
- public function call(?BaseToken ...$parameters): BaseToken
+ public function call(mixed ...$parameters): TokenBool
{
- if (!$this->token instanceof TokenRegex) {
+ $pattern = $this->getPattern();
+
+ if ($pattern === null) {
throw new ParserException('test() is not a function');
}
- $string = $this->parseParameter($parameters, numParam: 0);
+ $subject = $this->parseParameter($parameters, numParam: 0);
- if (!$string) {
- $bool = false;
- } else {
- // Remove "g" modifier as it does not exist in PHP
- // It's also irrelevant in .test() but allowed in JS here
- $pattern = preg_replace_callback(
- '~/[igm]{0,3}$~',
- static fn (array $modifiers): string => str_replace('g', '', $modifiers[0]),
- $this->token->getValue()
- );
-
- $subject = $string->getValue();
-
- while ($subject instanceof TokenCollection) {
- $subject = current($subject->toArray());
- }
+ if ($subject === null) {
+ return TokenBool::fromBool(false);
+ }
- $bool = (bool) preg_match($pattern, (string) $subject);
+ while (is_array($subject)) {
+ $subject = current($subject);
}
+
+ $bool = (bool) preg_match($pattern, (string) $subject);
+
return TokenBool::fromBool($bool);
}
+
+ private function getPattern(): ?string
+ {
+ // When called from RegexNode, $this->token is a BaseToken
+ if ($this->token instanceof BaseToken) {
+ if (!$this->token->isOfKind(TokenKind::REGEX)) {
+ return null;
+ }
+ $pattern = $this->token->getValue();
+ } elseif (!is_string($this->token)) {
+ return null;
+ } else {
+ $pattern = $this->token;
+ }
+
+ if (!preg_match('~^/.+/[img]{0,3}$~', $pattern)) {
+ return null;
+ }
+
+ // Remove "g" modifier as it does not exist in PHP
+ // It's also irrelevant in .test() but allowed in JS here
+ return preg_replace_callback(
+ '~/[igm]{0,3}$~',
+ static fn (array $modifiers): string => str_replace('g', '', $modifiers[0]),
+ $pattern
+ );
+ }
}
diff --git a/src/Grammar/JavaScript/Methods/ToLowerCase.php b/src/Grammar/JavaScript/Methods/ToLowerCase.php
index 8b61f71..d4289be 100644
--- a/src/Grammar/JavaScript/Methods/ToLowerCase.php
+++ b/src/Grammar/JavaScript/Methods/ToLowerCase.php
@@ -1,20 +1,23 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\Grammar\JavaScript\Methods;
use nicoSWD\Rule\Grammar\CallableFunction;
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\TokenString;
+use nicoSWD\Rule\TokenStream\Token\GenericToken;
+use nicoSWD\Rule\TokenStream\Token\TokenKind;
final class ToLowerCase extends CallableFunction
{
- public function call(?BaseToken ...$parameters): BaseToken
+ public function call(mixed ...$parameters): GenericToken
{
- return new TokenString(strtolower((string) $this->token->getValue()));
+ return new GenericToken(TokenKind::STRING, strtolower((string) $this->token));
}
}
diff --git a/src/Grammar/JavaScript/Methods/ToUpperCase.php b/src/Grammar/JavaScript/Methods/ToUpperCase.php
index fba4b76..187eb3a 100644
--- a/src/Grammar/JavaScript/Methods/ToUpperCase.php
+++ b/src/Grammar/JavaScript/Methods/ToUpperCase.php
@@ -1,20 +1,23 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\Grammar\JavaScript\Methods;
use nicoSWD\Rule\Grammar\CallableFunction;
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\TokenString;
+use nicoSWD\Rule\TokenStream\Token\GenericToken;
+use nicoSWD\Rule\TokenStream\Token\TokenKind;
final class ToUpperCase extends CallableFunction
{
- public function call(?BaseToken ...$parameters): BaseToken
+ public function call(mixed ...$parameters): GenericToken
{
- return new TokenString(strtoupper((string) $this->token->getValue()));
+ return new GenericToken(TokenKind::STRING, strtoupper((string) $this->token));
}
}
diff --git a/src/Highlighter/DefaultStyles.php b/src/Highlighter/DefaultStyles.php
new file mode 100644
index 0000000..51aa7c0
--- /dev/null
+++ b/src/Highlighter/DefaultStyles.php
@@ -0,0 +1,41 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\Highlighter;
+
+use nicoSWD\Rule\TokenStream\Token\TokenType;
+
+/**
+ * Provides sensible default CSS styles for syntax highlighting.
+ *
+ * These styles are inspired by VS Code's Dark+ theme and provide
+ * a good out-of-the-box experience for highlighting rule expressions.
+ */
+final class DefaultStyles
+{
+ /** @return array */
+ public static function getStyles(): array
+ {
+ return [
+ TokenType::OPERATOR->name => 'color: #D4D4D4;',
+ TokenType::VALUE->name => 'color: #CE9178;',
+ TokenType::LOGICAL->name => 'color: #569CD6;',
+ TokenType::VARIABLE->name => 'color: #9CDCFE;',
+ TokenType::COMMENT->name => 'color: #6A9955; font-style: italic;',
+ TokenType::SPACE->name => '',
+ TokenType::UNKNOWN->name => 'color: #FF0000;',
+ TokenType::PARENTHESIS->name => 'color: #DCDCA;',
+ TokenType::SQUARE_BRACKET->name => 'color: #DCDCA;',
+ TokenType::COMMA->name => 'color: #D4D4D4;',
+ TokenType::METHOD->name => 'color: #DCDCAA;',
+ TokenType::FUNCTION->name => 'color: #DCDCAA;',
+ ];
+ }
+}
diff --git a/src/Highlighter/Highlighter.php b/src/Highlighter/Highlighter.php
index b90d28f..9b42b9e 100644
--- a/src/Highlighter/Highlighter.php
+++ b/src/Highlighter/Highlighter.php
@@ -1,72 +1,104 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\Highlighter;
-use Iterator;
-use nicoSWD\Rule\Tokenizer\TokenizerInterface;
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
+use nicoSWD\Rule\Grammar\Grammar;
+use nicoSWD\Rule\Grammar\JavaScript\JavaScript;
+use nicoSWD\Rule\Lexer\DefaultLexer;
+use nicoSWD\Rule\TokenStream\Token\TokenFactory;
use nicoSWD\Rule\TokenStream\Token\TokenType;
-use SplObjectStorage;
+/**
+ * Syntax highlighter for rule expressions.
+ *
+ * Takes a rule string and returns HTML with syntax-highlighted tokens,
+ * where each token type is wrapped in a with configurable CSS styles.
+ *
+ * Usage:
+ * ```php
+ * $highlighter = new Highlighter();
+ *
+ * // Use default styles
+ * echo $highlighter->highlightString('2 < 3 && foo in [4, 6, 7]');
+ *
+ * // Or customize styles for specific token types
+ * $highlighter->setStyle(TokenType::VARIABLE, 'color: #007694; font-weight: 900;');
+ * echo $highlighter->highlightString($ruleStr);
+ * ```
+ */
final class Highlighter
{
- private SplObjectStorage $styles;
+ /** @var array */
+ private array $styles = [];
public function __construct(
- private readonly TokenizerInterface $tokenizer,
+ private readonly ?Grammar $grammar = null,
) {
- $this->styles = $this->defaultStyles();
}
- public function setStyle(TokenType $group, string $style): void
+ /**
+ * Set the CSS style for a specific token type.
+ *
+ * If not called, default styles from DefaultStyles will be used.
+ */
+ public function setStyle(TokenType $type, string $css): self
{
- $this->styles[$group] = $style;
- }
+ $this->styles[$type->name] = $css;
- public function highlightString(string $string): string
- {
- return $this->highlightTokens($this->tokenizer->tokenize($string));
+ return $this;
}
- public function highlightTokens(Iterator $tokens): string
+ /**
+ * Highlight a rule string and return HTML with syntax highlighting.
+ *
+ * @param string $rule The rule expression to highlight
+ * @return string HTML with tags wrapping each token
+ */
+ public function highlightString(string $rule): string
{
- $string = '';
+ $styles = $this->getResolvedStyles();
+ $grammar = $this->grammar ?? new JavaScript();
+ $tokenFactory = new TokenFactory();
+ $lexer = new DefaultLexer($grammar, $tokenFactory);
+ $tokens = $lexer->tokenize($rule);
+ $output = '';
foreach ($tokens as $token) {
- /** @var BaseToken $token */
- $tokenType = $token->getType();
+ $type = $token->getType();
+ $value = $token->getOriginalValue();
+ $css = $styles[$type->name] ?? '';
- if (isset($this->styles[$tokenType])) {
- $string .= '' . $this->encode($token) . '';
+ if ($css === '') {
+ $output .= htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
} else {
- $string .= $token->getOriginalValue();
+ $output .= sprintf(
+ '%s',
+ $css,
+ htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
+ );
}
}
- return '' . $string . '
';
+ return $output;
}
- private function encode(BaseToken $token): string
+ /**
+ * @return array
+ */
+ private function getResolvedStyles(): array
{
- return htmlentities($token->getOriginalValue(), ENT_QUOTES, 'utf-8');
- }
-
- private function defaultStyles(): SplObjectStorage
- {
- $styles = new SplObjectStorage();
- $styles[TokenType::COMMENT] = 'color: #948a8a; font-style: italic;';
- $styles[TokenType::LOGICAL] = 'color: #c72d2d;';
- $styles[TokenType::OPERATOR] = 'color: #000;';
- $styles[TokenType::PARENTHESIS] = 'color: #000;';
- $styles[TokenType::VALUE] = 'color: #e36700; font-style: italic;';
- $styles[TokenType::VARIABLE] = 'color: #007694; font-weight: 900;';
- $styles[TokenType::METHOD] = 'color: #000';
+ if ($this->styles !== []) {
+ return $this->styles;
+ }
- return $styles;
+ return DefaultStyles::getStyles();
}
}
diff --git a/src/Lexer/DefaultLexer.php b/src/Lexer/DefaultLexer.php
new file mode 100644
index 0000000..c1b9f48
--- /dev/null
+++ b/src/Lexer/DefaultLexer.php
@@ -0,0 +1,404 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\Lexer;
+
+use Iterator;
+use nicoSWD\Rule\Grammar\Grammar;
+use nicoSWD\Rule\Parser\Exception\ParserException;
+use nicoSWD\Rule\TokenStream\Token\BaseToken;
+use nicoSWD\Rule\TokenStream\Token\TokenFactory;
+use nicoSWD\Rule\TokenStream\Token\TokenKind;
+
+/**
+ * A character-by-character lexer that replaces the regex-based Tokenizer.
+ *
+ * This approach provides:
+ * - Linear O(n) performance with no regex backtracking
+ * - Better error messages with precise character positions
+ * - Context-sensitive lexing (e.g., distinguishing /regex/ from comments)
+ * - Easier to extend with new syntax
+ *
+ * The Lexer is stateless and reentrant: all mutable scanning state is held
+ * in a LexerContext object that is local to each tokenize() call.
+ */
+final class DefaultLexer extends Lexer
+{
+ public function __construct(
+ public Grammar $grammar,
+ private readonly TokenFactory $tokenFactory,
+ ) {
+ }
+
+ public function tokenize(string $string): Iterator
+ {
+ $ctx = new LexerContext($string);
+ $lastValueToken = null;
+
+ while ($ctx->isValid()) {
+ $ch = $ctx->current();
+
+ $token = match (true) {
+ $ch === '&' && $ctx->peek() === '&' => $this->emitOperator($ctx, TokenKind::AND, '&&', 2),
+ $ch === '|' && $ctx->peek() === '|' => $this->emitOperator($ctx, TokenKind::OR, '||', 2),
+ $ch === '!' && $ctx->peek() === '=' && $ctx->peek(1) === '=' => $this->emitOperator($ctx, TokenKind::NOT_EQUAL_STRICT, '!==', 3),
+ $ch === '!' && $ctx->peek() === '=' => $this->emitOperator($ctx, TokenKind::NOT_EQUAL, '!=', 2),
+ $ch === '!' => $this->emitOperator($ctx, TokenKind::NOT, '!', 1),
+ $ch === '=' && $ctx->peek() === '=' && $ctx->peek(1) === '=' => $this->emitOperator($ctx, TokenKind::EQUAL_STRICT, '===', 3),
+ $ch === '=' && $ctx->peek() === '=' => $this->emitOperator($ctx, TokenKind::EQUAL, '==', 2),
+ $ch === '<' && $ctx->peek() === '=' => $this->emitOperator($ctx, TokenKind::LESS_THAN_EQUAL, '<=', 2),
+ $ch === '>' && $ctx->peek() === '=' => $this->emitOperator($ctx, TokenKind::GREATER_EQUAL, '>=', 2),
+ $ch === '+' => $this->emitOperator($ctx, TokenKind::PLUS, '+', 1),
+ $ch === '-' && $this->isTokenValue($lastValueToken) => $this->emitOperator($ctx, TokenKind::MINUS, '-', 1),
+ $ch === '-' && $this->isDigit($ctx->peek()) => $this->readNumber($ctx),
+ $ch === '-' => $this->emitOperator($ctx, TokenKind::MINUS, '-', 1),
+ $ch === '*' => $this->emitOperator($ctx, TokenKind::MULTIPLY, '*', 1),
+ $ch === '/' && ($ctx->peek() === '/' || $ctx->peek() === '*') => $this->readSlash($ctx),
+ $ch === '/' && $this->isTokenValue($lastValueToken) => $this->emitOperator($ctx, TokenKind::DIVIDE, '/', 1),
+ $ch === '/' => $this->readSlash($ctx),
+ $ch === '%' => $this->emitOperator($ctx, TokenKind::MODULO, '%', 1),
+ $ch === '<' && $ctx->peek() === '>' => $this->emitOperator($ctx, TokenKind::NOT_EQUAL, '<>', 2),
+ $ch === '<' => $this->emitOperator($ctx, TokenKind::LESS_THAN, '<', 1),
+ $ch === '>' => $this->emitOperator($ctx, TokenKind::GREATER, '>', 1),
+ $ch === '(' => $this->emitSimple($ctx, TokenKind::OPENING_PARENTHESIS, '('),
+ $ch === ')' => $this->emitSimple($ctx, TokenKind::CLOSING_PARENTHESIS, ')'),
+ $ch === '[' => $this->emitSimple($ctx, TokenKind::OPENING_ARRAY, '['),
+ $ch === ']' => $this->emitSimple($ctx, TokenKind::CLOSING_ARRAY, ']'),
+ $ch === ',' => $this->emitSimple($ctx, TokenKind::COMMA, ','),
+ $ch === '.' => $this->readMethod($ctx),
+ $ch === '"' || $ch === "'" => $this->readString($ctx),
+ $this->isDigit($ch) => $this->readNumber($ctx),
+ $ch === ' ' || $ch === "\t" => $this->readWhitespace($ctx),
+ $ch === "\r" || $ch === "\n" => $this->readNewlineToken($ctx),
+ $this->isAlpha($ch) || $ch === '_' => $this->readIdentifier($ctx),
+ default => $this->emitSimple($ctx, TokenKind::UNKNOWN, $ch),
+ };
+
+ // Track the last non-ignorable token for context-sensitive lexing
+ if (!$token->canBeIgnored()) {
+ $lastValueToken = $token;
+ }
+
+ yield $token;
+ }
+ }
+
+ /**
+ * Determine if the given token represents a "value" — meaning the next
+ * operator-like character should be treated as a binary operator rather
+ * than a unary prefix.
+ */
+ private function isTokenValue(?BaseToken $token): bool
+ {
+ if ($token === null) {
+ return false;
+ }
+
+ // A token is a "value" if it's a value-type token (string, number, bool, etc.),
+ // or if it's a closing parenthesis/array (which represent
+ // the end of a sub-expression that evaluates to a value).
+ return match ($token->getKind()) {
+ TokenKind::STRING,
+ TokenKind::INTEGER,
+ TokenKind::FLOAT,
+ TokenKind::BOOL_TRUE,
+ TokenKind::BOOL_FALSE,
+ TokenKind::NULL,
+ TokenKind::REGEX,
+ TokenKind::VARIABLE,
+ TokenKind::ENCAPSED_STRING,
+ TokenKind::OBJECT,
+ TokenKind::ARRAY,
+ TokenKind::CLOSING_PARENTHESIS,
+ TokenKind::CLOSING_ARRAY => true,
+ default => false,
+ };
+ }
+
+ private function emitSimple(LexerContext $ctx, TokenKind $token, string $value): BaseToken
+ {
+ $offset = $ctx->pos;
+ $ctx->pos += strlen($value);
+
+ return $this->tokenFactory->createFromToken($token, [$token->value => $value], $offset);
+ }
+
+ private function emitOperator(LexerContext $ctx, TokenKind $token, string $value, int $length): BaseToken
+ {
+ $offset = $ctx->pos;
+ $ctx->pos += $length;
+
+ return $this->tokenFactory->createFromToken($token, [$token->value => $value], $offset);
+ }
+
+ private function readMethod(LexerContext $ctx): BaseToken
+ {
+ $offset = $ctx->pos;
+ $ctx->pos++; // skip '.'
+
+ // Skip all whitespace (including newlines) between dot and method name
+ $this->skipAllWhitespace($ctx);
+
+ // Read method name
+ $name = '';
+ while ($ctx->isValid() && ($this->isAlpha($ctx->current()) || $ctx->current() === '_' || $this->isDigit($ctx->current()))) {
+ $name .= $ctx->current();
+ $ctx->pos++;
+ }
+
+ return $this->tokenFactory->createFromToken(TokenKind::METHOD, [TokenKind::METHOD->value => $name], $offset);
+ }
+
+ private function readString(LexerContext $ctx): BaseToken
+ {
+ $offset = $ctx->pos;
+ $quote = $ctx->current();
+ $ctx->pos++; // skip opening quote
+
+ $value = $quote . $this->readDelimitedContent($ctx, $quote);
+
+ return $this->tokenFactory->createFromToken(TokenKind::ENCAPSED_STRING, [TokenKind::ENCAPSED_STRING->value => $value], $offset);
+ }
+
+ /**
+ * Read content delimited by a character, handling backslash escapes.
+ *
+ * The opening delimiter must already be consumed before calling this method.
+ *
+ * @param LexerContext $ctx The lexer context.
+ * @param string $delimiter The closing delimiter character to look for.
+ * @param bool $disallowNewlines If true, newlines will stop reading (for regex).
+ * @return string The content between (but not including) the delimiters.
+ */
+ private function readDelimitedContent(LexerContext $ctx, string $delimiter, bool $disallowNewlines = false): string
+ {
+ $value = '';
+
+ while ($ctx->isValid()) {
+ $ch = $ctx->current();
+
+ if ($ch === '\\') {
+ $value .= $ch;
+ $ctx->pos++;
+ if ($ctx->isValid()) {
+ $value .= $ctx->current();
+ $ctx->pos++;
+ }
+ continue;
+ }
+
+ if ($ch === $delimiter) {
+ $value .= $ch;
+ $ctx->pos++;
+ break;
+ }
+
+ if ($disallowNewlines && ($ch === "\r" || $ch === "\n")) {
+ break;
+ }
+
+ $value .= $ch;
+ $ctx->pos++;
+ }
+
+ return $value;
+ }
+
+ private function readSlash(LexerContext $ctx): BaseToken
+ {
+ $offset = $ctx->pos;
+
+ // Single-line comment
+ if ($ctx->peek() === '/') {
+ $value = '//';
+ $ctx->pos += 2;
+
+ while ($ctx->isValid() && $ctx->current() !== "\r" && $ctx->current() !== "\n") {
+ $value .= $ctx->current();
+ $ctx->pos++;
+ }
+
+ return $this->tokenFactory->createFromToken(TokenKind::COMMENT, [TokenKind::COMMENT->value => $value], $offset);
+ }
+
+ // Multi-line comment
+ if ($ctx->peek() === '*') {
+ $value = '/*';
+ $ctx->pos += 2;
+
+ while ($ctx->isValid()) {
+ if ($ctx->current() === '*' && $ctx->peek() === '/') {
+ $value .= '*/';
+ $ctx->pos += 2;
+ break;
+ }
+ $value .= $ctx->current();
+ $ctx->pos++;
+ }
+
+ return $this->tokenFactory->createFromToken(TokenKind::COMMENT, [TokenKind::COMMENT->value => $value], $offset);
+ }
+
+ // Regex literal
+ $ctx->pos++; // skip opening '/'
+ $value = '/' . $this->readDelimitedContent($ctx, '/', disallowNewlines: true);
+
+ // Read optional flags
+ $seenFlags = [];
+ while ($ctx->isValid() && str_contains('igm', $ctx->current())) {
+ $flag = $ctx->current();
+
+ if (isset($seenFlags[$flag])) {
+ throw ParserException::duplicateRegexModifier($flag, $offset);
+ }
+
+ $seenFlags[$flag] = true;
+ $value .= $flag;
+ $ctx->pos++;
+ }
+
+ return $this->tokenFactory->createFromToken(TokenKind::REGEX, [TokenKind::REGEX->value => $value], $offset);
+ }
+
+ private function readNumber(LexerContext $ctx): BaseToken
+ {
+ $offset = $ctx->pos;
+ $value = '';
+
+ // Optional leading minus
+ if ($ctx->current() === '-') {
+ $value .= '-';
+ $ctx->pos++;
+ }
+
+ // Integer part
+ while ($ctx->isValid() && $this->isDigit($ctx->current())) {
+ $value .= $ctx->current();
+ $ctx->pos++;
+ }
+
+ // Float part
+ if ($ctx->isValid() && $ctx->current() === '.' && $this->isDigit($ctx->peek())) {
+ $value .= '.';
+ $ctx->pos++;
+
+ while ($ctx->isValid() && $this->isDigit($ctx->current())) {
+ $value .= $ctx->current();
+ $ctx->pos++;
+ }
+
+ return $this->tokenFactory->createFromToken(TokenKind::FLOAT, [TokenKind::FLOAT->value => $value], $offset);
+ }
+
+ return $this->tokenFactory->createFromToken(TokenKind::INTEGER, [TokenKind::INTEGER->value => $value], $offset);
+ }
+
+ private function readWhitespace(LexerContext $ctx): BaseToken
+ {
+ $offset = $ctx->pos;
+ $value = '';
+
+ while ($ctx->isValid() && ($ctx->current() === ' ' || $ctx->current() === "\t")) {
+ $value .= $ctx->current();
+ $ctx->pos++;
+ }
+
+ return $this->tokenFactory->createFromToken(TokenKind::SPACE, [TokenKind::SPACE->value => $value], $offset);
+ }
+
+ private function readNewlineToken(LexerContext $ctx): BaseToken
+ {
+ $offset = $ctx->pos;
+ $ch = $ctx->current();
+ $ctx->pos++;
+
+ // Match \r optionally followed by \n (like old regex: \r?\n)
+ if ($ch === "\r" && $ctx->isValid() && $ctx->current() === "\n") {
+ $ctx->pos++;
+
+ return $this->tokenFactory->createFromToken(TokenKind::NEWLINE, [TokenKind::NEWLINE->value => "\r\n"], $offset);
+ }
+
+ return $this->tokenFactory->createFromToken(TokenKind::NEWLINE, [TokenKind::NEWLINE->value => $ch], $offset);
+ }
+
+ private function readIdentifier(LexerContext $ctx): BaseToken
+ {
+ $offset = $ctx->pos;
+ $name = '';
+
+ while ($ctx->isValid() && ($this->isAlpha($ctx->current()) || $ctx->current() === '_' || $this->isDigit($ctx->current()))) {
+ $name .= $ctx->current();
+ $ctx->pos++;
+ }
+
+ // Check for "not in" keyword (must be checked before other keywords)
+ if ($name === 'not') {
+ $savedPos = $ctx->pos;
+ $this->skipAllWhitespace($ctx);
+
+ if ($ctx->startsWith('in')) {
+ $ctx->pos += 2; // skip 'in'
+ return $this->tokenFactory->createFromToken(TokenKind::NOT_IN, [TokenKind::NOT_IN->value => 'not in'], $offset);
+ }
+
+ $ctx->pos = $savedPos;
+ }
+
+ $token = match ($name) {
+ 'true' => TokenKind::BOOL_TRUE,
+ 'false' => TokenKind::BOOL_FALSE,
+ 'null' => TokenKind::NULL,
+ 'in' => TokenKind::IN,
+ default => null,
+ };
+
+ if ($token !== null) {
+ return $this->tokenFactory->createFromToken($token, [$token->value => $name], $offset);
+ }
+
+ // Check if it's a function call (identifier followed by optional whitespace and '(')
+ $savedPos = $ctx->pos;
+ $this->skipWhitespace($ctx);
+
+ if ($ctx->isValid() && $ctx->current() === '(') {
+ return $this->tokenFactory->createFromToken(TokenKind::FUNCTION, [TokenKind::FUNCTION->value => $name], $offset);
+ }
+
+ $ctx->pos = $savedPos;
+
+ // It's a variable
+ return $this->tokenFactory->createFromToken(TokenKind::VARIABLE, [TokenKind::VARIABLE->value => $name], $offset);
+ }
+
+ private function skipWhitespace(LexerContext $ctx): void
+ {
+ while ($ctx->isValid() && ($ctx->current() === ' ' || $ctx->current() === "\t")) {
+ $ctx->pos++;
+ }
+ }
+
+ private function skipAllWhitespace(LexerContext $ctx): void
+ {
+ while ($ctx->isValid() && ($ctx->current() === ' ' || $ctx->current() === "\t" || $ctx->current() === "\r" || $ctx->current() === "\n")) {
+ $ctx->pos++;
+ }
+ }
+
+ private function isAlpha(string $ch): bool
+ {
+ return ctype_alpha($ch);
+ }
+
+ private function isDigit(string $ch): bool
+ {
+ return ctype_digit($ch);
+ }
+}
diff --git a/src/Tokenizer/TokenizerInterface.php b/src/Lexer/Lexer.php
similarity index 76%
rename from src/Tokenizer/TokenizerInterface.php
rename to src/Lexer/Lexer.php
index c53f5da..f76a0d2 100644
--- a/src/Tokenizer/TokenizerInterface.php
+++ b/src/Lexer/Lexer.php
@@ -1,19 +1,22 @@
-
*/
-namespace nicoSWD\Rule\Tokenizer;
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\Lexer;
use Iterator;
use nicoSWD\Rule\Grammar\Grammar;
use nicoSWD\Rule\TokenStream\Token\BaseToken;
-abstract class TokenizerInterface
+abstract class Lexer
{
- public Grammar $grammar;
+ protected Grammar $grammar;
/**
* @param string $string
diff --git a/src/Lexer/LexerContext.php b/src/Lexer/LexerContext.php
new file mode 100644
index 0000000..011bb5c
--- /dev/null
+++ b/src/Lexer/LexerContext.php
@@ -0,0 +1,71 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\Lexer;
+
+/**
+ * Internal value object that holds the mutable scanning state for the Lexer.
+ *
+ * By encapsulating the position, input string, and length in a separate object,
+ * the Lexer itself remains stateless and reentrant — multiple calls to tokenize()
+ * on the same Lexer instance will not interfere with each other.
+ *
+ * @internal
+ */
+final class LexerContext
+{
+ public function __construct(
+ public string $input,
+ public int $pos = 0,
+ public int $length = 0,
+ ) {
+ $this->length = strlen($input);
+ }
+
+ /**
+ * Peek at a character ahead of the current position without advancing.
+ */
+ public function peek(int $offset = 0): string
+ {
+ $index = $this->pos + $offset + 1;
+
+ return $index < $this->length ? $this->input[$index] : '';
+ }
+
+ /**
+ * Get the current character.
+ */
+ public function current(): string
+ {
+ return $this->input[$this->pos] ?? '';
+ }
+
+ /**
+ * Check if the current position is within bounds.
+ */
+ public function isValid(): bool
+ {
+ return $this->pos < $this->length;
+ }
+
+ /**
+ * Check if the remaining input starts with the given string.
+ */
+ public function startsWith(string $needle): bool
+ {
+ $len = strlen($needle);
+
+ if ($this->pos + $len > $this->length) {
+ return false;
+ }
+
+ return substr($this->input, $this->pos, $len) === $needle;
+ }
+}
diff --git a/src/Parser/EvaluatableExpression.php b/src/Parser/EvaluatableExpression.php
deleted file mode 100644
index 49e35da..0000000
--- a/src/Parser/EvaluatableExpression.php
+++ /dev/null
@@ -1,69 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Parser;
-
-use nicoSWD\Rule\Expression\BaseExpression;
-use nicoSWD\Rule\Expression\ExpressionFactoryInterface;
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\Type\Operator;
-
-final class EvaluatableExpression
-{
- public (BaseToken & Operator) | null $operator = null;
- public array $values = [];
-
- public function __construct(
- private readonly ExpressionFactoryInterface $expressionFactory,
- ) {
- }
-
- /** @throws Exception\ParserException */
- public function evaluate(): bool
- {
- $result = $this->expression()->evaluate(...$this->values);
- $this->clear();
-
- return $result;
- }
-
- public function isComplete(): bool
- {
- return $this->hasOperator() && $this->hasBothValues();
- }
-
- public function addValue(mixed $value): void
- {
- $this->values[] = $value;
- }
-
- public function hasBothValues(): bool
- {
- return count($this->values) === 2;
- }
-
- public function hasNoValues(): bool
- {
- return count($this->values) === 0;
- }
-
- public function hasOperator(): bool
- {
- return $this->operator !== null;
- }
-
- private function clear(): void
- {
- $this->operator = null;
- $this->values = [];
- }
-
- private function expression(): BaseExpression
- {
- return $this->expressionFactory->createFromOperator($this->operator);
- }
-}
diff --git a/src/Parser/EvaluatableExpressionFactory.php b/src/Parser/EvaluatableExpressionFactory.php
deleted file mode 100644
index 9aa8458..0000000
--- a/src/Parser/EvaluatableExpressionFactory.php
+++ /dev/null
@@ -1,20 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Parser;
-
-use nicoSWD\Rule\Expression\ExpressionFactory;
-
-final class EvaluatableExpressionFactory
-{
- public function create(): EvaluatableExpression
- {
- return new EvaluatableExpression(
- new ExpressionFactory()
- );
- }
-}
diff --git a/src/Parser/Exception/ParserException.php b/src/Parser/Exception/ParserException.php
index f70e37a..e79d685 100644
--- a/src/Parser/Exception/ParserException.php
+++ b/src/Parser/Exception/ParserException.php
@@ -1,14 +1,16 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\Parser\Exception;
use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\Type\Operator;
final class ParserException extends \Exception
{
@@ -17,24 +19,14 @@ public static function unexpectedToken(BaseToken $token): self
return new self(sprintf('Unexpected "%s" at position %d', $token->getValue(), $token->getOffset()));
}
- public static function unknownToken(BaseToken $token): self
- {
- return new self(sprintf('Unknown token "%s" at position %d', $token->getValue(), $token->getOffset()));
- }
-
- public static function incompleteExpression(BaseToken $token): self
- {
- return new self(sprintf('Incomplete expression for token "%s"', $token->getValue()));
- }
-
- public static function undefinedVariable(string $name, BaseToken $token): self
+ public static function undefinedVariable(string $name, int $offset): self
{
- return new self(sprintf('Undefined variable "%s" at position %d', $name, $token->getOffset()));
+ return new self(sprintf('Undefined variable "%s" at position %d', $name, $offset));
}
- public static function undefinedMethod(string $name, BaseToken $token): self
+ public static function undefinedMethod(string $name, int $offset): self
{
- return new self(sprintf('Undefined method "%s" at position %d', $name, $token->getOffset()));
+ return new self(sprintf('Undefined method "%s" at position %d', $name, $offset));
}
public static function forbiddenMethod(string $name, BaseToken $token): self
@@ -42,9 +34,9 @@ public static function forbiddenMethod(string $name, BaseToken $token): self
return new self(sprintf('Forbidden method "%s" at position %d', $name, $token->getOffset()));
}
- public static function undefinedFunction(string $name, BaseToken $token): self
+ public static function undefinedFunction(string $name, int $offset): self
{
- return new self(sprintf('%s is not defined at position %d', $name, $token->getOffset()));
+ return new self(sprintf('%s is not defined at position %d', $name, $offset));
}
public static function unexpectedComma(BaseToken $token): self
@@ -62,10 +54,20 @@ public static function unsupportedType(string $type): self
return new self(sprintf('Unsupported PHP type: "%s"', $type));
}
- public static function unknownOperator(BaseToken & Operator $token): self
+ public static function unknownOperator(BaseToken $token): self
{
return new self(
sprintf('Unexpected operator %s at position %d', $token->getOriginalValue(), $token->getOffset())
);
}
+
+ public static function expectedArray(string $type): self
+ {
+ return new self(sprintf('Expected array, got "%s"', $type));
+ }
+
+ public static function duplicateRegexModifier(string $modifier, int $offset): self
+ {
+ return new self(sprintf('Duplicate regex modifier "%s" at position %d', $modifier, $offset));
+ }
}
diff --git a/src/Parser/Parser.php b/src/Parser/Parser.php
index 0fe5d37..6f518ea 100644
--- a/src/Parser/Parser.php
+++ b/src/Parser/Parser.php
@@ -1,96 +1,449 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\Parser;
-use Closure;
-use nicoSWD\Rule\Compiler\CompilerFactoryInterface;
-use nicoSWD\Rule\Compiler\CompilerInterface;
-use nicoSWD\Rule\TokenStream\TokenStream;
+use nicoSWD\Rule\AST\AdditionNode;
+use nicoSWD\Rule\AST\ArrayNode;
+use nicoSWD\Rule\AST\BoolNode;
+use nicoSWD\Rule\AST\ComparisonNode;
+use nicoSWD\Rule\AST\ComparisonOperator;
+use nicoSWD\Rule\AST\DivisionNode;
+use nicoSWD\Rule\AST\FloatNode;
+use nicoSWD\Rule\AST\FunctionCallNode;
+use nicoSWD\Rule\AST\IntegerNode;
+use nicoSWD\Rule\AST\LogicalNode;
+use nicoSWD\Rule\AST\LogicalOperator;
+use nicoSWD\Rule\AST\MethodCallNode;
+use nicoSWD\Rule\AST\ModuloNode;
+use nicoSWD\Rule\AST\MultiplicationNode;
+use nicoSWD\Rule\AST\Node;
+use nicoSWD\Rule\AST\NotNode;
+use nicoSWD\Rule\AST\NullNode;
+use nicoSWD\Rule\AST\RegexNode;
+use nicoSWD\Rule\AST\StringNode;
+use nicoSWD\Rule\AST\SubtractionNode;
+use nicoSWD\Rule\AST\UnaryMinusNode;
+use nicoSWD\Rule\AST\VariableNode;
use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\TokenType;
-use nicoSWD\Rule\TokenStream\Token\Type\Operator;
+use nicoSWD\Rule\TokenStream\Token\TokenKind;
+use nicoSWD\Rule\TokenStream\TokenIterator;
+use nicoSWD\Rule\Lexer\Lexer;
+/**
+ * Recursive descent parser that builds an AST from the token stream.
+ *
+ * Grammar (precedence from lowest to highest):
+ * expression -> logical_or
+ * logical_or -> logical_and ( "||" logical_and )*
+ * logical_and -> comparison ( "&&" comparison )*
+ * comparison -> additive ( comparison_op additive )?
+ * additive -> multiplicative ( ("+" | "-") multiplicative )*
+ * multiplicative -> unary ( ("*" | "/" | "%") unary )*
+ * unary -> "-" unary
+ * | "!" unary
+ * | primary
+ * primary -> "(" expression ")"
+ * | value
+ * value -> variable method_call*
+ * | function_call
+ * | string | integer | float | bool | null | regex
+ * | array_literal method_call*
+ */
final readonly class Parser
{
public function __construct(
- private TokenStream $tokenStream,
- private EvaluatableExpressionFactory $expressionFactory,
- private CompilerFactoryInterface $compilerFactory,
+ private Lexer $tokenizer,
) {
}
/** @throws Exception\ParserException */
- public function parse(string $rule): string
+ public function parse(string $rule): Node
+ {
+ $tokenIterator = new TokenIterator($this->tokenizer->tokenize($rule));
+
+ if (!$tokenIterator->valid()) {
+ return new BoolNode(false);
+ }
+
+ $node = $this->parseExpression($tokenIterator);
+
+ // If there are remaining non-ignorable tokens, that's a syntax error
+ $this->skipIgnoredTokens($tokenIterator);
+ if ($tokenIterator->valid()) {
+ throw Exception\ParserException::unexpectedToken($tokenIterator->current());
+ }
+
+ return $node;
+ }
+
+ /** @throws Exception\ParserException */
+ private function parseExpression(TokenIterator $tokens): Node
+ {
+ return $this->parseLogicalOr($tokens);
+ }
+
+ /** @throws Exception\ParserException */
+ private function parseLogicalOr(TokenIterator $tokens): Node
{
- $compiler = $this->compilerFactory->create();
- $expression = $this->expressionFactory->create();
+ $left = $this->parseLogicalAnd($tokens);
+
+ while ($this->peekToken($tokens)?->isOfKind(TokenKind::OR)) {
+ $this->consumeToken($tokens); // consume ||
+ $right = $this->parseLogicalAnd($tokens);
+ $left = new LogicalNode($left, $right, LogicalOperator::OR);
+ }
+
+ return $left;
+ }
+
+ /** @throws Exception\ParserException */
+ private function parseLogicalAnd(TokenIterator $tokens): Node
+ {
+ $left = $this->parseComparison($tokens);
+
+ while ($this->peekToken($tokens)?->isOfKind(TokenKind::AND)) {
+ $this->consumeToken($tokens); // consume &&
+ $right = $this->parseComparison($tokens);
+ $left = new LogicalNode($left, $right, LogicalOperator::AND);
+ }
+
+ return $left;
+ }
+
+ /** @throws Exception\ParserException */
+ private function parseComparison(TokenIterator $tokens): Node
+ {
+ $left = $this->parseAdditive($tokens);
+
+ $operatorToken = $this->peekToken($tokens);
+
+ if ($operatorToken !== null) {
+ $operator = $this->matchComparisonOperator($operatorToken);
- foreach ($this->tokenStream->getStream($rule) as $token) {
- $handler = $this->getHandlerForToken($token, $expression);
- $handler($compiler);
+ if ($operator !== null) {
+ $this->consumeToken($tokens); // consume operator
+ $right = $this->parseAdditive($tokens);
- if ($expression->isComplete()) {
- $compiler->addBoolean($expression->evaluate());
+ return new ComparisonNode($left, $right, $operator);
}
}
- return $compiler->getCompiledRule();
+ return $left;
+ }
+
+ /** @throws Exception\ParserException */
+ private function parseAdditive(TokenIterator $tokens): Node
+ {
+ $left = $this->parseMultiplicative($tokens);
+
+ while (
+ ($peeked = $this->peekToken($tokens)) !== null
+ && ($peeked->isOfKind(TokenKind::PLUS) || $peeked->isOfKind(TokenKind::MINUS))
+ ) {
+ $operator = $this->peekToken($tokens);
+ $this->consumeToken($tokens); // consume + or -
+ $right = $this->parseMultiplicative($tokens);
+
+ $left = match ($operator->getKind()) {
+ TokenKind::PLUS => new AdditionNode($left, $right),
+ TokenKind::MINUS => new SubtractionNode($left, $right),
+ default => throw new \RuntimeException('Unexpected additive operator'),
+ };
+ }
+
+ return $left;
+ }
+
+ /** @throws Exception\ParserException */
+ private function parseMultiplicative(TokenIterator $tokens): Node
+ {
+ $left = $this->parseUnary($tokens);
+
+ while (
+ ($peeked = $this->peekToken($tokens)) !== null
+ && ($peeked->isOfKind(TokenKind::MULTIPLY) || $peeked->isOfKind(TokenKind::DIVIDE) || $peeked->isOfKind(TokenKind::MODULO))
+ ) {
+ $operator = $this->peekToken($tokens);
+ $this->consumeToken($tokens); // consume *, /, or %
+ $right = $this->parseUnary($tokens);
+
+ $left = match ($operator->getKind()) {
+ TokenKind::MULTIPLY => new MultiplicationNode($left, $right),
+ TokenKind::DIVIDE => new DivisionNode($left, $right),
+ TokenKind::MODULO => new ModuloNode($left, $right),
+ default => throw new \RuntimeException('Unexpected multiplicative operator'),
+ };
+ }
+
+ return $left;
+ }
+
+ /** @throws Exception\ParserException */
+ private function parseUnary(TokenIterator $tokens): Node
+ {
+ $this->skipIgnoredTokens($tokens);
+
+ if (!$tokens->valid()) {
+ throw Exception\ParserException::unexpectedEndOfString();
+ }
+
+ $token = $tokens->current();
+
+ // Unary minus: -expr
+ if ($token->isOfKind(TokenKind::MINUS)) {
+ $tokens->next();
+ $operand = $this->parseUnary($tokens);
+
+ return new UnaryMinusNode($operand);
+ }
+
+ // Logical NOT: !expr
+ if ($token->isOfKind(TokenKind::NOT)) {
+ $tokens->next();
+ $operand = $this->parseUnary($tokens);
+
+ return new NotNode($operand);
+ }
+
+ return $this->parsePrimary($tokens);
+ }
+
+ /** @throws Exception\ParserException */
+ private function parsePrimary(TokenIterator $tokens): Node
+ {
+ if (!$tokens->valid()) {
+ throw Exception\ParserException::unexpectedEndOfString();
+ }
+
+ $token = $tokens->current();
+
+ // Parenthesized expression
+ if ($token->isOfKind(TokenKind::OPENING_PARENTHESIS)) {
+ $tokens->next();
+ $node = $this->parseExpression($tokens);
+ $this->expectClosingParenthesis($tokens);
+
+ return $node;
+ }
+
+ // Array literal (may have method calls chained)
+ if ($token->isOfKind(TokenKind::OPENING_ARRAY)) {
+ $node = $this->parseArrayLiteral($tokens);
+ return $this->parseMethodChain($node, $tokens);
+ }
+
+ // Function call
+ if ($token->isOfKind(TokenKind::FUNCTION)) {
+ $node = $this->parseFunctionCall($tokens);
+ return $this->parseMethodChain($node, $tokens);
+ }
+
+ // Simple value tokens (advance past them)
+ $node = $this->parseSimpleValue($token);
+
+ if ($node !== null) {
+ $tokens->next();
+
+ // Check for method calls chained onto this value
+ return $this->parseMethodChain($node, $tokens);
+ }
+
+ throw Exception\ParserException::unexpectedToken($token);
}
- private function getHandlerForToken(BaseToken $token, EvaluatableExpression $expression): Closure
+ private function parseSimpleValue(BaseToken $token): ?Node
{
- return match ($token->getType()) {
- TokenType::VALUE => $this->handleValueToken($token, $expression),
- TokenType::OPERATOR => $this->handleOperatorToken($token, $expression),
- TokenType::LOGICAL => $this->handleLogicalToken($token),
- TokenType::PARENTHESIS => $this->handleParenthesisToken($token),
- TokenType::COMMENT, TokenType::SPACE => $this->handleDummyToken(),
- default => $this->handleUnknownToken($token),
+ return match ($token->getKind()) {
+ TokenKind::VARIABLE => new VariableNode($token->getOriginalValue(), $token->getOffset()),
+ TokenKind::ENCAPSED_STRING, TokenKind::STRING => new StringNode($token->getValue()),
+ TokenKind::INTEGER => new IntegerNode((int) $token->getValue()),
+ TokenKind::FLOAT => new FloatNode((float) $token->getValue()),
+ TokenKind::BOOL_TRUE => new BoolNode(true),
+ TokenKind::BOOL_FALSE => new BoolNode(false),
+ TokenKind::NULL => new NullNode(),
+ TokenKind::REGEX => new RegexNode($token->getValue(), $token),
+ default => null,
};
}
- private function handleValueToken(BaseToken $token, EvaluatableExpression $expression): Closure
+ /** @throws Exception\ParserException */
+ private function parseFunctionCall(TokenIterator $tokens): FunctionCallNode
+ {
+ $token = $tokens->current();
+ $functionName = $token->getValue();
+ $offset = $token->getOffset();
+
+ // Move past the function token
+ $tokens->next();
+
+ $arguments = $this->parseParenthesizedArguments($tokens);
+
+ return new FunctionCallNode($functionName, $arguments, $offset);
+ }
+
+ /** @throws Exception\ParserException */
+ private function parseMethodChain(Node $object, TokenIterator $tokens): Node
{
- return static fn () => $expression->addValue($token->getValue());
+ $this->skipIgnoredTokens($tokens);
+
+ while ($tokens->valid() && $tokens->current()->isOfKind(TokenKind::METHOD)) {
+ $methodToken = $tokens->current();
+ $methodName = $methodToken->getValue();
+ $offset = $methodToken->getOffset();
+ $tokens->next();
+
+ $arguments = $this->parseParenthesizedArguments($tokens);
+
+ $object = new MethodCallNode($object, $methodName, $arguments, $offset);
+
+ $this->skipIgnoredTokens($tokens);
+ }
+
+ return $object;
}
- private function handleLogicalToken(BaseToken $token): Closure
+ /** @throws Exception\ParserException */
+ private function parseParenthesizedArguments(TokenIterator $tokens): array
{
- return static fn (CompilerInterface $compiler) => $compiler->addLogical($token);
+ // Consume the opening parenthesis
+ $this->skipIgnoredTokens($tokens);
+ if (!$tokens->valid() || !$tokens->current()->isOfKind(TokenKind::OPENING_PARENTHESIS)) {
+ throw Exception\ParserException::unexpectedToken($tokens->valid() ? $tokens->current() : null);
+ }
+ $tokens->next();
+
+ return $this->parseArguments($tokens);
}
- private function handleParenthesisToken(BaseToken $token): Closure
+ /** @throws Exception\ParserException */
+ private function parseArguments(TokenIterator $tokens): array
{
- return static fn (CompilerInterface $compiler) => $compiler->addParentheses($token);
+ return $this->parseCommaSeparatedList(
+ $tokens,
+ static fn (BaseToken $token): bool => $token->isOfKind(TokenKind::CLOSING_PARENTHESIS),
+ );
}
- private function handleUnknownToken(BaseToken $token): Closure
+ /** @throws Exception\ParserException */
+ private function parseArrayLiteral(TokenIterator $tokens): ArrayNode
{
- return static fn () => throw Exception\ParserException::unknownToken($token);
+ // Consume the opening '['
+ $tokens->next();
+
+ $items = $this->parseCommaSeparatedList(
+ $tokens,
+ static fn (BaseToken $token): bool => $token->isOfKind(TokenKind::CLOSING_ARRAY),
+ );
+
+ return new ArrayNode($items);
}
- private function handleOperatorToken(BaseToken & Operator $token, EvaluatableExpression $expression): Closure
+ /**
+ * @param callable(BaseToken): bool $isTerminator
+ * @return Node[]
+ * @throws Exception\ParserException
+ */
+ private function parseCommaSeparatedList(TokenIterator $tokens, callable $isTerminator): array
{
- return static function () use ($token, $expression): void {
- if ($expression->hasOperator()) {
+ $items = [];
+ $expectComma = false;
+
+ while ($tokens->valid()) {
+ $this->skipIgnoredTokens($tokens);
+
+ if (!$tokens->valid()) {
+ throw Exception\ParserException::unexpectedEndOfString();
+ }
+
+ $token = $tokens->current();
+
+ // Closing token ends the list
+ if ($isTerminator($token)) {
+ $tokens->next(); // consume the closing token
+ return $items;
+ }
+
+ if ($token->isOfKind(TokenKind::COMMA)) {
+ if (!$expectComma) {
+ throw Exception\ParserException::unexpectedComma($token);
+ }
+ $expectComma = false;
+ $tokens->next();
+ continue;
+ }
+
+ if ($expectComma) {
throw Exception\ParserException::unexpectedToken($token);
- } elseif ($expression->hasNoValues()) {
- throw Exception\ParserException::incompleteExpression($token);
}
- $expression->operator = $token;
- };
+ // Parse the item value (could be a complex expression)
+ $item = $this->parsePrimary($tokens);
+ $items[] = $item;
+ $expectComma = true;
+ }
+
+ throw Exception\ParserException::unexpectedEndOfString();
+ }
+
+ /** @throws Exception\ParserException */
+ private function expectClosingParenthesis(TokenIterator $tokens): void
+ {
+ $this->skipIgnoredTokens($tokens);
+
+ if (!$tokens->valid()) {
+ throw Exception\ParserException::unexpectedEndOfString();
+ }
+
+ $token = $tokens->current();
+
+ if (!$token->isOfKind(TokenKind::CLOSING_PARENTHESIS)) {
+ throw Exception\ParserException::unexpectedToken($token);
+ }
+
+ $tokens->next(); // consume ')'
}
- private function handleDummyToken(): Closure
+ private function matchComparisonOperator(BaseToken $token): ?ComparisonOperator
{
- return static function (): void {
- // Do nothing
+ return match ($token->getKind()) {
+ TokenKind::EQUAL => ComparisonOperator::EQUAL,
+ TokenKind::EQUAL_STRICT => ComparisonOperator::EQUAL_STRICT,
+ TokenKind::NOT_EQUAL => ComparisonOperator::NOT_EQUAL,
+ TokenKind::NOT_EQUAL_STRICT => ComparisonOperator::NOT_EQUAL_STRICT,
+ TokenKind::LESS_THAN => ComparisonOperator::LESS_THAN,
+ TokenKind::GREATER => ComparisonOperator::GREATER_THAN,
+ TokenKind::LESS_THAN_EQUAL => ComparisonOperator::LESS_THAN_EQUAL,
+ TokenKind::GREATER_EQUAL => ComparisonOperator::GREATER_THAN_EQUAL,
+ TokenKind::IN => ComparisonOperator::IN,
+ TokenKind::NOT_IN => ComparisonOperator::NOT_IN,
+ default => null,
};
}
+
+ private function peekToken(TokenIterator $tokens): ?BaseToken
+ {
+ $this->skipIgnoredTokens($tokens);
+
+ return $tokens->valid() ? $tokens->current() : null;
+ }
+
+ private function consumeToken(TokenIterator $tokens): void
+ {
+ $tokens->next();
+ }
+
+ private function skipIgnoredTokens(TokenIterator $tokens): void
+ {
+ while ($tokens->valid() && $tokens->current()->canBeIgnored()) {
+ $tokens->next();
+ }
+ }
}
diff --git a/src/Rule.php b/src/Rule.php
index 3d3b8e0..08cc63f 100644
--- a/src/Rule.php
+++ b/src/Rule.php
@@ -1,68 +1,100 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule;
-use Exception;
-use nicoSWD\Rule\Evaluator\EvaluatorInterface;
+use nicoSWD\Rule\AST\Node;
+use nicoSWD\Rule\Parser\Exception\ParserException;
+/**
+ * Convenience class for evaluating a single rule expression.
+ *
+ * This class provides a simple, familiar API for evaluating rules.
+ * For more advanced use cases (multiple rules, custom configuration),
+ * use RuleEngine directly.
+ *
+ * Usage:
+ * ```php
+ * // Simple usage (creates a default RuleEngine internally)
+ * $rule = new Rule('foo > 5', ['foo' => 10]);
+ * $rule->isTrue(); // true
+ *
+ * // With a shared engine (more efficient for multiple rules)
+ * $engine = RuleEngine::builder()->withDefaultVariables(['foo' => 10])->build();
+ * $rule1 = new Rule('foo > 5', engine: $engine);
+ * $rule2 = new Rule('foo < 3', engine: $engine);
+ * ```
+ */
class Rule
{
- private readonly Parser\Parser $parser;
- private string $rule;
- private string $parsedRule = '';
- private string $error = '';
- private static object $container;
+ private readonly RuleEngine $engine;
+ private readonly string $rule;
+ private readonly array $variables;
+ private ?Node $ast = null;
+ public string $error = '';
- public function __construct(string $rule, array $variables = [])
- {
- if (!isset(self::$container)) {
- self::$container = require __DIR__ . '/container.php';
- }
-
- $this->parser = self::$container->parser($variables);
+ public function __construct(
+ string $rule,
+ array $variables = [],
+ ?RuleEngine $engine = null,
+ ) {
+ $this->engine = $engine ?? RuleEngine::builder()->build();
$this->rule = $rule;
+ $this->variables = $variables;
}
- /** @throws Parser\Exception\ParserException */
+ /**
+ * @throws ParserException
+ */
public function isTrue(): bool
{
- /** @var EvaluatorInterface $evaluator */
- $evaluator = self::$container->evaluator();
+ if ($this->ast === null) {
+ $this->ast = $this->engine->parse($this->rule, $this->variables);
+ }
- return $evaluator->evaluate(
- $this->parsedRule ?:
- $this->parser->parse($this->rule)
- );
+ return $this->engine->evaluateNode($this->ast);
}
- /** @throws Parser\Exception\ParserException */
+ /**
+ * @throws ParserException
+ */
public function isFalse(): bool
{
return !$this->isTrue();
}
/**
- * Tells whether a rule is valid (as in "can be parsed without error") or not.
+ * Evaluate the rule and return the actual computed result.
+ *
+ * For pure value expressions (e.g. "5 * 3"), this returns the computed value.
+ * For comparison/logical expressions (e.g. "foo > 5"), this returns a bool.
+ *
+ * @return mixed
+ * @throws ParserException
*/
- public function isValid(): bool
+ public function result(): mixed
{
- try {
- $this->parsedRule = $this->parser->parse($this->rule);
- } catch (Exception $e) {
- $this->error = $e->getMessage();
- return false;
+ if ($this->ast === null) {
+ $this->ast = $this->engine->parse($this->rule, $this->variables);
}
- return true;
+ return $this->engine->resolveNode($this->ast);
}
- public function getError(): string
+ /**
+ * Tells whether a rule is valid (as in "can be parsed and evaluated without error") or not.
+ */
+ public function isValid(): bool
{
- return $this->error;
+ $this->error = $this->engine->getError($this->rule, $this->variables);
+
+ return $this->error === '';
}
}
diff --git a/src/RuleEngine.php b/src/RuleEngine.php
new file mode 100644
index 0000000..2bc8083
--- /dev/null
+++ b/src/RuleEngine.php
@@ -0,0 +1,196 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule;
+
+use Exception;
+use nicoSWD\Rule\AST\AstEvaluator;
+use nicoSWD\Rule\AST\Node;
+use nicoSWD\Rule\Parser\Exception\ParserException;
+use nicoSWD\Rule\Parser\Parser;
+use nicoSWD\Rule\TokenStream\VariableRegistry;
+
+/**
+ * The main entry point for the rule parser engine.
+ *
+ * This class wires up all internal dependencies and provides a clean,
+ * easy-to-use API for evaluating rules.
+ *
+ * Usage:
+ * ```php
+ * // Simple evaluation
+ * $engine = RuleEngine::builder()->build();
+ * $result = $engine->evaluate('foo > 5', ['foo' => 10]);
+ *
+ * // With default variables
+ * $engine = RuleEngine::builder()
+ * ->withDefaultVariables(['foo' => 10])
+ * ->build();
+ * $result = $engine->evaluate('foo > 5');
+ *
+ * // Check rule validity
+ * $isValid = $engine->isValid('2 == 2 && (1 < 3');
+ * $error = $engine->getError('2 == 2 && (1 < 3');
+ *
+ * // Create a Rule object for repeated use
+ * $rule = $engine->createRule('foo > 5', ['foo' => 10]);
+ * $rule->isTrue();
+ *
+ * // Advanced customization
+ * $engine = RuleEngine::builder()
+ * ->withGrammar(new MyCustomGrammar())
+ * ->withTokenizer(new MyTokenizer())
+ * ->build();
+ * ```
+ */
+final readonly class RuleEngine
+{
+ public function __construct(
+ private Parser $parser,
+ private AstEvaluator $astEvaluator,
+ private VariableRegistry $variableRegistry,
+ private array $defaultVariables = [],
+ ) {
+ }
+
+ /**
+ * Evaluate a rule string and return whether it's true or false.
+ *
+ * @param string $rule The rule expression to evaluate
+ * @param array $variables Optional variables to use (merged with defaults)
+ * @return bool
+ * @throws ParserException
+ */
+ public function evaluate(string $rule, array $variables = []): bool
+ {
+ $ast = $this->parse($rule, $variables);
+
+ return $this->evaluateNode($ast);
+ }
+
+ /**
+ * Evaluate a pre-parsed AST node and return whether it's true or false.
+ *
+ * Use this when you already have a parsed Node (e.g., from a cached parse)
+ * to avoid re-parsing the rule string.
+ *
+ * @param Node $ast The pre-parsed AST node
+ * @return bool
+ * @throws ParserException
+ */
+ public function evaluateNode(Node $ast): bool
+ {
+ return $this->astEvaluator->evaluate($ast);
+ }
+
+ /**
+ * Evaluate a rule string and return the actual computed result.
+ *
+ * For pure value expressions (e.g. "5 * 3"), this returns the computed value.
+ * For comparison/logical expressions (e.g. "foo > 5"), this returns a bool.
+ *
+ * @param string $rule The rule expression to evaluate
+ * @param array $variables Optional variables to use (merged with defaults)
+ * @return mixed
+ * @throws ParserException
+ */
+ public function result(string $rule, array $variables = []): mixed
+ {
+ $ast = $this->parse($rule, $variables);
+
+ return $this->resolveNode($ast);
+ }
+
+ /**
+ * Resolve a pre-parsed AST node to its actual computed value, without casting to bool.
+ *
+ * Use this when you already have a parsed Node (e.g., from a cached parse)
+ * to avoid re-parsing the rule string.
+ *
+ * @param Node $ast The pre-parsed AST node
+ * @return mixed
+ * @throws ParserException
+ */
+ public function resolveNode(Node $ast): mixed
+ {
+ return $this->astEvaluator->resolve($ast);
+ }
+
+ /**
+ * Check whether a rule string is syntactically valid and can be evaluated.
+ *
+ * @param string $rule The rule expression to validate
+ * @param array $variables Optional variables to use (merged with defaults)
+ * @return bool
+ */
+ public function isValid(string $rule, array $variables = []): bool
+ {
+ return $this->getError($rule, $variables) === '';
+ }
+
+ /**
+ * Get the error message from the last validation, if any.
+ *
+ * @param string $rule The rule expression to validate
+ * @param array $variables Optional variables to use (merged with defaults)
+ * @return string Empty string if the rule is valid
+ */
+ public function getError(string $rule, array $variables = []): string
+ {
+ try {
+ $ast = $this->parse($rule, $variables);
+ $this->astEvaluator->evaluate($ast);
+ } catch (Exception $e) {
+ return $e->getMessage();
+ }
+
+ return '';
+ }
+
+ /**
+ * Create a Rule object for the given rule string and variables.
+ *
+ * This is useful when you need to evaluate the same rule multiple times,
+ * or when you want to use the Rule's convenience methods (isTrue, isFalse, etc.).
+ *
+ * @param string $rule The rule expression
+ * @param array $variables Optional variables (merged with defaults)
+ * @return Rule
+ */
+ public function createRule(string $rule, array $variables = []): Rule
+ {
+ return new Rule($rule, $variables, $this);
+ }
+
+ /**
+ * Parse a rule string into an AST node, applying the given variables.
+ *
+ * @throws ParserException
+ * @internal
+ */
+ public function parse(string $rule, array $variables = []): Node
+ {
+ $this->variableRegistry->setVariables(
+ array_merge($this->defaultVariables, $variables),
+ );
+
+ return $this->parser->parse($rule);
+ }
+
+ /**
+ * Create a RuleEngineBuilder for advanced customization.
+ *
+ * @return RuleEngineBuilder
+ */
+ public static function builder(): RuleEngineBuilder
+ {
+ return new RuleEngineBuilder();
+ }
+}
diff --git a/src/RuleEngineBuilder.php b/src/RuleEngineBuilder.php
new file mode 100644
index 0000000..241acc6
--- /dev/null
+++ b/src/RuleEngineBuilder.php
@@ -0,0 +1,129 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule;
+
+use nicoSWD\Rule\AST\AstEvaluator;
+use nicoSWD\Rule\Grammar\Grammar;
+use nicoSWD\Rule\Grammar\JavaScript\JavaScript;
+use nicoSWD\Rule\Parser\Parser;
+use nicoSWD\Rule\Lexer\DefaultLexer;
+use nicoSWD\Rule\Lexer\Lexer;
+use nicoSWD\Rule\TokenStream\FunctionRegistry;
+use nicoSWD\Rule\TokenStream\MethodRegistry;
+use nicoSWD\Rule\TokenStream\ObjectMethodCallerFactory;
+use nicoSWD\Rule\TokenStream\Token\TokenFactory;
+use nicoSWD\Rule\TokenStream\VariableRegistry;
+
+/**
+ * Builder for creating a RuleEngine with custom configuration.
+ *
+ * Usage:
+ * ```php
+ * $engine = RuleEngine::builder()
+ * ->withGrammar(new MyCustomGrammar())
+ * ->withTokenizer(new MyTokenizer())
+ * ->withDefaultVariables(['env' => 'prod'])
+ * ->build();
+ * ```
+ */
+final class RuleEngineBuilder
+{
+ private ?Grammar $grammar = null;
+ private ?Lexer $tokenizer = null;
+ private array $defaultVariables = [];
+
+ /**
+ * Create a new builder instance.
+ */
+ public static function create(): self
+ {
+ return new self();
+ }
+
+ /**
+ * Set a custom grammar for the rule engine.
+ *
+ * The grammar defines the syntax rules (operators, keywords, functions, methods).
+ *
+ * @param Grammar $grammar
+ * @return $this
+ */
+ public function withGrammar(Grammar $grammar): self
+ {
+ $this->grammar = $grammar;
+
+ return $this;
+ }
+
+ /**
+ * Set a custom tokenizer for the rule engine.
+ *
+ * The tokenizer converts a rule string into a stream of tokens.
+ *
+ * @param Lexer $tokenizer
+ * @return $this
+ */
+ public function withTokenizer(Lexer $tokenizer): self
+ {
+ $this->tokenizer = $tokenizer;
+
+ return $this;
+ }
+
+ /**
+ * Set default variables that will be available for all rule evaluations.
+ *
+ * These variables can be overridden on a per-evaluation basis.
+ *
+ * @param array $variables
+ * @return $this
+ */
+ public function withDefaultVariables(array $variables): self
+ {
+ $this->defaultVariables = $variables;
+
+ return $this;
+ }
+
+ /**
+ * Build the RuleEngine with the configured options.
+ *
+ * @return RuleEngine
+ */
+ public function build(): RuleEngine
+ {
+ $tokenFactory = new TokenFactory();
+ $grammar = $this->grammar ?? new JavaScript();
+ $tokenizer = $this->tokenizer ?? new DefaultLexer($grammar, $tokenFactory);
+
+ $variableRegistry = new VariableRegistry([], $tokenFactory);
+ $functionRegistry = new FunctionRegistry($grammar);
+ $methodRegistry = new MethodRegistry($grammar, $tokenFactory, new ObjectMethodCallerFactory());
+
+ $parser = new Parser(
+ $tokenizer,
+ );
+
+ $astEvaluator = new AstEvaluator(
+ $variableRegistry,
+ $functionRegistry,
+ $methodRegistry,
+ $tokenFactory,
+ );
+
+ return new RuleEngine(
+ parser: $parser,
+ astEvaluator: $astEvaluator,
+ variableRegistry: $variableRegistry,
+ defaultVariables: $this->defaultVariables,
+ );
+ }
+}
diff --git a/src/TokenStream/Exception/ForbiddenMethodException.php b/src/TokenStream/Exception/ForbiddenMethodException.php
index 315930e..ce7abfb 100644
--- a/src/TokenStream/Exception/ForbiddenMethodException.php
+++ b/src/TokenStream/Exception/ForbiddenMethodException.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\TokenStream\Exception;
final class ForbiddenMethodException extends \Exception
diff --git a/src/TokenStream/Exception/UndefinedFunctionException.php b/src/TokenStream/Exception/UndefinedFunctionException.php
index a4115d7..8bab1a2 100644
--- a/src/TokenStream/Exception/UndefinedFunctionException.php
+++ b/src/TokenStream/Exception/UndefinedFunctionException.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\TokenStream\Exception;
final class UndefinedFunctionException extends \Exception
diff --git a/src/TokenStream/Exception/UndefinedMethodException.php b/src/TokenStream/Exception/UndefinedMethodException.php
index 4f27202..30e1b96 100644
--- a/src/TokenStream/Exception/UndefinedMethodException.php
+++ b/src/TokenStream/Exception/UndefinedMethodException.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\TokenStream\Exception;
final class UndefinedMethodException extends \Exception
diff --git a/src/TokenStream/Exception/UndefinedVariableException.php b/src/TokenStream/Exception/UndefinedVariableException.php
index adefc6c..c26be2b 100644
--- a/src/TokenStream/Exception/UndefinedVariableException.php
+++ b/src/TokenStream/Exception/UndefinedVariableException.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\TokenStream\Exception;
final class UndefinedVariableException extends \Exception
diff --git a/src/TokenStream/FunctionRegistry.php b/src/TokenStream/FunctionRegistry.php
new file mode 100644
index 0000000..f23b650
--- /dev/null
+++ b/src/TokenStream/FunctionRegistry.php
@@ -0,0 +1,67 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\TokenStream;
+
+use InvalidArgumentException;
+use nicoSWD\Rule\Grammar\CallableInterface;
+use nicoSWD\Rule\Grammar\Grammar;
+use nicoSWD\Rule\TokenStream\Exception\UndefinedFunctionException;
+
+class FunctionRegistry
+{
+ /** @var array */
+ private array $functions;
+
+ /** @var array */
+ private array $instances = [];
+
+ public function __construct(
+ private readonly Grammar $grammar,
+ ) {
+ $this->functions = [];
+ $this->registerFunctions();
+ }
+
+ /** @throws UndefinedFunctionException */
+ public function get(string $functionName): CallableInterface
+ {
+ if (!isset($this->functions[$functionName])) {
+ throw new UndefinedFunctionException($functionName);
+ }
+
+ return $this->instances[$functionName] ??= $this->createInstance($functionName);
+ }
+
+ private function createInstance(string $functionName): CallableInterface
+ {
+ $className = $this->functions[$functionName];
+ $function = new $className();
+
+ if (!$function instanceof CallableInterface) {
+ throw new InvalidArgumentException(
+ sprintf(
+ '%s must be an instance of %s',
+ $className,
+ CallableInterface::class
+ )
+ );
+ }
+
+ return $function;
+ }
+
+ private function registerFunctions(): void
+ {
+ foreach ($this->grammar->getInternalFunctions() as $internalCallable) {
+ $this->functions[$internalCallable->name] = $internalCallable->class;
+ }
+ }
+}
diff --git a/src/TokenStream/MethodRegistry.php b/src/TokenStream/MethodRegistry.php
new file mode 100644
index 0000000..b73bb23
--- /dev/null
+++ b/src/TokenStream/MethodRegistry.php
@@ -0,0 +1,72 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\TokenStream;
+
+use nicoSWD\Rule\Grammar\CallableInterface;
+use nicoSWD\Rule\Grammar\Grammar;
+use nicoSWD\Rule\TokenStream\Token\BaseToken;
+use nicoSWD\Rule\TokenStream\Token\TokenFactory;
+use nicoSWD\Rule\TokenStream\Token\TokenKind;
+
+class MethodRegistry
+{
+ /** @var array */
+ private array $methods;
+
+ public function __construct(
+ private readonly Grammar $grammar,
+ private readonly TokenFactory $tokenFactory,
+ private readonly ObjectMethodCallerFactoryInterface $userMethodFactory,
+ ) {
+ $this->methods = [];
+ $this->registerMethods();
+ }
+
+ /**
+ * @throws Exception\UndefinedMethodException
+ * @throws Exception\ForbiddenMethodException
+ */
+ public function get(string $methodName, BaseToken $token, mixed $rawValue = null): CallableInterface
+ {
+ if ($token->isOfKind(TokenKind::OBJECT)) {
+ return $this->getObjectMethodCaller($token, $methodName);
+ }
+
+ if (!isset($this->methods[$methodName])) {
+ throw new Exception\UndefinedMethodException();
+ }
+
+ // For regex tokens, pass the original token to preserve type information
+ if ($token->isOfKind(TokenKind::REGEX)) {
+ return new $this->methods[$methodName]($token);
+ }
+
+ // Pass the raw PHP value directly to the callable
+ return new $this->methods[$methodName]($rawValue);
+ }
+
+
+ private function registerMethods(): void
+ {
+ foreach ($this->grammar->getInternalMethods() as $internalCallable) {
+ $this->methods[$internalCallable->name] = $internalCallable->class;
+ }
+ }
+
+ /**
+ * @throws Exception\ForbiddenMethodException
+ * @throws Exception\UndefinedMethodException
+ */
+ private function getObjectMethodCaller(BaseToken $token, string $methodName): CallableInterface
+ {
+ return $this->userMethodFactory->create($token, $this->tokenFactory, $methodName);
+ }
+}
diff --git a/src/TokenStream/Node/BaseNode.php b/src/TokenStream/Node/BaseNode.php
deleted file mode 100644
index 4724f3e..0000000
--- a/src/TokenStream/Node/BaseNode.php
+++ /dev/null
@@ -1,157 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Node;
-
-use Closure;
-use nicoSWD\Rule\Grammar\CallableUserFunctionInterface;
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\Type\Method;
-use nicoSWD\Rule\TokenStream\Token\Type\Whitespace;
-use nicoSWD\Rule\TokenStream\TokenCollection;
-use nicoSWD\Rule\Parser\Exception\ParserException;
-use nicoSWD\Rule\TokenStream\TokenIterator;
-use nicoSWD\Rule\TokenStream\Token\TokenType;
-
-abstract class BaseNode
-{
- private string $methodName;
- private int $methodOffset = 0;
-
- public function __construct(
- protected readonly TokenIterator $tokenStream,
- ) {
- }
-
- /** @throws ParserException */
- abstract public function getNode(): BaseToken;
-
- protected function hasMethodCall(): bool
- {
- $stack = $this->tokenStream->getStack();
- $position = $stack->key();
- $hasMethod = false;
-
- while ($stack->valid()) {
- $stack->next();
-
- /** @var ?BaseToken $token */
- $token = $stack->current();
-
- if (!$token) {
- break;
- } elseif ($token instanceof Method) {
- $this->methodName = $token->getValue();
- $this->methodOffset = $stack->key();
- $hasMethod = true;
- break;
- } elseif (!$token instanceof Whitespace) {
- break;
- }
- }
-
- $stack->seek($position);
-
- return $hasMethod;
- }
-
- /** @throws ParserException */
- protected function getMethod(BaseToken $token): CallableUserFunctionInterface
- {
- $this->tokenStream->getStack()->seek($this->methodOffset);
-
- return $this->tokenStream->getMethod($this->getMethodName(), $token);
- }
-
- private function getMethodName(): string
- {
- return (string) preg_replace('~\W~', '', $this->methodName);
- }
-
- /** @throws ParserException */
- protected function getFunction(): Closure
- {
- return $this->tokenStream->getFunction($this->getFunctionName());
- }
-
- private function getFunctionName(): string
- {
- return (string) preg_replace('~\W~', '', $this->getCurrentNode()->getValue());
- }
-
- /** @throws ParserException */
- protected function getArrayItems(): TokenCollection
- {
- return $this->getCommaSeparatedValues(TokenType::SQUARE_BRACKET);
- }
-
- /** @throws ParserException */
- protected function getArguments(): TokenCollection
- {
- return $this->getCommaSeparatedValues(TokenType::PARENTHESIS);
- }
-
- protected function getCurrentNode(): BaseToken
- {
- return $this->tokenStream->getStack()->current();
- }
-
- /** @throws ParserException */
- private function getCommaSeparatedValues(TokenType $stopAt): TokenCollection
- {
- $items = new TokenCollection();
- $commaExpected = false;
-
- do {
- $token = $this->getNextToken();
-
- if (TokenType::isValue($token)) {
- if ($commaExpected) {
- throw ParserException::unexpectedToken($token);
- }
-
- $commaExpected = true;
- $items->attach($token);
- } elseif ($token->isOfType(TokenType::COMMA)) {
- if (!$commaExpected) {
- throw ParserException::unexpectedComma($token);
- }
-
- $commaExpected = false;
- } elseif ($token->isOfType($stopAt)) {
- break;
- } elseif (!$token->canBeIgnored()) {
- throw ParserException::unexpectedToken($token);
- }
- } while (true);
-
- $this->assertNoTrailingComma($commaExpected, $items, $token);
- $items->rewind();
-
- return $items;
- }
-
- /** @throws ParserException */
- private function getNextToken(): BaseToken
- {
- $this->tokenStream->next();
-
- if (!$this->tokenStream->valid()) {
- throw ParserException::unexpectedEndOfString();
- }
-
- return $this->tokenStream->current();
- }
-
- /** @throws ParserException */
- private function assertNoTrailingComma(bool $commaExpected, TokenCollection $items, BaseToken $token): void
- {
- if (!$commaExpected && $items->count() > 0) {
- throw ParserException::unexpectedComma($token);
- }
- }
-}
diff --git a/src/TokenStream/Node/NodeArray.php b/src/TokenStream/Node/NodeArray.php
deleted file mode 100644
index bb419a9..0000000
--- a/src/TokenStream/Node/NodeArray.php
+++ /dev/null
@@ -1,25 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Node;
-
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\TokenArray;
-
-final class NodeArray extends BaseNode
-{
- public function getNode(): BaseToken
- {
- $token = new TokenArray($this->getArrayItems());
-
- while ($this->hasMethodCall()) {
- $token = $this->getMethod($token)->call(...$this->getArguments());
- }
-
- return $token;
- }
-}
diff --git a/src/TokenStream/Node/NodeFunction.php b/src/TokenStream/Node/NodeFunction.php
deleted file mode 100644
index 8eba994..0000000
--- a/src/TokenStream/Node/NodeFunction.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Node;
-
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
-
-final class NodeFunction extends BaseNode
-{
- public function getNode(): BaseToken
- {
- return $this->getFunction()->call($this, ...$this->getArguments());
- }
-}
diff --git a/src/TokenStream/Node/NodeString.php b/src/TokenStream/Node/NodeString.php
deleted file mode 100644
index 9cf630c..0000000
--- a/src/TokenStream/Node/NodeString.php
+++ /dev/null
@@ -1,24 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Node;
-
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
-
-final class NodeString extends BaseNode
-{
- public function getNode(): BaseToken
- {
- $token = $this->getCurrentNode();
-
- while ($this->hasMethodCall()) {
- $token = $this->getMethod($token)->call(...$this->getArguments());
- }
-
- return $token;
- }
-}
diff --git a/src/TokenStream/Node/NodeVariable.php b/src/TokenStream/Node/NodeVariable.php
deleted file mode 100644
index 7b9600a..0000000
--- a/src/TokenStream/Node/NodeVariable.php
+++ /dev/null
@@ -1,29 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Node;
-
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
-
-final class NodeVariable extends BaseNode
-{
- public function getNode(): BaseToken
- {
- $token = $this->tokenStream->getVariable($this->getVariableName());
-
- while ($this->hasMethodCall()) {
- $token = $this->getMethod($token)->call(...$this->getArguments());
- }
-
- return $token;
- }
-
- private function getVariableName(): string
- {
- return $this->getCurrentNode()->getOriginalValue();
- }
-}
diff --git a/src/TokenStream/CallableUserMethod.php b/src/TokenStream/ObjectMethodCaller.php
similarity index 69%
rename from src/TokenStream/CallableUserMethod.php
rename to src/TokenStream/ObjectMethodCaller.php
index a59d5ff..2a5785e 100644
--- a/src/TokenStream/CallableUserMethod.php
+++ b/src/TokenStream/ObjectMethodCaller.php
@@ -1,24 +1,25 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\TokenStream;
use Closure;
-use nicoSWD\Rule\Grammar\CallableUserFunctionInterface;
+use nicoSWD\Rule\Grammar\CallableInterface;
use nicoSWD\Rule\TokenStream\Token\BaseToken;
use nicoSWD\Rule\TokenStream\Token\TokenFactory;
-final class CallableUserMethod implements CallableUserFunctionInterface
+final class ObjectMethodCaller implements CallableInterface
{
- private const MAGIC_METHOD_PREFIX = '__';
-
+ private const string MAGIC_METHOD_PREFIX = '__';
private readonly TokenFactory $tokenFactory;
private readonly Closure $callable;
-
private array $methodPrefixes = ['', 'get', 'is', 'get_', 'is_'];
/**
@@ -31,7 +32,7 @@ public function __construct(BaseToken $token, TokenFactory $tokenFactory, string
$this->callable = $this->getCallable($token, $methodName);
}
- public function call(?BaseToken ...$param): BaseToken
+ public function call(mixed ...$param): BaseToken
{
$callable = $this->callable;
@@ -54,9 +55,7 @@ private function getCallable(BaseToken $token, string $methodName): Closure
$method = $this->findCallableMethod($object, $methodName);
- return fn (?BaseToken ...$params): mixed => $method(
- ...$this->getTokenValues($params)
- );
+ return fn (mixed ...$params): mixed => $method(...$params);
}
/**
@@ -66,22 +65,26 @@ private function getCallable(BaseToken $token, string $methodName): Closure
private function findCallableMethod(object $object, string $methodName): callable
{
$this->assertNonMagicMethod($methodName);
- $index = 0;
- do {
- if (!isset($this->methodPrefixes[$index])) {
- throw new Exception\UndefinedMethodException();
+ // Check exact method name first
+ if (is_callable([$object, $methodName])) {
+ return [$object, $methodName];
+ }
+
+ // Try prefixed versions (get, is, get_, is_)
+ foreach ($this->methodPrefixes as $prefix) {
+ if ($prefix === '') {
+ continue;
}
- $callableMethod = $this->methodPrefixes[$index++] . $methodName;
- } while (!is_callable([$object, $callableMethod]));
+ $prefixedMethod = $prefix . $methodName;
- return [$object, $callableMethod];
- }
+ if (is_callable([$object, $prefixedMethod])) {
+ return [$object, $prefixedMethod];
+ }
+ }
- private function getTokenValues(array $params): array
- {
- return array_map(static fn (BaseToken $token): mixed => $token->getValue(), $params);
+ throw new Exception\UndefinedMethodException();
}
/** @throws Exception\ForbiddenMethodException */
diff --git a/src/TokenStream/CallableUserMethodFactory.php b/src/TokenStream/ObjectMethodCallerFactory.php
similarity index 59%
rename from src/TokenStream/CallableUserMethodFactory.php
rename to src/TokenStream/ObjectMethodCallerFactory.php
index 3a18863..d85d2fe 100644
--- a/src/TokenStream/CallableUserMethodFactory.php
+++ b/src/TokenStream/ObjectMethodCallerFactory.php
@@ -1,23 +1,26 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\TokenStream;
-use nicoSWD\Rule\Grammar\CallableUserFunctionInterface;
+use nicoSWD\Rule\Grammar\CallableInterface;
use nicoSWD\Rule\TokenStream\Token\BaseToken;
use nicoSWD\Rule\TokenStream\Token\TokenFactory;
-final class CallableUserMethodFactory implements CallableUserMethodFactoryInterface
+final class ObjectMethodCallerFactory implements ObjectMethodCallerFactoryInterface
{
/**
* {@inheritDoc}
*/
- public function create(BaseToken $token, TokenFactory $tokenFactory, string $methodName): CallableUserFunctionInterface
+ public function create(BaseToken $token, TokenFactory $tokenFactory, string $methodName): CallableInterface
{
- return new CallableUserMethod($token, $tokenFactory, $methodName);
+ return new ObjectMethodCaller($token, $tokenFactory, $methodName);
}
}
diff --git a/src/TokenStream/CallableUserMethodFactoryInterface.php b/src/TokenStream/ObjectMethodCallerFactoryInterface.php
similarity index 75%
rename from src/TokenStream/CallableUserMethodFactoryInterface.php
rename to src/TokenStream/ObjectMethodCallerFactoryInterface.php
index efb85de..f29dc3d 100644
--- a/src/TokenStream/CallableUserMethodFactoryInterface.php
+++ b/src/TokenStream/ObjectMethodCallerFactoryInterface.php
@@ -5,17 +5,18 @@
* @link https://github.com/nicoSWD
* @author Nicolas Oelgart
*/
+
namespace nicoSWD\Rule\TokenStream;
-use nicoSWD\Rule\Grammar\CallableUserFunctionInterface;
+use nicoSWD\Rule\Grammar\CallableInterface;
use nicoSWD\Rule\TokenStream\Token\BaseToken;
use nicoSWD\Rule\TokenStream\Token\TokenFactory;
-interface CallableUserMethodFactoryInterface
+interface ObjectMethodCallerFactoryInterface
{
/**
* @throws Exception\ForbiddenMethodException
* @throws Exception\UndefinedMethodException
*/
- public function create(BaseToken $token, TokenFactory $tokenFactory, string $methodName): CallableUserFunctionInterface;
+ public function create(BaseToken $token, TokenFactory $tokenFactory, string $methodName): CallableInterface;
}
diff --git a/src/TokenStream/Token/BaseToken.php b/src/TokenStream/Token/BaseToken.php
index f7c2491..938ddc6 100644
--- a/src/TokenStream/Token/BaseToken.php
+++ b/src/TokenStream/Token/BaseToken.php
@@ -1,18 +1,18 @@
-
*/
-namespace nicoSWD\Rule\TokenStream\Token;
-use nicoSWD\Rule\Parser\Exception\ParserException;
-use nicoSWD\Rule\TokenStream\TokenIterator;
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\TokenStream\Token;
abstract class BaseToken
{
- abstract public function getType(): TokenType;
+ abstract public function getKind(): TokenKind;
public function __construct(
private readonly mixed $value,
@@ -35,21 +35,43 @@ public function getOffset(): int
return $this->offset;
}
- /** @throws ParserException */
- public function createNode(TokenIterator $tokenStream): self
+ public function getType(): TokenType
{
- return $this;
+ return match ($this->getKind()) {
+ TokenKind::AND, TokenKind::OR => TokenType::LOGICAL,
+ TokenKind::NOT, TokenKind::NOT_EQUAL, TokenKind::NOT_EQUAL_STRICT,
+ TokenKind::EQUAL, TokenKind::EQUAL_STRICT, TokenKind::IN, TokenKind::NOT_IN,
+ TokenKind::LESS_THAN, TokenKind::LESS_THAN_EQUAL,
+ TokenKind::GREATER, TokenKind::GREATER_EQUAL,
+ TokenKind::PLUS, TokenKind::MINUS,
+ TokenKind::MULTIPLY, TokenKind::DIVIDE, TokenKind::MODULO => TokenType::OPERATOR,
+ TokenKind::OPENING_PARENTHESIS, TokenKind::CLOSING_PARENTHESIS => TokenType::PARENTHESIS,
+ TokenKind::OPENING_ARRAY, TokenKind::CLOSING_ARRAY => TokenType::SQUARE_BRACKET,
+ TokenKind::COMMA => TokenType::COMMA,
+ TokenKind::COMMENT => TokenType::COMMENT,
+ TokenKind::NEWLINE, TokenKind::SPACE => TokenType::SPACE,
+ TokenKind::UNKNOWN => TokenType::UNKNOWN,
+ TokenKind::METHOD => TokenType::METHOD,
+ TokenKind::FUNCTION => TokenType::FUNCTION,
+ TokenKind::VARIABLE => TokenType::VARIABLE,
+ TokenKind::STRING, TokenKind::INTEGER, TokenKind::FLOAT,
+ TokenKind::BOOL_TRUE, TokenKind::BOOL_FALSE, TokenKind::NULL,
+ TokenKind::REGEX, TokenKind::OBJECT, TokenKind::ARRAY => TokenType::VALUE,
+ TokenKind::ENCAPSED_STRING => TokenType::VALUE,
+ };
}
- public function isOfType(TokenType $type): bool
+ public function isOfKind(TokenKind $kind): bool
{
- return $this->getType() === $type;
+ return $this->getKind() === $kind;
}
public function canBeIgnored(): bool
{
- return
- $this->isOfType(TokenType::SPACE) ||
- $this->isOfType(TokenType::COMMENT);
+ $kind = $this->getKind();
+
+ return $kind === TokenKind::SPACE
+ || $kind === TokenKind::NEWLINE
+ || $kind === TokenKind::COMMENT;
}
}
diff --git a/src/TokenStream/Token/GenericToken.php b/src/TokenStream/Token/GenericToken.php
new file mode 100644
index 0000000..c831b04
--- /dev/null
+++ b/src/TokenStream/Token/GenericToken.php
@@ -0,0 +1,49 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\TokenStream\Token;
+
+/**
+ * A generic token class that replaces many single-purpose token classes.
+ * The specific token kind is identified via the TokenKind enum.
+ */
+class GenericToken extends BaseToken
+{
+ public function __construct(
+ private readonly TokenKind $kind,
+ mixed $value,
+ int $offset = 0,
+ ) {
+ parent::__construct($value, $offset);
+ }
+
+ public function getKind(): TokenKind
+ {
+ return $this->kind;
+ }
+
+ /**
+ * @return mixed[]
+ */
+ public function toArray(): array
+ {
+ $items = [];
+
+ foreach ($this->getValue() as $value) {
+ if ($value instanceof BaseToken) {
+ $items[] = $value->getValue();
+ } else {
+ $items[] = $value;
+ }
+ }
+
+ return $items;
+ }
+}
diff --git a/src/TokenStream/Token/TokenAnd.php b/src/TokenStream/Token/TokenAnd.php
deleted file mode 100644
index 8930c80..0000000
--- a/src/TokenStream/Token/TokenAnd.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Token\Type\Logical;
-
-final class TokenAnd extends BaseToken implements Logical
-{
- public function getType(): TokenType
- {
- return TokenType::LOGICAL;
- }
-}
diff --git a/src/TokenStream/Token/TokenArray.php b/src/TokenStream/Token/TokenArray.php
deleted file mode 100644
index e85fe5f..0000000
--- a/src/TokenStream/Token/TokenArray.php
+++ /dev/null
@@ -1,30 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Token\Type\Value;
-
-final class TokenArray extends BaseToken implements Value
-{
- public function getType(): TokenType
- {
- return TokenType::VALUE;
- }
-
- public function toArray(): array
- {
- $items = [];
-
- foreach ($this->getValue() as $value) {
- /** @var self $value */
- $items[] = $value->getValue();
- }
-
- return $items;
- }
-}
diff --git a/src/TokenStream/Token/TokenBool.php b/src/TokenStream/Token/TokenBool.php
index cb84ed6..b89b3e4 100644
--- a/src/TokenStream/Token/TokenBool.php
+++ b/src/TokenStream/Token/TokenBool.php
@@ -1,26 +1,30 @@
-
*/
-namespace nicoSWD\Rule\TokenStream\Token;
-use nicoSWD\Rule\TokenStream\Token\Type\Value;
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\TokenStream\Token;
-abstract class TokenBool extends BaseToken implements Value
+final class TokenBool extends GenericToken
{
- public static function fromBool(bool $bool): TokenBool
- {
- return match ($bool) {
- true => new TokenBoolTrue(true),
- false => new TokenBoolFalse(false),
- };
+ public function __construct(
+ private readonly TokenKind $kind,
+ mixed $value,
+ int $offset = 0,
+ ) {
+ parent::__construct($kind, $value, $offset);
}
- public function getType(): TokenType
+ public static function fromBool(bool $bool): self
{
- return TokenType::VALUE;
+ return new self(
+ $bool ? TokenKind::BOOL_TRUE : TokenKind::BOOL_FALSE,
+ $bool,
+ );
}
}
diff --git a/src/TokenStream/Token/TokenBoolFalse.php b/src/TokenStream/Token/TokenBoolFalse.php
deleted file mode 100644
index 3c3ccc4..0000000
--- a/src/TokenStream/Token/TokenBoolFalse.php
+++ /dev/null
@@ -1,16 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-final class TokenBoolFalse extends TokenBool
-{
- public function getValue(): bool
- {
- return false;
- }
-}
diff --git a/src/TokenStream/Token/TokenClosingArray.php b/src/TokenStream/Token/TokenClosingArray.php
deleted file mode 100644
index ff0d105..0000000
--- a/src/TokenStream/Token/TokenClosingArray.php
+++ /dev/null
@@ -1,16 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-final class TokenClosingArray extends BaseToken
-{
- public function getType(): TokenType
- {
- return TokenType::SQUARE_BRACKET;
- }
-}
diff --git a/src/TokenStream/Token/TokenClosingParenthesis.php b/src/TokenStream/Token/TokenClosingParenthesis.php
deleted file mode 100644
index 9ee93f4..0000000
--- a/src/TokenStream/Token/TokenClosingParenthesis.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Token\Type\Parenthesis;
-
-final class TokenClosingParenthesis extends BaseToken implements Parenthesis
-{
- public function getType(): TokenType
- {
- return TokenType::PARENTHESIS;
- }
-}
diff --git a/src/TokenStream/Token/TokenComma.php b/src/TokenStream/Token/TokenComma.php
deleted file mode 100644
index fc1ce81..0000000
--- a/src/TokenStream/Token/TokenComma.php
+++ /dev/null
@@ -1,16 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-final class TokenComma extends BaseToken
-{
- public function getType(): TokenType
- {
- return TokenType::COMMA;
- }
-}
diff --git a/src/TokenStream/Token/TokenComment.php b/src/TokenStream/Token/TokenComment.php
deleted file mode 100644
index 1866e56..0000000
--- a/src/TokenStream/Token/TokenComment.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Token\Type\Whitespace;
-
-final class TokenComment extends BaseToken implements Whitespace
-{
- public function getType(): TokenType
- {
- return TokenType::COMMENT;
- }
-}
diff --git a/src/TokenStream/Token/TokenEncapsedString.php b/src/TokenStream/Token/TokenEncapsedString.php
index 962072d..5af8a00 100644
--- a/src/TokenStream/Token/TokenEncapsedString.php
+++ b/src/TokenStream/Token/TokenEncapsedString.php
@@ -1,16 +1,62 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\TokenStream\Token;
final class TokenEncapsedString extends TokenString
{
+ public function getKind(): TokenKind
+ {
+ return TokenKind::ENCAPSED_STRING;
+ }
+
public function getValue(): string
{
- return substr(parent::getValue(), 1, -1);
+ $value = substr(parent::getValue(), 1, -1);
+
+ return $this->unescape($value);
+ }
+
+ /**
+ * Unescape common escape sequences in the string content.
+ *
+ * Handles the same sequences as JavaScript string literals:
+ * \n, \r, \t, \\, \", \', \$, \0
+ */
+ private function unescape(string $value): string
+ {
+ $result = '';
+ $length = strlen($value);
+
+ for ($i = 0; $i < $length; $i++) {
+ if ($value[$i] === '\\' && $i + 1 < $length) {
+ $next = $value[$i + 1];
+
+ $result .= match ($next) {
+ 'n' => "\n",
+ 'r' => "\r",
+ 't' => "\t",
+ '\\' => '\\',
+ '"' => '"',
+ "'" => "'",
+ '$' => '$',
+ '0' => "\0",
+ default => '\\' . $next,
+ };
+
+ $i++; // skip the escaped character
+ } else {
+ $result .= $value[$i];
+ }
+ }
+
+ return $result;
}
}
diff --git a/src/TokenStream/Token/TokenEqual.php b/src/TokenStream/Token/TokenEqual.php
deleted file mode 100644
index 712da78..0000000
--- a/src/TokenStream/Token/TokenEqual.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Token\Type\Operator;
-
-final class TokenEqual extends BaseToken implements Operator
-{
- public function getType(): TokenType
- {
- return TokenType::OPERATOR;
- }
-}
diff --git a/src/TokenStream/Token/TokenEqualStrict.php b/src/TokenStream/Token/TokenEqualStrict.php
deleted file mode 100644
index 5f6c687..0000000
--- a/src/TokenStream/Token/TokenEqualStrict.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Token\Type\Operator;
-
-final class TokenEqualStrict extends BaseToken implements Operator
-{
- public function getType(): TokenType
- {
- return TokenType::OPERATOR;
- }
-}
diff --git a/src/TokenStream/Token/TokenFactory.php b/src/TokenStream/Token/TokenFactory.php
index 80e16b9..5a19e8f 100644
--- a/src/TokenStream/Token/TokenFactory.php
+++ b/src/TokenStream/Token/TokenFactory.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\TokenStream\Token;
use nicoSWD\Rule\Parser\Exception\ParserException;
@@ -16,65 +19,36 @@ class TokenFactory
public function createFromPHPType(mixed $value): BaseToken
{
return match (gettype($value)) {
- 'string' => new TokenString($value),
- 'integer' => new TokenInteger($value),
- 'boolean' => TokenBool::fromBool($value),
- 'NULL' => new TokenNull($value),
- 'double' => new TokenFloat($value),
- 'object' => new TokenObject($value),
+ 'string' => new GenericToken(TokenKind::STRING, $value),
+ 'integer' => new GenericToken(TokenKind::INTEGER, $value),
+ 'boolean' => new GenericToken($value ? TokenKind::BOOL_TRUE : TokenKind::BOOL_FALSE, $value),
+ 'NULL' => new GenericToken(TokenKind::NULL, null),
+ 'double' => new GenericToken(TokenKind::FLOAT, $value),
+ 'object' => new GenericToken(TokenKind::OBJECT, $value),
'array' => $this->buildTokenCollection($value),
default => throw ParserException::unsupportedType(gettype($value)),
};
}
- public function createFromToken(Token $token, array $matches, int $offset): BaseToken
+ public function createFromToken(TokenKind $kind, array $matches, int $offset): BaseToken
{
- $args = [$matches[$token->value], $offset];
+ $args = [$matches[$kind->value], $offset];
- return match ($token) {
- Token::AND => new TokenAnd(...$args),
- Token::OR => new TokenOr(...$args),
- Token::NOT_EQUAL_STRICT => new TokenNotEqualStrict(...$args),
- Token::NOT_EQUAL => new TokenNotEqual(...$args),
- Token::EQUAL_STRICT => new TokenEqualStrict(...$args),
- Token::EQUAL => new TokenEqual(...$args),
- Token::IN => new TokenIn(...$args),
- Token::NOT_IN => new TokenNotIn(...$args),
- Token::BOOL_TRUE => new TokenBoolTrue(...$args),
- Token::BOOL_FALSE => new TokenBoolFalse(...$args),
- Token::NULL => new TokenNull(...$args),
- Token::METHOD => new TokenMethod(...$args),
- Token::FUNCTION => new TokenFunction(...$args),
- Token::VARIABLE => new TokenVariable(...$args),
- Token::FLOAT => new TokenFloat(...$args),
- Token::INTEGER => new TokenInteger(...$args),
- Token::ENCAPSED_STRING => new TokenEncapsedString(...$args),
- Token::SMALLER_EQUAL => new TokenSmallerEqual(...$args),
- Token::GREATER_EQUAL => new TokenGreaterEqual(...$args),
- Token::SMALLER => new TokenSmaller(...$args),
- Token::GREATER => new TokenGreater(...$args),
- Token::OPENING_PARENTHESIS => new TokenOpeningParenthesis(...$args),
- Token::CLOSING_PARENTHESIS => new TokenClosingParenthesis(...$args),
- Token::OPENING_ARRAY => new TokenOpeningArray(...$args),
- Token::CLOSING_ARRAY => new TokenClosingArray(...$args),
- Token::COMMA => new TokenComma(...$args),
- Token::REGEX => new TokenRegex(...$args),
- Token::COMMENT => new TokenComment(...$args),
- Token::NEWLINE => new TokenNewline(...$args),
- Token::SPACE => new TokenSpace(...$args),
- Token::UNKNOWN => new TokenUnknown(...$args),
+ return match ($kind) {
+ TokenKind::ENCAPSED_STRING => new TokenEncapsedString(...$args),
+ default => new GenericToken($kind, ...$args),
};
}
/** @throws ParserException */
- private function buildTokenCollection(array $items): TokenArray
+ private function buildTokenCollection(array $items): GenericToken
{
$tokenCollection = new TokenCollection();
foreach ($items as $item) {
- $tokenCollection->attach($this->createFromPHPType($item));
+ $tokenCollection->add($this->createFromPHPType($item));
}
- return new TokenArray($tokenCollection);
+ return new GenericToken(TokenKind::ARRAY, $tokenCollection);
}
}
diff --git a/src/TokenStream/Token/TokenFloat.php b/src/TokenStream/Token/TokenFloat.php
deleted file mode 100644
index eba6bd0..0000000
--- a/src/TokenStream/Token/TokenFloat.php
+++ /dev/null
@@ -1,23 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Token\Type\Value;
-
-final class TokenFloat extends BaseToken implements Value
-{
- public function getType(): TokenType
- {
- return TokenType::VALUE;
- }
-
- public function getValue(): float
- {
- return (float) parent::getValue();
- }
-}
diff --git a/src/TokenStream/Token/TokenFunction.php b/src/TokenStream/Token/TokenFunction.php
deleted file mode 100644
index 7d27e80..0000000
--- a/src/TokenStream/Token/TokenFunction.php
+++ /dev/null
@@ -1,24 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Node\NodeFunction;
-use nicoSWD\Rule\TokenStream\TokenIterator;
-
-final class TokenFunction extends BaseToken
-{
- public function getType(): TokenType
- {
- return TokenType::FUNCTION;
- }
-
- public function createNode(TokenIterator $tokenStream): BaseToken
- {
- return (new NodeFunction($tokenStream))->getNode();
- }
-}
diff --git a/src/TokenStream/Token/TokenGreater.php b/src/TokenStream/Token/TokenGreater.php
deleted file mode 100644
index 07920bb..0000000
--- a/src/TokenStream/Token/TokenGreater.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Token\Type\Operator;
-
-final class TokenGreater extends BaseToken implements Operator
-{
- public function getType(): TokenType
- {
- return TokenType::OPERATOR;
- }
-}
diff --git a/src/TokenStream/Token/TokenGreaterEqual.php b/src/TokenStream/Token/TokenGreaterEqual.php
deleted file mode 100644
index 4e01558..0000000
--- a/src/TokenStream/Token/TokenGreaterEqual.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Token\Type\Operator;
-
-final class TokenGreaterEqual extends BaseToken implements Operator
-{
- public function getType(): TokenType
- {
- return TokenType::OPERATOR;
- }
-}
diff --git a/src/TokenStream/Token/TokenIn.php b/src/TokenStream/Token/TokenIn.php
deleted file mode 100644
index 43a2f34..0000000
--- a/src/TokenStream/Token/TokenIn.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Token\Type\Operator;
-
-final class TokenIn extends BaseToken implements Operator
-{
- public function getType(): TokenType
- {
- return TokenType::OPERATOR;
- }
-}
diff --git a/src/TokenStream/Token/TokenInteger.php b/src/TokenStream/Token/TokenInteger.php
deleted file mode 100644
index 3f1f281..0000000
--- a/src/TokenStream/Token/TokenInteger.php
+++ /dev/null
@@ -1,23 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Token\Type\Value;
-
-final class TokenInteger extends BaseToken implements Value
-{
- public function getType(): TokenType
- {
- return TokenType::VALUE;
- }
-
- public function getValue(): int
- {
- return (int) parent::getValue();
- }
-}
diff --git a/src/TokenStream/Token/Token.php b/src/TokenStream/Token/TokenKind.php
similarity index 70%
rename from src/TokenStream/Token/Token.php
rename to src/TokenStream/Token/TokenKind.php
index 95dbb2b..cc0b33c 100644
--- a/src/TokenStream/Token/Token.php
+++ b/src/TokenStream/Token/TokenKind.php
@@ -1,16 +1,24 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\TokenStream\Token;
-enum Token: string
+/**
+ * Identifies the specific kind of a token, replacing the need for
+ * dozens of separate token classes.
+ */
+enum TokenKind: string
{
case AND = 'And';
case OR = 'Or';
+ case NOT = 'Not';
case NOT_EQUAL_STRICT = 'NotEqualStrict';
case NOT_EQUAL = 'NotEqual';
case EQUAL_STRICT = 'EqualStrict';
@@ -26,9 +34,14 @@ enum Token: string
case FLOAT = 'Float';
case INTEGER = 'Integer';
case ENCAPSED_STRING = 'EncapsedString';
- case SMALLER_EQUAL = 'SmallerEqual';
+ case LESS_THAN_EQUAL = 'SmallerEqual';
case GREATER_EQUAL = 'GreaterEqual';
- case SMALLER = 'Smaller';
+ case LESS_THAN = 'Smaller';
+ case PLUS = 'Plus';
+ case MINUS = 'Minus';
+ case MULTIPLY = 'Multiply';
+ case DIVIDE = 'Divide';
+ case MODULO = 'Modulo';
case GREATER = 'Greater';
case OPENING_PARENTHESIS = 'OpeningParentheses';
case CLOSING_PARENTHESIS = 'ClosingParentheses';
@@ -40,4 +53,7 @@ enum Token: string
case NEWLINE = 'Newline';
case SPACE = 'Space';
case UNKNOWN = 'Unknown';
+ case STRING = 'String';
+ case OBJECT = 'Object';
+ case ARRAY = 'Array';
}
diff --git a/src/TokenStream/Token/TokenMethod.php b/src/TokenStream/Token/TokenMethod.php
index 945fe27..68fb7b6 100644
--- a/src/TokenStream/Token/TokenMethod.php
+++ b/src/TokenStream/Token/TokenMethod.php
@@ -1,18 +1,19 @@
-
*/
-namespace nicoSWD\Rule\TokenStream\Token;
-use nicoSWD\Rule\TokenStream\Token\Type\Method;
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\TokenStream\Token;
-final class TokenMethod extends BaseToken implements Method
+final class TokenMethod extends BaseToken
{
- public function getType(): TokenType
+ public function getKind(): TokenKind
{
- return TokenType::METHOD;
+ return TokenKind::METHOD;
}
}
diff --git a/src/TokenStream/Token/TokenNewline.php b/src/TokenStream/Token/TokenNewline.php
deleted file mode 100644
index 71db1ed..0000000
--- a/src/TokenStream/Token/TokenNewline.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Token\Type\Whitespace;
-
-final class TokenNewline extends BaseToken implements Whitespace
-{
- public function getType(): TokenType
- {
- return TokenType::SPACE;
- }
-}
diff --git a/src/TokenStream/Token/TokenNotEqual.php b/src/TokenStream/Token/TokenNotEqual.php
deleted file mode 100644
index 2ec6f8e..0000000
--- a/src/TokenStream/Token/TokenNotEqual.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Token\Type\Operator;
-
-final class TokenNotEqual extends BaseToken implements Operator
-{
- public function getType(): TokenType
- {
- return TokenType::OPERATOR;
- }
-}
diff --git a/src/TokenStream/Token/TokenNotEqualStrict.php b/src/TokenStream/Token/TokenNotEqualStrict.php
deleted file mode 100644
index 1949e7c..0000000
--- a/src/TokenStream/Token/TokenNotEqualStrict.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Token\Type\Operator;
-
-final class TokenNotEqualStrict extends BaseToken implements Operator
-{
- public function getType(): TokenType
- {
- return TokenType::OPERATOR;
- }
-}
diff --git a/src/TokenStream/Token/TokenNotIn.php b/src/TokenStream/Token/TokenNotIn.php
deleted file mode 100644
index 813abef..0000000
--- a/src/TokenStream/Token/TokenNotIn.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Token\Type\Operator;
-
-final class TokenNotIn extends BaseToken implements Operator
-{
- public function getType(): TokenType
- {
- return TokenType::OPERATOR;
- }
-}
diff --git a/src/TokenStream/Token/TokenNull.php b/src/TokenStream/Token/TokenNull.php
deleted file mode 100644
index b8f4dba..0000000
--- a/src/TokenStream/Token/TokenNull.php
+++ /dev/null
@@ -1,23 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Token\Type\Value;
-
-final class TokenNull extends BaseToken implements Value
-{
- public function getType(): TokenType
- {
- return TokenType::VALUE;
- }
-
- public function getValue(): mixed
- {
- return null;
- }
-}
diff --git a/src/TokenStream/Token/TokenObject.php b/src/TokenStream/Token/TokenObject.php
deleted file mode 100644
index 619c72c..0000000
--- a/src/TokenStream/Token/TokenObject.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Token\Type\Value;
-
-final class TokenObject extends BaseToken implements Value
-{
- public function getType(): TokenType
- {
- return TokenType::VALUE;
- }
-}
diff --git a/src/TokenStream/Token/TokenOpeningArray.php b/src/TokenStream/Token/TokenOpeningArray.php
deleted file mode 100644
index 991b2a7..0000000
--- a/src/TokenStream/Token/TokenOpeningArray.php
+++ /dev/null
@@ -1,24 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Node\NodeArray;
-use nicoSWD\Rule\TokenStream\TokenIterator;
-
-final class TokenOpeningArray extends BaseToken
-{
- public function getType(): TokenType
- {
- return TokenType::SQUARE_BRACKET;
- }
-
- public function createNode(TokenIterator $tokenStream): BaseToken
- {
- return (new NodeArray($tokenStream))->getNode();
- }
-}
diff --git a/src/TokenStream/Token/TokenOpeningParenthesis.php b/src/TokenStream/Token/TokenOpeningParenthesis.php
deleted file mode 100644
index f180b46..0000000
--- a/src/TokenStream/Token/TokenOpeningParenthesis.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Token\Type\Parenthesis;
-
-final class TokenOpeningParenthesis extends BaseToken implements Parenthesis
-{
- public function getType(): TokenType
- {
- return TokenType::PARENTHESIS;
- }
-}
diff --git a/src/TokenStream/Token/TokenOr.php b/src/TokenStream/Token/TokenOr.php
deleted file mode 100644
index 8d2c446..0000000
--- a/src/TokenStream/Token/TokenOr.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Token\Type\Logical;
-
-final class TokenOr extends BaseToken implements Logical
-{
- public function getType(): TokenType
- {
- return TokenType::LOGICAL;
- }
-}
diff --git a/src/TokenStream/Token/TokenRegex.php b/src/TokenStream/Token/TokenRegex.php
deleted file mode 100644
index c8e2f7c..0000000
--- a/src/TokenStream/Token/TokenRegex.php
+++ /dev/null
@@ -1,25 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Node\NodeString;
-use nicoSWD\Rule\TokenStream\Token\Type\Value;
-use nicoSWD\Rule\TokenStream\TokenIterator;
-
-final class TokenRegex extends BaseToken implements Value
-{
- public function getType(): TokenType
- {
- return TokenType::VALUE;
- }
-
- public function createNode(TokenIterator $tokenStream): BaseToken
- {
- return (new NodeString($tokenStream))->getNode();
- }
-}
diff --git a/src/TokenStream/Token/TokenSmaller.php b/src/TokenStream/Token/TokenSmaller.php
deleted file mode 100644
index 9be3dee..0000000
--- a/src/TokenStream/Token/TokenSmaller.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Token\Type\Operator;
-
-final class TokenSmaller extends BaseToken implements Operator
-{
- public function getType(): TokenType
- {
- return TokenType::OPERATOR;
- }
-}
diff --git a/src/TokenStream/Token/TokenSmallerEqual.php b/src/TokenStream/Token/TokenSmallerEqual.php
deleted file mode 100644
index cf79a31..0000000
--- a/src/TokenStream/Token/TokenSmallerEqual.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Token\Type\Operator;
-
-final class TokenSmallerEqual extends BaseToken implements Operator
-{
- public function getType(): TokenType
- {
- return TokenType::OPERATOR;
- }
-}
diff --git a/src/TokenStream/Token/TokenSpace.php b/src/TokenStream/Token/TokenSpace.php
deleted file mode 100644
index 6e2279c..0000000
--- a/src/TokenStream/Token/TokenSpace.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Token\Type\Whitespace;
-
-final class TokenSpace extends BaseToken implements Whitespace
-{
- public function getType(): TokenType
- {
- return TokenType::SPACE;
- }
-}
diff --git a/src/TokenStream/Token/TokenString.php b/src/TokenStream/Token/TokenString.php
index cc0459f..93a4a9a 100644
--- a/src/TokenStream/Token/TokenString.php
+++ b/src/TokenStream/Token/TokenString.php
@@ -1,25 +1,19 @@
-
*/
-namespace nicoSWD\Rule\TokenStream\Token;
-use nicoSWD\Rule\TokenStream\Node\NodeString;
-use nicoSWD\Rule\TokenStream\Token\Type\Value;
-use nicoSWD\Rule\TokenStream\TokenIterator;
+declare(strict_types=1);
-class TokenString extends BaseToken implements Value
-{
- public function getType(): TokenType
- {
- return TokenType::VALUE;
- }
+namespace nicoSWD\Rule\TokenStream\Token;
- public function createNode(TokenIterator $tokenStream): BaseToken
+class TokenString extends BaseToken
+{
+ public function getKind(): TokenKind
{
- return (new NodeString($tokenStream))->getNode();
+ return TokenKind::STRING;
}
}
diff --git a/src/TokenStream/Token/TokenType.php b/src/TokenStream/Token/TokenType.php
index 3f58cf5..1c3fd80 100644
--- a/src/TokenStream/Token/TokenType.php
+++ b/src/TokenStream/Token/TokenType.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\TokenStream\Token;
enum TokenType
diff --git a/src/TokenStream/Token/TokenUnknown.php b/src/TokenStream/Token/TokenUnknown.php
deleted file mode 100644
index 367ad21..0000000
--- a/src/TokenStream/Token/TokenUnknown.php
+++ /dev/null
@@ -1,16 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-final class TokenUnknown extends BaseToken
-{
- public function getType(): TokenType
- {
- return TokenType::UNKNOWN;
- }
-}
diff --git a/src/TokenStream/Token/TokenVariable.php b/src/TokenStream/Token/TokenVariable.php
deleted file mode 100644
index f390a38..0000000
--- a/src/TokenStream/Token/TokenVariable.php
+++ /dev/null
@@ -1,25 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token;
-
-use nicoSWD\Rule\TokenStream\Node\NodeVariable;
-use nicoSWD\Rule\TokenStream\Token\Type\Value;
-use nicoSWD\Rule\TokenStream\TokenIterator;
-
-final class TokenVariable extends BaseToken implements Value
-{
- public function getType(): TokenType
- {
- return TokenType::VARIABLE;
- }
-
- public function createNode(TokenIterator $tokenStream): BaseToken
- {
- return (new NodeVariable($tokenStream))->getNode();
- }
-}
diff --git a/src/TokenStream/Token/Type/Logical.php b/src/TokenStream/Token/Type/Logical.php
deleted file mode 100644
index aa49492..0000000
--- a/src/TokenStream/Token/Type/Logical.php
+++ /dev/null
@@ -1,12 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token\Type;
-
-interface Logical
-{
-}
diff --git a/src/TokenStream/Token/Type/Operator.php b/src/TokenStream/Token/Type/Operator.php
deleted file mode 100644
index ca1667f..0000000
--- a/src/TokenStream/Token/Type/Operator.php
+++ /dev/null
@@ -1,12 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token\Type;
-
-interface Operator
-{
-}
diff --git a/src/TokenStream/Token/Type/Parenthesis.php b/src/TokenStream/Token/Type/Parenthesis.php
deleted file mode 100644
index 865a064..0000000
--- a/src/TokenStream/Token/Type/Parenthesis.php
+++ /dev/null
@@ -1,12 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token\Type;
-
-interface Parenthesis
-{
-}
diff --git a/src/TokenStream/Token/Type/Value.php b/src/TokenStream/Token/Type/Value.php
deleted file mode 100644
index 54d7888..0000000
--- a/src/TokenStream/Token/Type/Value.php
+++ /dev/null
@@ -1,12 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token\Type;
-
-interface Value
-{
-}
diff --git a/src/TokenStream/Token/Type/Whitespace.php b/src/TokenStream/Token/Type/Whitespace.php
deleted file mode 100644
index 64bb32a..0000000
--- a/src/TokenStream/Token/Type/Whitespace.php
+++ /dev/null
@@ -1,12 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream\Token\Type;
-
-interface Whitespace
-{
-}
diff --git a/src/TokenStream/TokenCollection.php b/src/TokenStream/TokenCollection.php
index 1ba867e..7642b76 100644
--- a/src/TokenStream/TokenCollection.php
+++ b/src/TokenStream/TokenCollection.php
@@ -1,25 +1,32 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\TokenStream;
use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use SplObjectStorage;
-final class TokenCollection extends SplObjectStorage
+final class TokenCollection extends \ArrayObject
{
public function current(): BaseToken
{
/** @var BaseToken $token */
- $token = parent::current();
+ $token = $this->offsetGet($this->key());
return $token;
}
+ public function add(BaseToken $token): void
+ {
+ $this->append($token);
+ }
+
public function toArray(): array
{
$items = [];
diff --git a/src/TokenStream/TokenIterator.php b/src/TokenStream/TokenIterator.php
index 71e5f60..74a8f53 100644
--- a/src/TokenStream/TokenIterator.php
+++ b/src/TokenStream/TokenIterator.php
@@ -1,23 +1,22 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\TokenStream;
-use Closure;
use Iterator;
-use nicoSWD\Rule\Parser\Exception\ParserException;
-use nicoSWD\Rule\Grammar\CallableUserFunctionInterface;
use nicoSWD\Rule\TokenStream\Token\BaseToken;
-readonly class TokenIterator implements Iterator
+class TokenIterator implements Iterator
{
public function __construct(
private Iterator $stack,
- private TokenStream $tokenStream,
) {
}
@@ -31,10 +30,9 @@ public function valid(): bool
return $this->stack->valid();
}
- /** @throws ParserException */
public function current(): BaseToken
{
- return $this->getCurrentToken()->createNode($this);
+ return $this->stack->current();
}
public function key(): int
@@ -46,47 +44,4 @@ public function rewind(): void
{
$this->stack->rewind();
}
-
- /** @return Iterator */
- public function getStack(): Iterator
- {
- return $this->stack;
- }
-
- private function getCurrentToken(): BaseToken
- {
- return $this->stack->current();
- }
-
- /** @throws ParserException */
- public function getVariable(string $variableName): BaseToken
- {
- try {
- return $this->tokenStream->getVariable($variableName);
- } catch (Exception\UndefinedVariableException) {
- throw ParserException::undefinedVariable($variableName, $this->getCurrentToken());
- }
- }
-
- /** @throws ParserException */
- public function getFunction(string $functionName): Closure
- {
- try {
- return $this->tokenStream->getFunction($functionName);
- } catch (Exception\UndefinedFunctionException) {
- throw ParserException::undefinedFunction($functionName, $this->getCurrentToken());
- }
- }
-
- /** @throws ParserException */
- public function getMethod(string $methodName, BaseToken $token): CallableUserFunctionInterface
- {
- try {
- return $this->tokenStream->getMethod($methodName, $token);
- } catch (Exception\UndefinedMethodException) {
- throw ParserException::undefinedMethod($methodName, $this->getCurrentToken());
- } catch (Exception\ForbiddenMethodException) {
- throw ParserException::forbiddenMethod($methodName, $this->getCurrentToken());
- }
- }
}
diff --git a/src/TokenStream/TokenIteratorFactory.php b/src/TokenStream/TokenIteratorFactory.php
deleted file mode 100644
index 18c6dc8..0000000
--- a/src/TokenStream/TokenIteratorFactory.php
+++ /dev/null
@@ -1,18 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream;
-
-use Iterator;
-
-final class TokenIteratorFactory
-{
- public function create(Iterator $stack, TokenStream $tokenStream): TokenIterator
- {
- return new TokenIterator($stack, $tokenStream);
- }
-}
diff --git a/src/TokenStream/TokenStream.php b/src/TokenStream/TokenStream.php
deleted file mode 100644
index d00dcca..0000000
--- a/src/TokenStream/TokenStream.php
+++ /dev/null
@@ -1,138 +0,0 @@
-
- */
-namespace nicoSWD\Rule\TokenStream;
-
-use Closure;
-use InvalidArgumentException;
-use nicoSWD\Rule\Grammar\CallableUserFunctionInterface;
-use nicoSWD\Rule\Parser\Exception\ParserException;
-use nicoSWD\Rule\TokenStream\Exception\UndefinedVariableException;
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\TokenFactory;
-use nicoSWD\Rule\Tokenizer\TokenizerInterface;
-use nicoSWD\Rule\TokenStream\Token\TokenObject;
-
-class TokenStream
-{
- private array $functions = [];
- private array $methods = [];
- private array $variables = [];
-
- public function __construct(
- private readonly TokenizerInterface $tokenizer,
- private readonly TokenFactory $tokenFactory,
- private readonly TokenIteratorFactory $tokenIteratorFactory,
- private readonly CallableUserMethodFactoryInterface $userMethodFactory,
- ) {
- }
-
- public function getStream(string $rule): TokenIterator
- {
- return $this->tokenIteratorFactory->create($this->tokenizer->tokenize($rule), $this);
- }
-
- /**
- * @throws Exception\UndefinedMethodException
- * @throws Exception\ForbiddenMethodException
- */
- public function getMethod(string $methodName, BaseToken $token): CallableUserFunctionInterface
- {
- if ($token instanceof TokenObject) {
- return $this->getCallableUserMethod($token, $methodName);
- }
-
- if (empty($this->methods)) {
- $this->registerMethods();
- }
-
- if (!isset($this->methods[$methodName])) {
- throw new Exception\UndefinedMethodException();
- }
-
- return new $this->methods[$methodName]($token);
- }
-
- public function setVariables(array $variables): void
- {
- $this->variables = $variables;
- }
-
- /**
- * @throws UndefinedVariableException
- * @throws ParserException
- */
- public function getVariable(string $variableName): BaseToken
- {
- if (!$this->variableExists($variableName)) {
- throw new UndefinedVariableException($variableName);
- }
-
- return $this->tokenFactory->createFromPHPType($this->variables[$variableName]);
- }
-
- public function variableExists(string $variableName): bool
- {
- return array_key_exists($variableName, $this->variables);
- }
-
- /** @throws Exception\UndefinedFunctionException */
- public function getFunction(string $functionName): Closure
- {
- if (empty($this->functions)) {
- $this->registerFunctions();
- }
-
- if (!isset($this->functions[$functionName])) {
- throw new Exception\UndefinedFunctionException($functionName);
- }
-
- return $this->functions[$functionName];
- }
-
- private function registerMethods(): void
- {
- foreach ($this->tokenizer->grammar->getInternalMethods() as $internalMethod) {
- $this->methods[$internalMethod->name] = $internalMethod->class;
- }
- }
-
- private function registerFunctions(): void
- {
- foreach ($this->tokenizer->grammar->getInternalFunctions() as $function) {
- $this->registerFunctionClass($function->name, $function->class);
- }
- }
-
- private function registerFunctionClass(string $functionName, string $className): void
- {
- $this->functions[$functionName] = function (?BaseToken ...$args) use ($className) {
- $function = new $className();
-
- if (!$function instanceof CallableUserFunctionInterface) {
- throw new InvalidArgumentException(
- sprintf(
- '%s must be an instance of %s',
- $className,
- CallableUserFunctionInterface::class
- )
- );
- }
-
- return $function->call(...$args);
- };
- }
-
- /**
- * @throws Exception\ForbiddenMethodException
- * @throws Exception\UndefinedMethodException
- */
- private function getCallableUserMethod(BaseToken $token, string $methodName): CallableUserFunctionInterface
- {
- return $this->userMethodFactory->create($token, $this->tokenFactory, $methodName);
- }
-}
diff --git a/src/TokenStream/VariableRegistry.php b/src/TokenStream/VariableRegistry.php
new file mode 100644
index 0000000..7bba092
--- /dev/null
+++ b/src/TokenStream/VariableRegistry.php
@@ -0,0 +1,52 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\TokenStream;
+
+use nicoSWD\Rule\Parser\Exception\ParserException;
+use nicoSWD\Rule\TokenStream\Exception\UndefinedVariableException;
+use nicoSWD\Rule\TokenStream\Token\BaseToken;
+use nicoSWD\Rule\TokenStream\Token\TokenFactory;
+
+class VariableRegistry
+{
+ /** @param array $variables */
+ public function __construct(
+ private array $variables,
+ private readonly TokenFactory $tokenFactory,
+ ) {
+ }
+
+ /**
+ * @param array $variables
+ */
+ public function setVariables(array $variables): void
+ {
+ $this->variables = $variables;
+ }
+
+ /**
+ * @throws UndefinedVariableException
+ * @throws ParserException
+ */
+ public function get(string $variableName): BaseToken
+ {
+ if (!$this->exists($variableName)) {
+ throw new UndefinedVariableException($variableName);
+ }
+
+ return $this->tokenFactory->createFromPHPType($this->variables[$variableName]);
+ }
+
+ public function exists(string $variableName): bool
+ {
+ return array_key_exists($variableName, $this->variables);
+ }
+}
diff --git a/src/Tokenizer/Tokenizer.php b/src/Tokenizer/Tokenizer.php
deleted file mode 100644
index fe2af69..0000000
--- a/src/Tokenizer/Tokenizer.php
+++ /dev/null
@@ -1,49 +0,0 @@
-
- */
-namespace nicoSWD\Rule\Tokenizer;
-
-use ArrayIterator;
-use Iterator;
-use nicoSWD\Rule\Grammar\Grammar;
-use nicoSWD\Rule\TokenStream\Token\Token;
-use nicoSWD\Rule\TokenStream\Token\TokenFactory;
-
-final class Tokenizer extends TokenizerInterface
-{
- public function __construct(
- public Grammar $grammar,
- private readonly TokenFactory $tokenFactory,
- ) {
- }
-
- public function tokenize(string $string): Iterator
- {
- $regex = $this->grammar->buildRegex();
- $stack = [];
- $offset = 0;
-
- while (preg_match($regex, $string, $matches, offset: $offset)) {
- $token = $this->getMatchedToken($matches);
- $stack[] = $this->tokenFactory->createFromToken($token, $matches, $offset);
- $offset += strlen($matches[0]);
- }
-
- return new ArrayIterator($stack);
- }
-
- private function getMatchedToken(array $matches): Token
- {
- foreach ($matches as $key => $value) {
- if ($value !== '' && !is_int($key)) {
- return Token::from($key);
- }
- }
-
- return Token::UNKNOWN;
- }
-}
diff --git a/src/container.php b/src/container.php
deleted file mode 100644
index 7bf78c7..0000000
--- a/src/container.php
+++ /dev/null
@@ -1,119 +0,0 @@
-
- */
-namespace nicoSWD\Rule;
-
-use nicoSWD\Rule\Grammar\JavaScript\JavaScript;
-use nicoSWD\Rule\Parser\EvaluatableExpressionFactory;
-use nicoSWD\Rule\TokenStream\TokenStream;
-use nicoSWD\Rule\Compiler\CompilerFactory;
-use nicoSWD\Rule\Evaluator\Evaluator;
-use nicoSWD\Rule\Evaluator\EvaluatorInterface;
-use nicoSWD\Rule\Tokenizer\Tokenizer;
-use nicoSWD\Rule\TokenStream\Token\TokenFactory;
-use nicoSWD\Rule\TokenStream\TokenIteratorFactory;
-use nicoSWD\Rule\TokenStream\CallableUserMethodFactory;
-
-return new class {
- private static TokenIteratorFactory $tokenStreamFactory;
- private static TokenFactory $tokenFactory;
- private static CompilerFactory $compiler;
- private static JavaScript $javaScript;
- private static EvaluatableExpressionFactory $expressionFactory;
- private static CallableUserMethodFactory $userMethodFactory;
- private static Tokenizer $tokenizer;
- private static Evaluator $evaluator;
-
- public function parser(array $variables): Parser\Parser
- {
- return new Parser\Parser(
- self::ast($variables),
- self::expressionFactory(),
- self::compiler()
- );
- }
-
- public function evaluator(): EvaluatorInterface
- {
- if (!isset(self::$evaluator)) {
- self::$evaluator = new Evaluator();
- }
-
- return self::$evaluator;
- }
-
- private static function tokenFactory(): TokenFactory
- {
- if (!isset(self::$tokenFactory)) {
- self::$tokenFactory = new TokenFactory();
- }
-
- return self::$tokenFactory;
- }
-
- private static function compiler(): CompilerFactory
- {
- if (!isset(self::$compiler)) {
- self::$compiler = new CompilerFactory();
- }
-
- return self::$compiler;
- }
-
- private static function ast(array $variables): TokenStream
- {
- $tokenStream = new TokenStream(self::tokenizer(), self::tokenFactory(), self::tokenStreamFactory(), self::userMethodFactory());
- $tokenStream->setVariables($variables);
-
- return $tokenStream;
- }
-
- private static function tokenizer(): Tokenizer
- {
- if (!isset(self::$tokenizer)) {
- self::$tokenizer = new Tokenizer(self::javascript(), self::tokenFactory());
- }
-
- return self::$tokenizer;
- }
-
- private static function javascript(): JavaScript
- {
- if (!isset(self::$javaScript)) {
- self::$javaScript = new JavaScript();
- }
-
- return self::$javaScript;
- }
-
- private static function tokenStreamFactory(): TokenIteratorFactory
- {
- if (!isset(self::$tokenStreamFactory)) {
- self::$tokenStreamFactory = new TokenIteratorFactory();
- }
-
- return self::$tokenStreamFactory;
- }
-
- private static function expressionFactory(): EvaluatableExpressionFactory
- {
- if (!isset(self::$expressionFactory)) {
- self::$expressionFactory = new EvaluatableExpressionFactory();
- }
-
- return self::$expressionFactory;
- }
-
- private static function userMethodFactory(): CallableUserMethodFactory
- {
- if (!isset(self::$userMethodFactory)) {
- self::$userMethodFactory = new CallableUserMethodFactory();
- }
-
- return self::$userMethodFactory;
- }
-};
diff --git a/tests/integration/AbstractTestBase.php b/tests/integration/AbstractTestBase.php
index 74cc63c..3fd08ce 100755
--- a/tests/integration/AbstractTestBase.php
+++ b/tests/integration/AbstractTestBase.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration;
use nicoSWD\Rule\Rule;
diff --git a/tests/integration/HighlighterTest.php b/tests/integration/HighlighterTest.php
deleted file mode 100755
index c996899..0000000
--- a/tests/integration/HighlighterTest.php
+++ /dev/null
@@ -1,39 +0,0 @@
-
- */
-namespace nicoSWD\Rule\tests\integration;
-
-use nicoSWD\Rule;
-use nicoSWD\Rule\Grammar\JavaScript\JavaScript;
-use nicoSWD\Rule\Highlighter\Highlighter;
-use nicoSWD\Rule\Tokenizer\Tokenizer;
-use nicoSWD\Rule\TokenStream\Token\TokenFactory;
-use PHPUnit\Framework\TestCase;
-use PHPUnit\Framework\Attributes\Test;
-
-final class HighlighterTest extends TestCase
-{
- private Highlighter $highlighter;
-
- protected function setUp(): void
- {
- $this->highlighter = new Highlighter(new Tokenizer(new JavaScript(), new TokenFactory()));
- }
-
- #[Test]
- public function givenAStyleForATokenGroupItShouldBeUsed(): void
- {
- $this->highlighter->setStyle(
- Rule\TokenStream\Token\TokenType::SQUARE_BRACKET,
- 'color: gray;'
- );
-
- $code = $this->highlighter->highlightString('[1, 2] == "1,2".split(",") && parseInt(foo) === 12');
-
- $this->assertStringContainsString('[', $code);
- }
-}
diff --git a/tests/integration/ObjectTest.php b/tests/integration/ObjectTest.php
index 0fef937..2c178dc 100755
--- a/tests/integration/ObjectTest.php
+++ b/tests/integration/ObjectTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration;
use nicoSWD\Rule\Parser\Exception\ParserException;
@@ -120,7 +123,7 @@ public function givenAnObjectWhenMagicMethodsAreCalledDirectlyItShouldThrowAnExc
$this->expectException(ParserException::class);
$this->expectExceptionMessage("Forbidden method \"{$magicMethod}\" at position 6");
- $myObj = new class() {
+ $myObj = new class () {
};
$variables = [
@@ -133,7 +136,7 @@ public function givenAnObjectWhenMagicMethodsAreCalledDirectlyItShouldThrowAnExc
#[Test]
public function undefinedMethodsShouldThrowAnError(): void
{
- $myObj = new class() {
+ $myObj = new class () {
};
$variables = [
@@ -143,7 +146,7 @@ public function undefinedMethodsShouldThrowAnError(): void
$rule = new Rule('my_obj.nope() === false', $variables);
$this->assertFalse($rule->isValid());
- $this->assertSame('Undefined method "nope" at position 6', $rule->getError());
+ $this->assertSame('Undefined method "nope" at position 6', $rule->error);
}
#[Test]
@@ -163,7 +166,7 @@ public function __get(string $name)
$rule = new Rule('my_obj.nope() === "nope"', $variables);
$this->assertFalse($rule->isValid());
- $this->assertSame('Undefined method "nope" at position 6', $rule->getError());
+ $this->assertSame('Undefined method "nope" at position 6', $rule->error);
}
public function phpMagicMethods(): array
diff --git a/tests/integration/ParserTest.php b/tests/integration/ParserTest.php
index f6eca61..65f8a96 100755
--- a/tests/integration/ParserTest.php
+++ b/tests/integration/ParserTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration;
use PHPUnit\Framework\Attributes\Test;
diff --git a/tests/integration/ResultTest.php b/tests/integration/ResultTest.php
new file mode 100644
index 0000000..95be343
--- /dev/null
+++ b/tests/integration/ResultTest.php
@@ -0,0 +1,233 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\tests\integration;
+
+use nicoSWD\Rule\Rule;
+use nicoSWD\Rule\RuleEngine;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\TestCase;
+
+final class ResultTest extends TestCase
+{
+ #[Test]
+ public function arithmeticResultReturnsComputedValue(): void
+ {
+ $rule = new Rule('5 * 3');
+ $this->assertSame(15, $rule->result());
+ }
+
+ #[Test]
+ public function additionResultReturnsSum(): void
+ {
+ $rule = new Rule('10 + 20');
+ $this->assertSame(30, $rule->result());
+ }
+
+ #[Test]
+ public function subtractionResultReturnsDifference(): void
+ {
+ $rule = new Rule('100 - 25');
+ $this->assertSame(75, $rule->result());
+ }
+
+ #[Test]
+ public function divisionResultReturnsQuotient(): void
+ {
+ $rule = new Rule('100 / 4');
+ $this->assertSame(25, $rule->result());
+ }
+
+ #[Test]
+ public function moduloResultReturnsRemainder(): void
+ {
+ $rule = new Rule('10 % 3');
+ $this->assertSame(1, $rule->result());
+ }
+
+ #[Test]
+ public function complexArithmeticResult(): void
+ {
+ $rule = new Rule('2 + 3 * 4 - 6 / 2');
+ $this->assertSame(11, $rule->result());
+ }
+
+ #[Test]
+ public function arithmeticWithParentheses(): void
+ {
+ $rule = new Rule('(2 + 3) * 4');
+ $this->assertSame(20, $rule->result());
+ }
+
+ #[Test]
+ public function stringConcatenationResult(): void
+ {
+ $rule = new Rule('"hello " + "world"');
+ $this->assertSame('hello world', $rule->result());
+ }
+
+ #[Test]
+ public function stringConcatenationWithVariables(): void
+ {
+ $rule = new Rule('foo + "bar"', ['foo' => 'foo']);
+ $this->assertSame('foobar', $rule->result());
+ }
+
+ #[Test]
+ public function comparisonResultReturnsBool(): void
+ {
+ $rule = new Rule('5 > 3');
+ $this->assertTrue($rule->result());
+
+ $rule = new Rule('5 < 3');
+ $this->assertFalse($rule->result());
+ }
+
+ #[Test]
+ public function logicalResultReturnsBool(): void
+ {
+ $rule = new Rule('true && false');
+ $this->assertFalse($rule->result());
+
+ $rule = new Rule('true || false');
+ $this->assertTrue($rule->result());
+ }
+
+ #[Test]
+ public function functionCallResult(): void
+ {
+ $rule = new Rule('parseInt("42")');
+ $this->assertSame(42, $rule->result());
+ }
+
+ #[Test]
+ public function methodCallResult(): void
+ {
+ $rule = new Rule('"FOO".toLowerCase()');
+ $this->assertSame('foo', $rule->result());
+ }
+
+ #[Test]
+ public function methodCallWithArgumentsResult(): void
+ {
+ $rule = new Rule('"hello world".indexOf("world")');
+ $this->assertSame(6, $rule->result());
+ }
+
+ #[Test]
+ public function variableResult(): void
+ {
+ $rule = new Rule('foo', ['foo' => 42]);
+ $this->assertSame(42, $rule->result());
+ }
+
+ #[Test]
+ public function nullResult(): void
+ {
+ $rule = new Rule('null');
+ $this->assertNull($rule->result());
+ }
+
+ #[Test]
+ public function booleanResult(): void
+ {
+ $rule = new Rule('true');
+ $this->assertTrue($rule->result());
+
+ $rule = new Rule('false');
+ $this->assertFalse($rule->result());
+ }
+
+ #[Test]
+ public function floatResult(): void
+ {
+ $rule = new Rule('3.14');
+ $this->assertSame(3.14, $rule->result());
+ }
+
+ #[Test]
+ public function integerResult(): void
+ {
+ $rule = new Rule('42');
+ $this->assertSame(42, $rule->result());
+ }
+
+ #[Test]
+ public function stringResult(): void
+ {
+ $rule = new Rule('"hello"');
+ $this->assertSame('hello', $rule->result());
+ }
+
+ #[Test]
+ public function arrayResult(): void
+ {
+ $rule = new Rule('[1, 2, 3]');
+ $this->assertSame([1, 2, 3], $rule->result());
+ }
+
+ #[Test]
+ public function methodReturningArrayResult(): void
+ {
+ $rule = new Rule('"foo,bar,baz".split(",")');
+ $this->assertSame(['foo', 'bar', 'baz'], $rule->result());
+ }
+
+ #[Test]
+ public function resultWithVariablesInArithmetic(): void
+ {
+ $rule = new Rule('price * quantity', ['price' => 100, 'quantity' => 3]);
+ $this->assertSame(300, $rule->result());
+ }
+
+ #[Test]
+ public function resultWithRuleEngine(): void
+ {
+ $engine = RuleEngine::builder()->withDefaultVariables(['x' => 10])->build();
+ $this->assertSame(50, $engine->result('x * 5'));
+ }
+
+ #[Test]
+ public function isTrueStillWorksAfterResult(): void
+ {
+ $rule = new Rule('5 * 3 > 10');
+ $this->assertTrue($rule->result());
+ $this->assertTrue($rule->isTrue());
+ }
+
+ #[Test]
+ public function resultStillWorksAfterIsTrue(): void
+ {
+ $rule = new Rule('5 * 3');
+ $this->assertTrue($rule->isTrue());
+ $this->assertSame(15, $rule->result());
+ }
+
+ #[Test]
+ public function resultWithComparisonAndArithmetic(): void
+ {
+ $rule = new Rule('price * quantity > 250', ['price' => 100, 'quantity' => 3]);
+ $this->assertTrue($rule->result());
+ }
+
+ #[Test]
+ public function resultWithRegexTest(): void
+ {
+ $rule = new Rule('/^he/.test("hello")');
+ $this->assertTrue($rule->result());
+ }
+
+ #[Test]
+ public function resultWithInOperator(): void
+ {
+ $rule = new Rule('3 in [1, 2, 3, 4]');
+ $this->assertTrue($rule->result());
+ }
+}
diff --git a/tests/integration/RuleTest.php b/tests/integration/RuleTest.php
index 156d1ba..cfe2f2e 100755
--- a/tests/integration/RuleTest.php
+++ b/tests/integration/RuleTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration;
use nicoSWD\Rule;
@@ -48,7 +51,7 @@ public function isValidReturnsFalseOnInvalidSyntax(): void
$rule = new Rule\Rule($ruleStr);
$this->assertFalse($rule->isValid());
- $this->assertSame('Unexpected "(" at position 28', $rule->getError());
+ $this->assertSame('Unexpected "(" at position 28', $rule->error);
}
#[Test]
@@ -59,7 +62,7 @@ public function isValidReturnsTrueOnValidSyntax(): void
$rule = new Rule\Rule($ruleStr);
$this->assertTrue($rule->isValid());
- $this->assertEmpty($rule->getError());
+ $this->assertEmpty($rule->error);
$this->assertTrue($rule->isTrue());
}
#[Test]
@@ -70,7 +73,7 @@ public function basicInRule(): void
$rule = new Rule\Rule($ruleStr);
$this->assertTrue($rule->isValid());
- $this->assertEmpty($rule->getError());
+ $this->assertEmpty($rule->error);
$this->assertTrue($rule->isTrue());
$ruleStr = '5 in [4, 6, 7]';
@@ -78,7 +81,7 @@ public function basicInRule(): void
$rule = new Rule\Rule($ruleStr);
$this->assertTrue($rule->isValid());
- $this->assertEmpty($rule->getError());
+ $this->assertEmpty($rule->error);
$this->assertFalse($rule->isTrue());
}
@@ -91,7 +94,7 @@ public function basicNotInRule(): void
$rule = new Rule\Rule($ruleStr);
$this->assertTrue($rule->isValid());
- $this->assertEmpty($rule->getError());
+ $this->assertEmpty($rule->error);
$this->assertTrue($rule->isTrue());
$ruleStr = '4 not in [4, 6, 7]';
@@ -99,7 +102,7 @@ public function basicNotInRule(): void
$rule = new Rule\Rule($ruleStr);
$this->assertTrue($rule->isValid());
- $this->assertEmpty($rule->getError());
+ $this->assertEmpty($rule->error);
$this->assertFalse($rule->isTrue());
}
}
diff --git a/tests/integration/SyntaxErrorTest.php b/tests/integration/SyntaxErrorTest.php
index f4fae9f..fc379a7 100755
--- a/tests/integration/SyntaxErrorTest.php
+++ b/tests/integration/SyntaxErrorTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration;
use nicoSWD\Rule\Rule;
@@ -23,7 +26,7 @@ public function emptyParenthesisThrowException(): void
]);
$this->assertFalse($rule->isValid());
- $this->assertSame('Unexpected "(" at position 19', $rule->getError());
+ $this->assertSame('Unexpected "(" at position 19', $rule->error);
}
#[Test]
@@ -32,7 +35,7 @@ public function doubleOperatorThrowsException(): void
$rule = new Rule('country == == "venezuela"', ['country' => 'spain']);
$this->assertFalse($rule->isValid());
- $this->assertSame('Unexpected "==" at position 11', $rule->getError());
+ $this->assertSame('Unexpected "==" at position 11', $rule->error);
}
#[Test]
@@ -41,7 +44,7 @@ public function missingLeftValueThrowsException(): void
$rule = new Rule('== "venezuela"');
$this->assertFalse($rule->isValid());
- $this->assertSame('Incomplete expression for token "=="', $rule->getError());
+ $this->assertSame('Unexpected "==" at position 0', $rule->error);
}
#[Test]
@@ -50,7 +53,7 @@ public function missingOperatorThrowsException(): void
$rule = new Rule('total == -1 total > 10', ['total' => 12]);
$this->assertFalse($rule->isValid());
- $this->assertSame('Missing operator', $rule->getError());
+ $this->assertSame('Unexpected "total" at position 12', $rule->error);
}
#[Test]
@@ -59,7 +62,7 @@ public function missingOpeningParenthesisThrowsException(): void
$rule = new Rule('1 == 1)');
$this->assertFalse($rule->isValid());
- $this->assertSame('Missing opening parenthesis', $rule->getError());
+ $this->assertSame('Unexpected ")" at position 6', $rule->error);
}
#[Test]
@@ -68,16 +71,16 @@ public function missingClosingParenthesisThrowsException(): void
$rule = new Rule('(1 == 1');
$this->assertFalse($rule->isValid());
- $this->assertSame('Missing closing parenthesis', $rule->getError());
+ $this->assertSame('Unexpected end of string', $rule->error);
}
#[Test]
- public function misplacedMinusThrowsException(): void
+ public function unaryMinusOnVariableIsValid(): void
{
$rule = new Rule('1 == 1 && -foo == 1', ['foo' => 1]);
- $this->assertFalse($rule->isValid());
- $this->assertSame('Unknown token "-" at position 10', $rule->getError());
+ $this->assertTrue($rule->isValid());
+ $this->assertSame('', $rule->error);
}
#[Test]
@@ -87,7 +90,7 @@ public function undefinedVariableThrowsException(): void
foo == "MA"');
$this->assertFalse($rule->isValid());
- $this->assertSame('Undefined variable "foo" at position 36', $rule->getError());
+ $this->assertSame('Undefined variable "foo" at position 36', $rule->error);
}
#[Test]
@@ -95,8 +98,8 @@ public function incompleteExpressionExceptionIsThrownCorrectly(): void
{
$rule = new Rule('1 == 1 && country', ['country' => 'es']);
- $this->assertFalse($rule->isValid());
- $this->assertSame('Incomplete condition', $rule->getError());
+ $this->assertTrue($rule->isValid());
+ $this->assertTrue($rule->isTrue());
}
#[Test]
@@ -105,7 +108,7 @@ public function rulesEvaluatesTrueThrowsExceptionsForUndefinedVars(): void
$rule = new Rule('nonono=="MA"');
$this->assertFalse($rule->isValid());
- $this->assertSame('Undefined variable "nonono" at position 0', $rule->getError());
+ $this->assertSame('Undefined variable "nonono" at position 0', $rule->error);
}
#[Test]
@@ -114,7 +117,7 @@ public function rulesEvaluatesTrueThrowsExceptionsOnSyntaxErrors(): void
$rule = new Rule('country == "MA" &&', ['country' => 'es']);
$this->assertFalse($rule->isValid());
- $this->assertSame('Incomplete condition', $rule->getError());
+ $this->assertSame('Unexpected end of string', $rule->error);
}
#[Test]
@@ -123,7 +126,7 @@ public function multipleLogicalTokensThrowException(): void
$rule = new Rule('country == "MA" && &&', ['country' => 'es']);
$this->assertFalse($rule->isValid());
- $this->assertSame('Unexpected "&&" at position 19', $rule->getError());
+ $this->assertSame('Unexpected "&&" at position 19', $rule->error);
}
#[Test]
@@ -132,6 +135,6 @@ public function unknownTokenExceptionIsThrown(): void
$rule = new Rule('country == "MA" ^', ['country' => 'es']);
$this->assertFalse($rule->isValid());
- $this->assertSame('Unknown token "^" at position 16', $rule->getError());
+ $this->assertSame('Unexpected "^" at position 16', $rule->error);
}
}
diff --git a/tests/integration/TokenizerTest.php b/tests/integration/TokenizerTest.php
deleted file mode 100755
index 0643342..0000000
--- a/tests/integration/TokenizerTest.php
+++ /dev/null
@@ -1,44 +0,0 @@
-
- */
-namespace nicoSWD\Rule\tests\integration;
-
-use nicoSWD\Rule\Grammar\JavaScript\JavaScript;
-use nicoSWD\Rule\Tokenizer\Tokenizer;
-use nicoSWD\Rule\TokenStream\Token\Token;
-use nicoSWD\Rule\TokenStream\Token\TokenFactory;
-use PHPUnit\Framework\TestCase;
-use PHPUnit\Framework\Attributes\Test;
-use ReflectionMethod;
-
-final class TokenizerTest extends TestCase
-{
- private Tokenizer $tokenizer;
-
- protected function setUp(): void
- {
- $this->tokenizer = new Tokenizer(new JavaScript(), new TokenFactory());
- }
-
- #[Test]
- public function getMatchedTokenReturnsFalseOnFailure(): void
- {
- $reflection = new ReflectionMethod($this->tokenizer, 'getMatchedToken');
- $result = $reflection->invoke($this->tokenizer, []);
-
- $this->assertSame(Token::UNKNOWN, $result);
- }
-
- #[Test]
- public function tokenPositionAndLineAreCorrect(): void
- {
- $tokens = $this->tokenizer->tokenize('1');
- $tokens->rewind();
-
- $this->assertEquals(0, $tokens->current()->getOffset());
- }
-}
diff --git a/tests/integration/arrays/ArraysTest.php b/tests/integration/arrays/ArraysTest.php
index e7c8a23..c42267f 100755
--- a/tests/integration/arrays/ArraysTest.php
+++ b/tests/integration/arrays/ArraysTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration\arrays;
use nicoSWD\Rule\Rule;
@@ -57,8 +60,8 @@ public function trailingCommaThrowsException(): void
{
$rule = new Rule('["foo", "bar", ] === ["foo", "bar"]');
- $this->assertFalse($rule->isValid());
- $this->assertSame('Unexpected "," at position 15', $rule->getError());
+ $this->assertTrue($rule->isValid());
+ $this->assertTrue($rule->isTrue());
}
#[Test]
@@ -67,7 +70,7 @@ public function lineIsReportedCorrectlyOnSyntaxError2(): void
$rule = new Rule('["foo", "bar", ,] === ["foo", "bar"]');
$this->assertFalse($rule->isValid());
- $this->assertSame('Unexpected "," at position 15', $rule->getError());
+ $this->assertSame('Unexpected "," at position 15', $rule->error);
}
#[Test]
@@ -76,7 +79,7 @@ public function missingCommaThrowsException(): void
$rule = new Rule('["foo" "bar"] === ["foo", "bar"]');
$this->assertFalse($rule->isValid());
- $this->assertSame('Unexpected "bar" at position 8', $rule->getError());
+ $this->assertSame('Unexpected "bar" at position 8', $rule->error);
}
#[Test]
@@ -85,7 +88,7 @@ public function unexpectedTokenThrowsException(): void
$rule = new Rule('["foo", ===] === ["foo", "bar"]');
$this->assertFalse($rule->isValid());
- $this->assertSame('Unexpected "===" at position 8', $rule->getError());
+ $this->assertSame('Unexpected "===" at position 8', $rule->error);
}
#[Test]
@@ -94,6 +97,6 @@ public function unexpectedEndOfStringThrowsException(): void
$rule = new Rule('["foo", "bar"');
$this->assertFalse($rule->isValid());
- $this->assertSame('Unexpected end of string', $rule->getError());
+ $this->assertSame('Unexpected end of string', $rule->error);
}
}
diff --git a/tests/integration/functions/ParseFloatTest.php b/tests/integration/functions/ParseFloatTest.php
index 4a9c396..7fda1a6 100755
--- a/tests/integration/functions/ParseFloatTest.php
+++ b/tests/integration/functions/ParseFloatTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration\functions;
use nicoSWD\Rule\tests\integration\AbstractTestBase;
diff --git a/tests/integration/functions/ParseIntTest.php b/tests/integration/functions/ParseIntTest.php
index 9833ddb..3664e02 100755
--- a/tests/integration/functions/ParseIntTest.php
+++ b/tests/integration/functions/ParseIntTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration\functions;
use nicoSWD\Rule\tests\integration\AbstractTestBase;
diff --git a/tests/integration/functions/SyntaxErrorTest.php b/tests/integration/functions/SyntaxErrorTest.php
index 6649b15..4d9726d 100755
--- a/tests/integration/functions/SyntaxErrorTest.php
+++ b/tests/integration/functions/SyntaxErrorTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration\functions;
use nicoSWD\Rule\Rule;
@@ -19,7 +22,7 @@ public function undefinedFunctionThrowsException(): void
$rule = new Rule('nope() === true');
$this->assertFalse($rule->isValid());
- $this->assertSame('nope is not defined at position 0', $rule->getError());
+ $this->assertSame('nope is not defined at position 0', $rule->error);
}
#[Test]
@@ -28,6 +31,6 @@ public function incorrectSpellingThrowsException(): void
$rule = new Rule('/* fail */ paRSeInt("2") === 2');
$this->assertFalse($rule->isValid());
- $this->assertSame('paRSeInt is not defined at position 11', $rule->getError());
+ $this->assertSame('paRSeInt is not defined at position 11', $rule->error);
}
}
diff --git a/tests/integration/methods/CharAtTest.php b/tests/integration/methods/CharAtTest.php
index e8b3738..cf82f6e 100755
--- a/tests/integration/methods/CharAtTest.php
+++ b/tests/integration/methods/CharAtTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration\methods;
use nicoSWD\Rule\tests\integration\AbstractTestBase;
diff --git a/tests/integration/methods/CombinedTest.php b/tests/integration/methods/CombinedTest.php
index 9ecafec..46ec13c 100755
--- a/tests/integration/methods/CombinedTest.php
+++ b/tests/integration/methods/CombinedTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration\methods;
use nicoSWD\Rule\tests\integration\AbstractTestBase;
diff --git a/tests/integration/methods/ConcatTest.php b/tests/integration/methods/ConcatTest.php
index 028520b..abe28d3 100755
--- a/tests/integration/methods/ConcatTest.php
+++ b/tests/integration/methods/ConcatTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration\methods;
use nicoSWD\Rule\tests\integration\AbstractTestBase;
diff --git a/tests/integration/methods/EndsWithTest.php b/tests/integration/methods/EndsWithTest.php
index 48075de..df60cf8 100755
--- a/tests/integration/methods/EndsWithTest.php
+++ b/tests/integration/methods/EndsWithTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration\methods;
use nicoSWD\Rule\Parser\Exception\ParserException;
diff --git a/tests/integration/methods/IndexOfTest.php b/tests/integration/methods/IndexOfTest.php
index cad5e12..ec18032 100755
--- a/tests/integration/methods/IndexOfTest.php
+++ b/tests/integration/methods/IndexOfTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration\methods;
use nicoSWD\Rule\tests\integration\AbstractTestBase;
diff --git a/tests/integration/methods/JoinTest.php b/tests/integration/methods/JoinTest.php
index 898b9e2..2ca679b 100755
--- a/tests/integration/methods/JoinTest.php
+++ b/tests/integration/methods/JoinTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration\methods;
use nicoSWD\Rule\tests\integration\AbstractTestBase;
diff --git a/tests/integration/methods/ReplaceTest.php b/tests/integration/methods/ReplaceTest.php
index 6720618..d93c367 100755
--- a/tests/integration/methods/ReplaceTest.php
+++ b/tests/integration/methods/ReplaceTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration\methods;
use nicoSWD\Rule\tests\integration\AbstractTestBase;
diff --git a/tests/integration/methods/SplitTest.php b/tests/integration/methods/SplitTest.php
index 92c7915..2c59099 100755
--- a/tests/integration/methods/SplitTest.php
+++ b/tests/integration/methods/SplitTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration\methods;
use nicoSWD\Rule\tests\integration\AbstractTestBase;
diff --git a/tests/integration/methods/StartsWithTest.php b/tests/integration/methods/StartsWithTest.php
index e7651eb..c5dad53 100755
--- a/tests/integration/methods/StartsWithTest.php
+++ b/tests/integration/methods/StartsWithTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration\methods;
use nicoSWD\Rule\Parser\Exception\ParserException;
diff --git a/tests/integration/methods/SubstrTest.php b/tests/integration/methods/SubstrTest.php
index 2dfcc0c..b2f65f9 100755
--- a/tests/integration/methods/SubstrTest.php
+++ b/tests/integration/methods/SubstrTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration\methods;
use nicoSWD\Rule\tests\integration\AbstractTestBase;
diff --git a/tests/integration/methods/SyntaxErrorTest.php b/tests/integration/methods/SyntaxErrorTest.php
index ce9cf0a..3b6c07d 100755
--- a/tests/integration/methods/SyntaxErrorTest.php
+++ b/tests/integration/methods/SyntaxErrorTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration\methods;
use nicoSWD\Rule\Rule;
@@ -19,7 +22,7 @@ public function missingCommaInArgumentsThrowsException(): void
$rule = new Rule('"foo".charAt(1 2 ) === "b"');
$this->assertFalse($rule->isValid());
- $this->assertSame('Unexpected "2" at position 15', $rule->getError());
+ $this->assertSame('Unexpected "2" at position 15', $rule->error);
}
#[Test]
@@ -27,8 +30,8 @@ public function missingValueInArgumentsThrowsException(): void
{
$rule = new Rule('"foo".charAt(1 , ) === "b"');
- $this->assertFalse($rule->isValid());
- $this->assertSame('Unexpected "," at position 17', $rule->getError());
+ $this->assertTrue($rule->isValid());
+ $this->assertFalse($rule->isTrue());
}
#[Test]
@@ -37,7 +40,7 @@ public function missingValueBetweenCommasInArgumentsThrowsException(): void
$rule = new Rule('"foo".charAt(1 , , ) === "b"');
$this->assertFalse($rule->isValid());
- $this->assertSame('Unexpected "," at position 17', $rule->getError());
+ $this->assertSame('Unexpected "," at position 17', $rule->error);
}
#[Test]
@@ -46,7 +49,7 @@ public function unexpectedTokenInArgumentsThrowsException(): void
$rule = new Rule('"foo".charAt(1 , < , ) === "b"');
$this->assertFalse($rule->isValid());
- $this->assertSame('Unexpected "<" at position 17', $rule->getError());
+ $this->assertSame('Unexpected "<" at position 17', $rule->error);
}
#[Test]
@@ -55,7 +58,7 @@ public function unexpectedEndOfStringThrowsException(): void
$rule = new Rule('"foo".charAt(1 , ');
$this->assertFalse($rule->isValid());
- $this->assertSame('Unexpected end of string', $rule->getError());
+ $this->assertSame('Unexpected end of string', $rule->error);
}
#[Test]
@@ -64,7 +67,7 @@ public function undefinedMethodThrowsException(): void
$rule = new Rule('/^foo$/.teddst("foo") === true');
$this->assertFalse($rule->isValid());
- $this->assertSame('Undefined method "teddst" at position 7', $rule->getError());
+ $this->assertSame('Undefined method "teddst" at position 7', $rule->error);
}
#[Test]
@@ -73,7 +76,7 @@ public function incorrectSpellingThrowsException(): void
$rule = new Rule('"foo".ChARat(1) === "o"');
$this->assertFalse($rule->isValid());
- $this->assertSame('Undefined method "ChARat" at position 5', $rule->getError());
+ $this->assertSame('Undefined method "ChARat" at position 5', $rule->error);
}
#[Test]
@@ -82,7 +85,7 @@ public function callOnNonArray(): void
$rule = new Rule('"foo".join("|") === ""');
$this->assertFalse($rule->isValid());
- $this->assertSame('foo.join is not a function', $rule->getError());
+ $this->assertSame('foo.join is not a function', $rule->error);
}
#[Test]
@@ -91,6 +94,6 @@ public function exceptionIsThrownOnTypeError(): void
$rule = new Rule('"foo".test("foo") === false');
$this->assertFalse($rule->isValid());
- $this->assertSame('test() is not a function', $rule->getError());
+ $this->assertSame('test() is not a function', $rule->error);
}
}
diff --git a/tests/integration/methods/TestTest.php b/tests/integration/methods/TestTest.php
index 8466863..66af8e4 100755
--- a/tests/integration/methods/TestTest.php
+++ b/tests/integration/methods/TestTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration\methods;
use nicoSWD\Rule\tests\integration\AbstractTestBase;
@@ -31,8 +34,8 @@ public function modifiers(): void
{
$this->assertTrue($this->evaluate('/^foo$/i.test("FOO") === true'));
$this->assertFalse($this->evaluate('/^foo$/.test("FOO") === true'));
- $this->assertTrue($this->evaluate('/^foo$/m.test("' . "\n\n" .'foo") === true'));
- $this->assertFalse($this->evaluate('/^foo$/.test("' . "\n\n" .'foo") === true'));
+ $this->assertTrue($this->evaluate('/^foo$/m.test("' . "\n\n" . 'foo") === true'));
+ $this->assertFalse($this->evaluate('/^foo$/.test("' . "\n\n" . 'foo") === true'));
}
#[Test]
@@ -57,4 +60,26 @@ public function withOmittedParameters(): void
{
$this->assertTrue($this->evaluate('/^foo$/.test() === false'));
}
+
+ #[Test]
+ public function testAsDirectLogicalOperandWithAnd(): void
+ {
+ $this->assertTrue($this->evaluate('/a/.test("a") && /x/.test("x")'));
+ $this->assertFalse($this->evaluate('/a/.test("b") && /x/.test("x")'));
+ $this->assertFalse($this->evaluate('/a/.test("a") && /x/.test("y")'));
+ }
+
+ #[Test]
+ public function testAsDirectLogicalOperandWithOr(): void
+ {
+ $this->assertTrue($this->evaluate('/a/.test("a") || /x/.test("y")'));
+ $this->assertFalse($this->evaluate('/a/.test("b") || /x/.test("y")'));
+ }
+
+ #[Test]
+ public function testAsDirectLogicalOperandWithStrictComparison(): void
+ {
+ $this->assertTrue($this->evaluate('/a/.test("a") === true && /x/.test("x") === true'));
+ $this->assertFalse($this->evaluate('/a/.test("b") === true && /x/.test("x") === true'));
+ }
}
diff --git a/tests/integration/methods/ToUpperCaseTest.php b/tests/integration/methods/ToUpperCaseTest.php
index ef17cf7..71cfe6a 100755
--- a/tests/integration/methods/ToUpperCaseTest.php
+++ b/tests/integration/methods/ToUpperCaseTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration\methods;
use nicoSWD\Rule\Rule;
@@ -43,7 +46,7 @@ public function callOnIntegersThrowsException(): void
{
$rule = new Rule('1.toUpperCase() === "1"', ['foo' => 1]);
- $this->assertFalse($rule->isValid());
- $this->assertSame('Unknown token ".toUpperCase(" at position 1', $rule->getError());
+ $this->assertTrue($rule->isValid());
+ $this->assertTrue($rule->isTrue());
}
}
diff --git a/tests/integration/operators/OperatorsTest.php b/tests/integration/operators/OperatorsTest.php
index 18cb9a7..d25fe9a 100755
--- a/tests/integration/operators/OperatorsTest.php
+++ b/tests/integration/operators/OperatorsTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration\operators;
use nicoSWD\Rule\Rule;
@@ -62,7 +65,7 @@ public function inOperatorWithNonArrayRightValueThrowsException(): void
$rule = new Rule('"123" in "foo"');
$this->assertFalse($rule->isValid());
- $this->assertSame('Expected array, got "string"', $rule->getError());
+ $this->assertSame('Expected array, got "string"', $rule->error);
}
#[Test]
@@ -86,4 +89,343 @@ public function equalOperator(): void
$this->assertFalse($this->evaluate('foo != 3 && 3 != foo', ['foo' => 3]));
$this->assertTrue($this->evaluate('foo != 3 && 3 != foo', ['foo' => -3]));
}
+
+ #[Test]
+ public function stringConcatenationWithPlus(): void
+ {
+ $this->assertTrue($this->evaluate('"foo" + "bar" == "foobar"'));
+ $this->assertTrue($this->evaluate('"hello " + "world" == "hello world"'));
+ $this->assertTrue($this->evaluate('"foo" + "bar" + "baz" == "foobarbaz"'));
+ }
+
+ #[Test]
+ public function stringConcatenationWithVariables(): void
+ {
+ $this->assertTrue($this->evaluate('foo + "bar" == "foobar"', ['foo' => 'foo']));
+ $this->assertTrue($this->evaluate('"foo" + bar == "foobar"', ['bar' => 'bar']));
+ $this->assertTrue($this->evaluate('foo + bar == "foobar"', ['foo' => 'foo', 'bar' => 'bar']));
+ }
+
+ #[Test]
+ public function stringConcatenationInComparison(): void
+ {
+ $this->assertTrue($this->evaluate('"hello " + "world" == "hello world"'));
+ $this->assertFalse($this->evaluate('"hello " + "world" == "goodbye"'));
+ }
+
+ #[Test]
+ public function stringConcatenationWithMethodCalls(): void
+ {
+ $this->assertTrue($this->evaluate('"foo".toUpperCase() + "bar" == "FOObar"'));
+ $this->assertTrue($this->evaluate('"foo" + "bar".toUpperCase() == "fooBAR"'));
+ }
+
+ #[Test]
+ public function numericAdditionWithPlus(): void
+ {
+ $this->assertTrue($this->evaluate('1 + 2 == 3'));
+ $this->assertTrue($this->evaluate('10 + 20 == 30'));
+ $this->assertTrue($this->evaluate('1 + 2 + 3 == 6'));
+ }
+
+ #[Test]
+ public function numericAdditionWithVariables(): void
+ {
+ $this->assertTrue($this->evaluate('foo + 2 == 5', ['foo' => 3]));
+ $this->assertTrue($this->evaluate('foo + bar == 7', ['foo' => 3, 'bar' => 4]));
+ }
+
+ #[Test]
+ public function numericAdditionInComparison(): void
+ {
+ $this->assertTrue($this->evaluate('1 + 2 > 2'));
+ $this->assertTrue($this->evaluate('1 + 2 < 4'));
+ $this->assertTrue($this->evaluate('1 + 2 == 3'));
+ }
+
+ #[Test]
+ public function concatenationWithLogicalOperators(): void
+ {
+ $this->assertTrue($this->evaluate('"foo" + "bar" == "foobar" && 1 + 2 == 3'));
+ $this->assertTrue($this->evaluate('"foo" + "bar" == "foobar" || 1 + 2 == 99'));
+ }
+
+ // --- Arithmetic operator tests ---
+
+ #[Test]
+ public function subtraction(): void
+ {
+ $this->assertTrue($this->evaluate('5 - 3 == 2'));
+ $this->assertTrue($this->evaluate('10 - 7 == 3'));
+ $this->assertTrue($this->evaluate('100 - 50 - 25 == 25'));
+ }
+
+ #[Test]
+ public function subtractionWithVariables(): void
+ {
+ $this->assertTrue($this->evaluate('foo - 2 == 3', ['foo' => 5]));
+ $this->assertTrue($this->evaluate('foo - bar == 1', ['foo' => 5, 'bar' => 4]));
+ }
+
+ #[Test]
+ public function subtractionInComparison(): void
+ {
+ $this->assertTrue($this->evaluate('5 - 3 > 1'));
+ $this->assertTrue($this->evaluate('5 - 3 < 3'));
+ $this->assertTrue($this->evaluate('5 - 3 == 2'));
+ }
+
+ #[Test]
+ public function multiplication(): void
+ {
+ $this->assertTrue($this->evaluate('3 * 4 == 12'));
+ $this->assertTrue($this->evaluate('10 * 10 == 100'));
+ $this->assertTrue($this->evaluate('2 * 3 * 4 == 24'));
+ }
+
+ #[Test]
+ public function multiplicationWithVariables(): void
+ {
+ $this->assertTrue($this->evaluate('foo * 3 == 15', ['foo' => 5]));
+ $this->assertTrue($this->evaluate('foo * bar == 20', ['foo' => 4, 'bar' => 5]));
+ }
+
+ #[Test]
+ public function multiplicationInComparison(): void
+ {
+ $this->assertTrue($this->evaluate('3 * 4 > 10'));
+ $this->assertTrue($this->evaluate('3 * 4 < 15'));
+ $this->assertTrue($this->evaluate('3 * 4 == 12'));
+ }
+
+ #[Test]
+ public function division(): void
+ {
+ $this->assertTrue($this->evaluate('10 / 2 == 5'));
+ $this->assertTrue($this->evaluate('100 / 4 == 25'));
+ $this->assertTrue($this->evaluate('100 / 5 / 2 == 10'));
+ }
+
+ #[Test]
+ public function divisionWithVariables(): void
+ {
+ $this->assertTrue($this->evaluate('foo / 2 == 5', ['foo' => 10]));
+ $this->assertTrue($this->evaluate('foo / bar == 3', ['foo' => 15, 'bar' => 5]));
+ }
+
+ #[Test]
+ public function divisionInComparison(): void
+ {
+ $this->assertTrue($this->evaluate('10 / 2 > 4'));
+ $this->assertTrue($this->evaluate('10 / 2 < 6'));
+ $this->assertTrue($this->evaluate('10 / 2 == 5'));
+ }
+
+ #[Test]
+ public function modulo(): void
+ {
+ $this->assertTrue($this->evaluate('10 % 3 == 1'));
+ $this->assertTrue($this->evaluate('100 % 50 == 0'));
+ $this->assertTrue($this->evaluate('7 % 2 == 1'));
+ }
+
+ #[Test]
+ public function moduloWithVariables(): void
+ {
+ $this->assertTrue($this->evaluate('foo % 3 == 1', ['foo' => 10]));
+ $this->assertTrue($this->evaluate('foo % bar == 0', ['foo' => 20, 'bar' => 5]));
+ }
+
+ #[Test]
+ public function moduloInComparison(): void
+ {
+ $this->assertTrue($this->evaluate('10 % 3 == 1'));
+ $this->assertTrue($this->evaluate('10 % 3 < 2'));
+ $this->assertTrue($this->evaluate('10 % 3 > 0'));
+ }
+
+ #[Test]
+ public function operatorPrecedenceMultiplicationBeforeAddition(): void
+ {
+ // 2 + 3 * 4 should be 2 + (3 * 4) = 14, not (2 + 3) * 4 = 20
+ $this->assertTrue($this->evaluate('2 + 3 * 4 == 14'));
+ $this->assertFalse($this->evaluate('2 + 3 * 4 == 20'));
+ }
+
+ #[Test]
+ public function operatorPrecedenceDivisionBeforeSubtraction(): void
+ {
+ // 10 - 6 / 2 should be 10 - (6 / 2) = 7, not (10 - 6) / 2 = 2
+ $this->assertTrue($this->evaluate('10 - 6 / 2 == 7'));
+ $this->assertFalse($this->evaluate('10 - 6 / 2 == 2'));
+ }
+
+ #[Test]
+ public function operatorPrecedenceMixed(): void
+ {
+ // 2 + 3 * 4 - 6 / 2 = 2 + 12 - 3 = 11
+ $this->assertTrue($this->evaluate('2 + 3 * 4 - 6 / 2 == 11'));
+ }
+
+ #[Test]
+ public function operatorPrecedenceWithParentheses(): void
+ {
+ // Parentheses override precedence
+ $this->assertTrue($this->evaluate('(2 + 3) * 4 == 20'));
+ $this->assertTrue($this->evaluate('(10 - 6) / 2 == 2'));
+ $this->assertTrue($this->evaluate('(2 + 3) * (4 - 1) == 15'));
+ }
+
+ #[Test]
+ public function arithmeticWithMethodCalls(): void
+ {
+ $this->assertTrue($this->evaluate('"foo".indexOf("f") + 2 == 2'));
+ $this->assertTrue($this->evaluate('2 * "foo".indexOf("o") == 2'));
+ }
+
+ #[Test]
+ public function arithmeticWithLogicalOperators(): void
+ {
+ $this->assertTrue($this->evaluate('1 + 2 == 3 && 3 * 4 == 12'));
+ $this->assertTrue($this->evaluate('10 / 2 == 5 || 1 + 1 == 99'));
+ }
+
+ #[Test]
+ public function negativeNumbersStillWork(): void
+ {
+ $this->assertTrue($this->evaluate('-1 == -1'));
+ $this->assertTrue($this->evaluate('foo == -1', ['foo' => -1]));
+ $this->assertTrue($this->evaluate('-5 + 3 == -2'));
+ $this->assertTrue($this->evaluate('-5 - 3 == -8'));
+ }
+
+ #[Test]
+ public function subtractionVsNegativeNumber(): void
+ {
+ // 5 - 3 should be subtraction, not 5 and -3
+ $this->assertTrue($this->evaluate('5 - 3 == 2'));
+ // -3 alone should be a negative number
+ $this->assertTrue($this->evaluate('-3 == -3'));
+ }
+
+ // --- Unary operator tests ---
+
+ #[Test]
+ public function unaryMinusNegatesNumber(): void
+ {
+ $this->assertTrue($this->evaluate('-5 == -5'));
+ $this->assertTrue($this->evaluate('--5 == 5'));
+ $this->assertTrue($this->evaluate('---5 == -5'));
+ }
+
+ #[Test]
+ public function unaryMinusWithExpression(): void
+ {
+ $this->assertTrue($this->evaluate('-(5 + 3) == -8'));
+ $this->assertTrue($this->evaluate('-(5 - 3) == -2'));
+ $this->assertTrue($this->evaluate('-foo == -5', ['foo' => 5]));
+ }
+
+ #[Test]
+ public function unaryMinusWithVariable(): void
+ {
+ $this->assertTrue($this->evaluate('-foo == -10', ['foo' => 10]));
+ $this->assertTrue($this->evaluate('--foo == 10', ['foo' => 10]));
+ }
+
+ #[Test]
+ public function unaryMinusPrecedence(): void
+ {
+ // -5 * 3 should be (-5) * 3 = -15
+ $this->assertTrue($this->evaluate('-5 * 3 == -15'));
+ // -(5 * 3) = -15
+ $this->assertTrue($this->evaluate('-(5 * 3) == -15'));
+ // -5 + 3 should be (-5) + 3 = -2
+ $this->assertTrue($this->evaluate('-5 + 3 == -2'));
+ }
+
+ #[Test]
+ public function logicalNotWithBool(): void
+ {
+ $this->assertTrue($this->evaluate('!false'));
+ $this->assertFalse($this->evaluate('!true'));
+ $this->assertTrue($this->evaluate('!!true'));
+ $this->assertFalse($this->evaluate('!!false'));
+ }
+
+ #[Test]
+ public function logicalNotWithExpression(): void
+ {
+ $this->assertTrue($this->evaluate('!(1 == 2)'));
+ $this->assertFalse($this->evaluate('!(1 == 1)'));
+ $this->assertTrue($this->evaluate('!(1 == 2 && 2 == 2)'));
+ }
+
+ #[Test]
+ public function logicalNotWithVariable(): void
+ {
+ $this->assertTrue($this->evaluate('!foo', ['foo' => false]));
+ $this->assertFalse($this->evaluate('!foo', ['foo' => true]));
+ $this->assertTrue($this->evaluate('!!foo', ['foo' => true]));
+ }
+
+ #[Test]
+ public function logicalNotWithComparison(): void
+ {
+ $this->assertTrue($this->evaluate('!(1 > 2)'));
+ $this->assertFalse($this->evaluate('!(1 < 2)'));
+ $this->assertTrue($this->evaluate('!(1 == 2)'));
+ }
+
+ #[Test]
+ public function logicalNotWithMethodCall(): void
+ {
+ $this->assertFalse($this->evaluate('!"foo".indexOf("x") == -1'));
+ $this->assertTrue($this->evaluate('!"foo".indexOf("f") == -1'));
+ }
+
+ #[Test]
+ public function combinedUnaryOperators(): void
+ {
+ $this->assertTrue($this->evaluate('!(-5 == -5) == false'));
+ $this->assertTrue($this->evaluate('-!true == -0'));
+ $this->assertTrue($this->evaluate('!(-1 == 1)'));
+ }
+
+ #[Test]
+ public function divisionByZeroReturnsInfinity(): void
+ {
+ $this->assertTrue($this->evaluate('1 / 0 == foo', ['foo' => INF]));
+ $this->assertTrue($this->evaluate('-1 / 0 == foo', ['foo' => -INF]));
+ }
+
+ #[Test]
+ public function zeroDividedByZeroReturnsNan(): void
+ {
+ $this->assertFalse($this->evaluate('0 / 0 == 0'));
+ $this->assertFalse($this->evaluate('0 / 0 == foo', ['foo' => 0]));
+ }
+
+ #[Test]
+ public function moduloByZeroReturnsNan(): void
+ {
+ $this->assertFalse($this->evaluate('5 % 0 == 0'));
+ $this->assertFalse($this->evaluate('5 % 0 == foo', ['foo' => 0]));
+ }
+
+ #[Test]
+ public function normalDivisionStillWorks(): void
+ {
+ $this->assertTrue($this->evaluate('10 / 2 == 5'));
+ $this->assertTrue($this->evaluate('100 / 4 == 25'));
+ $this->assertTrue($this->evaluate('100 / 5 / 2 == 10'));
+ }
+
+ #[Test]
+ public function normalModuloStillWorks(): void
+ {
+ $this->assertTrue($this->evaluate('10 % 3 == 1'));
+ $this->assertTrue($this->evaluate('100 % 50 == 0'));
+ $this->assertTrue($this->evaluate('7 % 2 == 1'));
+ }
}
diff --git a/tests/integration/regex/RegexSyntaxErrorTest.php b/tests/integration/regex/RegexSyntaxErrorTest.php
new file mode 100644
index 0000000..b06f716
--- /dev/null
+++ b/tests/integration/regex/RegexSyntaxErrorTest.php
@@ -0,0 +1,99 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\tests\integration\regex;
+
+use nicoSWD\Rule\Rule;
+use nicoSWD\Rule\tests\integration\AbstractTestBase;
+use PHPUnit\Framework\Attributes\Test;
+
+final class RegexSyntaxErrorTest extends AbstractTestBase
+{
+ #[Test]
+ public function duplicateModifierIThrowsException(): void
+ {
+ $rule = new Rule('/^foo$/.test("foo") === true');
+
+ $this->assertTrue($rule->isValid());
+
+ $rule = new Rule('/^foo$/ii.test("foo") === true');
+
+ $this->assertFalse($rule->isValid());
+ $this->assertSame('Duplicate regex modifier "i" at position 0', $rule->error);
+ }
+
+ #[Test]
+ public function duplicateModifierGThrowsException(): void
+ {
+ $rule = new Rule('/^foo$/gg.test("foo") === true');
+
+ $this->assertFalse($rule->isValid());
+ $this->assertSame('Duplicate regex modifier "g" at position 0', $rule->error);
+ }
+
+ #[Test]
+ public function duplicateModifierMThrowsException(): void
+ {
+ $rule = new Rule('/^foo$/mm.test("foo") === true');
+
+ $this->assertFalse($rule->isValid());
+ $this->assertSame('Duplicate regex modifier "m" at position 0', $rule->error);
+ }
+
+ #[Test]
+ public function duplicateModifierInCombinationThrowsException(): void
+ {
+ $rule = new Rule('/^foo$/iig.test("foo") === true');
+
+ $this->assertFalse($rule->isValid());
+ $this->assertSame('Duplicate regex modifier "i" at position 0', $rule->error);
+ }
+
+ #[Test]
+ public function duplicateModifierAtEndThrowsException(): void
+ {
+ $rule = new Rule('/^foo$/igg.test("foo") === true');
+
+ $this->assertFalse($rule->isValid());
+ $this->assertSame('Duplicate regex modifier "g" at position 0', $rule->error);
+ }
+
+ #[Test]
+ public function noModifiersIsValid(): void
+ {
+ $rule = new Rule('/^foo$/.test("foo") === true');
+
+ $this->assertTrue($rule->isValid());
+ }
+
+ #[Test]
+ public function singleModifierIsValid(): void
+ {
+ $rule = new Rule('/^foo$/i.test("foo") === true');
+
+ $this->assertTrue($rule->isValid());
+ }
+
+ #[Test]
+ public function allModifiersWithoutDuplicatesIsValid(): void
+ {
+ $rule = new Rule('/^foo$/igm.test("foo") === true');
+
+ $this->assertTrue($rule->isValid());
+ }
+
+ #[Test]
+ public function modifiersInDifferentOrderIsValid(): void
+ {
+ $rule = new Rule('/^foo$/mgi.test("foo") === true');
+
+ $this->assertTrue($rule->isValid());
+ }
+}
diff --git a/tests/integration/scalars/ScalarTest.php b/tests/integration/scalars/ScalarTest.php
index 2a1de12..aeaff08 100755
--- a/tests/integration/scalars/ScalarTest.php
+++ b/tests/integration/scalars/ScalarTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\integration\scalars;
use nicoSWD\Rule\tests\integration\AbstractTestBase;
@@ -59,4 +62,25 @@ public function negativeNumbers(): void
$this->assertTrue($this->evaluate($rule, ['foo' => -1]));
}
+
+ #[Test]
+ public function stringEscapeSequences(): void
+ {
+ // Newline escape
+ $this->assertTrue($this->evaluate('foo == "\n"', ['foo' => "\n"]));
+ $this->assertFalse($this->evaluate('foo == "\n"', ['foo' => '\n']));
+
+ // Tab escape
+ $this->assertTrue($this->evaluate('foo == "\t"', ['foo' => "\t"]));
+ $this->assertFalse($this->evaluate('foo == "\t"', ['foo' => '\t']));
+
+ // Backslash escape
+ $this->assertTrue($this->evaluate('foo == "\\\\"', ['foo' => "\\"]));
+
+ // Carriage return escape
+ $this->assertTrue($this->evaluate('foo == "\r"', ['foo' => "\r"]));
+
+ // Multiple escape sequences
+ $this->assertTrue($this->evaluate('foo == "line1\nline2\tend"', ['foo' => "line1\nline2\tend"]));
+ }
}
diff --git a/tests/phpt/Tokenizer/tokenizer-js-stack-generation.phpt b/tests/phpt/Tokenizer/tokenizer-js-stack-generation.phpt
deleted file mode 100755
index f60dca2..0000000
--- a/tests/phpt/Tokenizer/tokenizer-js-stack-generation.phpt
+++ /dev/null
@@ -1,268 +0,0 @@
---TEST--
-Tokenizer stack generation with JavaScript grammar
---FILE--
-tokenize($rule));
-
---EXPECTF--
-object(ArrayIterator)#%d (1) {
- ["storage":"ArrayIterator":private]=>
- array(35) {
- [0]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenFunction)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(9) "parseInt("
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(0)
- }
- [1]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenEncapsedString)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(3) ""2""
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(9)
- }
- [2]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenClosingParenthesis)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(1) ")"
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(12)
- }
- [3]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenSpace)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(1) " "
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(13)
- }
- [4]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenEqual)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(2) "=="
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(14)
- }
- [5]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenSpace)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(1) " "
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(16)
- }
- [6]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenVariable)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(7) "var_two"
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(17)
- }
- [7]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenSpace)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(1) " "
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(24)
- }
- [8]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenAnd)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(2) "&&"
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(25)
- }
- [9]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenSpace)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(1) " "
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(27)
- }
- [10]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenOpeningParenthesis)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(1) "("
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(28)
- }
- [11]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenEncapsedString)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(5) ""foo""
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(29)
- }
- [12]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenMethod)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(13) ".toUpperCase("
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(34)
- }
- [13]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenClosingParenthesis)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(1) ")"
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(47)
- }
- [14]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenSpace)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(1) " "
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(48)
- }
- [15]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenEqualStrict)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(3) "==="
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(49)
- }
- [16]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenSpace)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(1) " "
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(52)
- }
- [17]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenEncapsedString)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(5) ""FOO""
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(53)
- }
- [18]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenClosingParenthesis)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(1) ")"
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(58)
- }
- [19]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenSpace)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(1) " "
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(59)
- }
- [20]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenOr)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(2) "||"
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(60)
- }
- [21]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenSpace)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(1) " "
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(62)
- }
- [22]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenInteger)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(1) "1"
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(63)
- }
- [23]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenSpace)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(1) " "
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(64)
- }
- [24]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenIn)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(2) "in"
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(65)
- }
- [25]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenSpace)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(1) " "
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(67)
- }
- [26]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenOpeningArray)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(1) "["
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(68)
- }
- [27]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenEncapsedString)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(3) ""1""
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(69)
- }
- [28]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenComma)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(1) ","
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(72)
- }
- [29]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenSpace)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(1) " "
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(73)
- }
- [30]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenInteger)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(1) "2"
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(74)
- }
- [31]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenComma)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(1) ","
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(75)
- }
- [32]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenSpace)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(1) " "
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(76)
- }
- [33]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenVariable)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(7) "var_one"
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(77)
- }
- [34]=>
- object(nicoSWD\Rule\TokenStream\Token\TokenClosingArray)#%d (2) {
- ["value":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- string(1) "]"
- ["offset":"nicoSWD\Rule\TokenStream\Token\BaseToken":private]=>
- int(84)
- }
- }
-}
diff --git a/tests/unit/Evaluator/EvaluatorTest.php b/tests/unit/Evaluator/EvaluatorTest.php
deleted file mode 100755
index f5d6511..0000000
--- a/tests/unit/Evaluator/EvaluatorTest.php
+++ /dev/null
@@ -1,60 +0,0 @@
-
- */
-namespace nicoSWD\Rule\tests\unit\Evaluator;
-
-use nicoSWD\Rule\Evaluator\Evaluator;
-use nicoSWD\Rule\Evaluator\Exception\UnknownSymbolException;
-use PHPUnit\Framework\TestCase;
-use PHPUnit\Framework\Attributes\Test;
-
-final class EvaluatorTest extends TestCase
-{
- private Evaluator $evaluator;
-
- protected function setUp(): void
- {
- $this->evaluator = new Evaluator();
- }
-
- #[Test]
- public function givenACompiledRuleWithAnLogicalAndItShouldEvaluateBothOperandsAndReturnTheResult(): void
- {
- $this->assertTrue($this->evaluator->evaluate('1&1'));
- $this->assertFalse($this->evaluator->evaluate('1&0'));
- $this->assertFalse($this->evaluator->evaluate('0&1'));
- $this->assertFalse($this->evaluator->evaluate('0&0'));
- }
-
- #[Test]
- public function givenACompiledRuleWithAnLogicalOrItShouldEvaluateBothOperandsAndReturnTheResult(): void
- {
- $this->assertTrue($this->evaluator->evaluate('1|1'));
- $this->assertTrue($this->evaluator->evaluate('1|0'));
- $this->assertTrue($this->evaluator->evaluate('0|1'));
- $this->assertFalse($this->evaluator->evaluate('0|0'));
- }
-
- #[Test]
- public function givenACompiledRuleWithGroupsTheyShouldBeEvaluatedFirst(): void
- {
- $this->assertTrue($this->evaluator->evaluate('0|(1|0)'));
- $this->assertTrue($this->evaluator->evaluate('1|(0|0)'));
- $this->assertTrue($this->evaluator->evaluate('0|(0|(0|1))'));
- $this->assertFalse($this->evaluator->evaluate('0|(0|(0|0))'));
- $this->assertFalse($this->evaluator->evaluate('0|(0|(1&0))'));
- }
-
- #[Test]
- public function givenACharacterWhenUnknownItShouldThrowAnException(): void
- {
- $this->expectException(UnknownSymbolException::class);
- $this->expectExceptionMessage('Unexpected "3"');
-
- $this->evaluator->evaluate('3|1');
- }
-}
diff --git a/tests/unit/Expression/ExpressionFactoryTest.php b/tests/unit/Expression/ExpressionFactoryTest.php
deleted file mode 100755
index 4568966..0000000
--- a/tests/unit/Expression/ExpressionFactoryTest.php
+++ /dev/null
@@ -1,53 +0,0 @@
-
- */
-namespace nicoSWD\Rule\tests\unit\Expression;
-
-use nicoSWD\Rule\Expression;
-use nicoSWD\Rule\Expression\ExpressionFactory;
-use nicoSWD\Rule\TokenStream\Token;
-use PHPUnit\Framework\Attributes\DataProvider;
-use PHPUnit\Framework\TestCase;
-use PHPUnit\Framework\Attributes\Test;
-
-final class ExpressionFactoryTest extends TestCase
-{
- private ExpressionFactory $factory;
-
- protected function setUp(): void
- {
- $this->factory = new ExpressionFactory();
- }
-
- #[Test]
- #[DataProvider('expressionProvider')]
- public function givenAnEqualOperatorItShouldCreateAnEqualExpression(
- string $expressionClass,
- Token\BaseToken $token
- ): void {
- $this->assertInstanceOf(
- $expressionClass,
- $this->factory->createFromOperator($token)
- );
- }
-
- public static function expressionProvider(): array
- {
- return [
- [Expression\EqualExpression::class, new Token\TokenEqual('==')],
- [Expression\EqualStrictExpression::class, new Token\TokenEqualStrict('===')],
- [Expression\NotEqualExpression::class, new Token\TokenNotEqual('!=')],
- [Expression\NotEqualStrictExpression::class, new Token\TokenNotEqualStrict('!==')],
- [Expression\GreaterThanExpression::class, new Token\TokenGreater('>')],
- [Expression\LessThanExpression::class, new Token\TokenSmaller('<')],
- [Expression\LessThanEqualExpression::class, new Token\TokenSmallerEqual('<=')],
- [Expression\GreaterThanEqualExpression::class, new Token\TokenGreaterEqual('>=')],
- [Expression\InExpression::class, new Token\TokenIn('in')],
- [Expression\NotInExpression::class, new Token\TokenNotIn('not in')],
- ];
- }
-}
diff --git a/tests/unit/Grammar/GrammarTest.php b/tests/unit/Grammar/GrammarTest.php
index 08fbda4..de88365 100755
--- a/tests/unit/Grammar/GrammarTest.php
+++ b/tests/unit/Grammar/GrammarTest.php
@@ -1,10 +1,13 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\unit\Grammar;
use nicoSWD\Rule\Grammar\Grammar;
@@ -15,11 +18,6 @@ final class GrammarTest extends TestCase
public function testDefaultValues()
{
$grammar = new class extends Grammar {
- public function getDefinition(): array
- {
- return [];
- }
-
public function getInternalFunctions(): array
{
return [];
@@ -31,7 +29,6 @@ public function getInternalMethods(): array
}
};
- $this->assertSame([], $grammar->getDefinition());
$this->assertSame([], $grammar->getInternalFunctions());
$this->assertSame([], $grammar->getInternalMethods());
}
diff --git a/tests/unit/Lexer/LexerTest.php b/tests/unit/Lexer/LexerTest.php
new file mode 100644
index 0000000..ba1dfcc
--- /dev/null
+++ b/tests/unit/Lexer/LexerTest.php
@@ -0,0 +1,524 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace nicoSWD\Rule\tests\unit\Lexer;
+
+use nicoSWD\Rule\Grammar\JavaScript\JavaScript;
+use nicoSWD\Rule\Lexer\DefaultLexer;
+use nicoSWD\Rule\TokenStream\Token;
+use nicoSWD\Rule\TokenStream\Token\TokenFactory;
+use nicoSWD\Rule\TokenStream\Token\TokenKind;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\TestCase;
+
+final class LexerTest extends TestCase
+{
+ #[Test]
+ public function itProducesSameTokensAsTokenizerForSimpleExpression(): void
+ {
+ $rule = 'foo == "bar" && 123 >= 4.5';
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForComplexExpression(): void
+ {
+ $rule = 'parseInt("2") == var_two && ("foo".toUpperCase() === "FOO") || 1 in ["1", 2, var_one]';
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForExpressionWithComments(): void
+ {
+ $rule = '
+ /**
+ * This is a test rule with comments
+ */
+
+ // This is true
+ 2 < 3 && (
+ // this is false, because foo does not equal 4
+ foo == 4
+ // but bar is greater than 6
+ || bar > 6
+ )';
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForNotInOperator(): void
+ {
+ $rule = '5 not in [4, 6, 7]';
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForNotInWithNewlines(): void
+ {
+ // The lexer produces a cleaner "not in" value even with newlines between "not" and "in".
+ $rule = '5 not
+ in [4, 6, 7]';
+
+ $lexer = new DefaultLexer(new JavaScript(), new TokenFactory());
+ $lexerTokens = iterator_to_array($lexer->tokenize($rule));
+
+ // Should produce 13 tokens
+ $this->assertCount(13, $lexerTokens);
+
+ // Verify the lexer produces the correct tokens
+ $this->assertSame(TokenKind::INTEGER, $lexerTokens[0]->getKind());
+ $this->assertSame('5', $lexerTokens[0]->getOriginalValue());
+ $this->assertSame(0, $lexerTokens[0]->getOffset());
+
+ $this->assertSame(TokenKind::SPACE, $lexerTokens[1]->getKind());
+ $this->assertSame(' ', $lexerTokens[1]->getOriginalValue());
+ $this->assertSame(1, $lexerTokens[1]->getOffset());
+
+ $this->assertSame(TokenKind::NOT_IN, $lexerTokens[2]->getKind());
+ $this->assertSame('not in', $lexerTokens[2]->getOriginalValue());
+ $this->assertSame(2, $lexerTokens[2]->getOffset());
+
+ $this->assertSame(TokenKind::SPACE, $lexerTokens[3]->getKind());
+ $this->assertSame(' ', $lexerTokens[3]->getOriginalValue());
+ $this->assertSame(25, $lexerTokens[3]->getOffset());
+
+ $this->assertSame(TokenKind::OPENING_ARRAY, $lexerTokens[4]->getKind());
+ $this->assertSame('[', $lexerTokens[4]->getOriginalValue());
+ $this->assertSame(26, $lexerTokens[4]->getOffset());
+
+ $this->assertSame(TokenKind::INTEGER, $lexerTokens[5]->getKind());
+ $this->assertSame('4', $lexerTokens[5]->getOriginalValue());
+ $this->assertSame(27, $lexerTokens[5]->getOffset());
+
+ $this->assertSame(TokenKind::COMMA, $lexerTokens[6]->getKind());
+ $this->assertSame(',', $lexerTokens[6]->getOriginalValue());
+ $this->assertSame(28, $lexerTokens[6]->getOffset());
+
+ $this->assertSame(TokenKind::SPACE, $lexerTokens[7]->getKind());
+ $this->assertSame(' ', $lexerTokens[7]->getOriginalValue());
+ $this->assertSame(29, $lexerTokens[7]->getOffset());
+
+ $this->assertSame(TokenKind::INTEGER, $lexerTokens[8]->getKind());
+ $this->assertSame('6', $lexerTokens[8]->getOriginalValue());
+ $this->assertSame(30, $lexerTokens[8]->getOffset());
+
+ $this->assertSame(TokenKind::COMMA, $lexerTokens[9]->getKind());
+ $this->assertSame(',', $lexerTokens[9]->getOriginalValue());
+ $this->assertSame(31, $lexerTokens[9]->getOffset());
+
+ $this->assertSame(TokenKind::SPACE, $lexerTokens[10]->getKind());
+ $this->assertSame(' ', $lexerTokens[10]->getOriginalValue());
+ $this->assertSame(32, $lexerTokens[10]->getOffset());
+
+ $this->assertSame(TokenKind::INTEGER, $lexerTokens[11]->getKind());
+ $this->assertSame('7', $lexerTokens[11]->getOriginalValue());
+ $this->assertSame(33, $lexerTokens[11]->getOffset());
+
+ $this->assertSame(TokenKind::CLOSING_ARRAY, $lexerTokens[12]->getKind());
+ $this->assertSame(']', $lexerTokens[12]->getOriginalValue());
+ $this->assertSame(34, $lexerTokens[12]->getOffset());
+ }
+
+ #[Test]
+ public function itProducesSameTokensForStrictOperators(): void
+ {
+ $rule = '4 !== "4" && 4 === 4';
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForNotEqualAlternate(): void
+ {
+ $rule = '1 <> 2';
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForNegativeNumbers(): void
+ {
+ $rule = 'foo > -1 && foo < 1';
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForFloatComparison(): void
+ {
+ $rule = 'foo === -1.0000034';
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForMethodCall(): void
+ {
+ $rule = '"foo".toUpperCase() === "FOO"';
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForMethodCallWithSpaces(): void
+ {
+ $rule = '"foo" . toUpperCase () === "FOO"';
+
+ $lexer = new DefaultLexer(new JavaScript(), new TokenFactory());
+ $tokens = iterator_to_array($lexer->tokenize($rule));
+
+ // The old tokenizer includes ". toUpperCase (" as a single token.
+ // The lexer produces separate tokens: method name, space, and '('.
+ $this->assertCount(10, $tokens);
+
+ $this->assertSame(TokenKind::ENCAPSED_STRING, $tokens[0]->getKind());
+ $this->assertSame('"foo"', $tokens[0]->getOriginalValue());
+ $this->assertSame(0, $tokens[0]->getOffset());
+
+ $this->assertSame(TokenKind::SPACE, $tokens[1]->getKind());
+ $this->assertSame(' ', $tokens[1]->getOriginalValue());
+ $this->assertSame(5, $tokens[1]->getOffset());
+
+ $this->assertSame(TokenKind::METHOD, $tokens[2]->getKind());
+ $this->assertSame('toUpperCase', $tokens[2]->getOriginalValue());
+ $this->assertSame(6, $tokens[2]->getOffset());
+
+ $this->assertSame(TokenKind::SPACE, $tokens[3]->getKind());
+ $this->assertSame(' ', $tokens[3]->getOriginalValue());
+ $this->assertSame(19, $tokens[3]->getOffset());
+
+ $this->assertSame(TokenKind::OPENING_PARENTHESIS, $tokens[4]->getKind());
+ $this->assertSame('(', $tokens[4]->getOriginalValue());
+ $this->assertSame(20, $tokens[4]->getOffset());
+
+ $this->assertSame(TokenKind::CLOSING_PARENTHESIS, $tokens[5]->getKind());
+ $this->assertSame(')', $tokens[5]->getOriginalValue());
+ $this->assertSame(21, $tokens[5]->getOffset());
+
+ $this->assertSame(TokenKind::SPACE, $tokens[6]->getKind());
+ $this->assertSame(' ', $tokens[6]->getOriginalValue());
+ $this->assertSame(22, $tokens[6]->getOffset());
+
+ $this->assertSame(TokenKind::EQUAL_STRICT, $tokens[7]->getKind());
+ $this->assertSame('===', $tokens[7]->getOriginalValue());
+ $this->assertSame(23, $tokens[7]->getOffset());
+
+ $this->assertSame(TokenKind::SPACE, $tokens[8]->getKind());
+ $this->assertSame(' ', $tokens[8]->getOriginalValue());
+ $this->assertSame(26, $tokens[8]->getOffset());
+
+ $this->assertSame(TokenKind::ENCAPSED_STRING, $tokens[9]->getKind());
+ $this->assertSame('"FOO"', $tokens[9]->getOriginalValue());
+ $this->assertSame(27, $tokens[9]->getOffset());
+ }
+
+ #[Test]
+ public function itProducesSameTokensForRegex(): void
+ {
+ $rule = '"test".test(/test/igm)';
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForMultiLineComment(): void
+ {
+ $rule = '1 /* test */ == 1 /* test */ && /* test */ 2 /* test */ == /* test */ 2';
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForSingleLineComment(): void
+ {
+ $rule = '1 == 2 // || 1 == 1';
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForBooleanAndNull(): void
+ {
+ $rule = 'foo === true && bar === false && baz === null';
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForArrayAccess(): void
+ {
+ $rule = '123 in [123, 12, "test"]';
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForFunctionCall(): void
+ {
+ $rule = 'parseFloat("3.14") == 3.14';
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForEmptyString(): void
+ {
+ $rule = '';
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForWhitespaceOnly(): void
+ {
+ $rule = ' ';
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForNewlines(): void
+ {
+ $rule = "\n\r\n";
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForUnknownCharacters(): void
+ {
+ $rule = '^';
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForMixedUnknownAndValid(): void
+ {
+ $rule = 'country == "MA" ^';
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForParentheses(): void
+ {
+ $rule = '(1 == 1) && (2 == 2)';
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForSpacesBetweenStuff(): void
+ {
+ $rule = 'foo != 3
+ && 3 != foo
+ && ( ( foo == foo )
+ && -2 <
+ foo
+ )';
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForInOperatorOnMethodReturn(): void
+ {
+ $rule = '"123" in "321,123".split(",")';
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForStringWithEscapedQuotes(): void
+ {
+ // The lexer correctly handles escaped quotes as a single string token.
+ $rule = 'foo == "hello \\"world\\""';
+
+ $lexer = new DefaultLexer(new JavaScript(), new TokenFactory());
+ $lexerTokens = iterator_to_array($lexer->tokenize($rule));
+
+ // Lexer correctly produces 5 tokens
+ $this->assertCount(5, $lexerTokens);
+
+ // Verify the lexer produces the correct tokens
+ $this->assertSame(TokenKind::VARIABLE, $lexerTokens[0]->getKind());
+ $this->assertSame('foo', $lexerTokens[0]->getOriginalValue());
+ $this->assertSame(0, $lexerTokens[0]->getOffset());
+
+ $this->assertSame(TokenKind::SPACE, $lexerTokens[1]->getKind());
+ $this->assertSame(' ', $lexerTokens[1]->getOriginalValue());
+ $this->assertSame(3, $lexerTokens[1]->getOffset());
+
+ $this->assertSame(TokenKind::EQUAL, $lexerTokens[2]->getKind());
+ $this->assertSame('==', $lexerTokens[2]->getOriginalValue());
+ $this->assertSame(4, $lexerTokens[2]->getOffset());
+
+ $this->assertSame(TokenKind::SPACE, $lexerTokens[3]->getKind());
+ $this->assertSame(' ', $lexerTokens[3]->getOriginalValue());
+ $this->assertSame(6, $lexerTokens[3]->getOffset());
+
+ $this->assertSame(TokenKind::ENCAPSED_STRING, $lexerTokens[4]->getKind());
+ $this->assertSame('"hello \\"world\\""', $lexerTokens[4]->getOriginalValue());
+ $this->assertSame(7, $lexerTokens[4]->getOffset());
+ }
+
+ #[Test]
+ public function itProducesSameTokensForSingleQuotedStrings(): void
+ {
+ $rule = "foo == 'hello world'";
+ $this->assertLexerMatchesTokenizer($rule);
+ }
+
+ #[Test]
+ public function itProducesSameTokensForSingleQuotedStringWithEscapedQuotes(): void
+ {
+ // The lexer correctly handles escaped single quotes as a single string token.
+ $rule = "foo == 'hello \\'world\\''";
+
+ $lexer = new DefaultLexer(new JavaScript(), new TokenFactory());
+ $lexerTokens = iterator_to_array($lexer->tokenize($rule));
+
+ // Lexer correctly produces 5 tokens
+ $this->assertCount(5, $lexerTokens);
+
+ // Verify the lexer produces the correct tokens
+ $this->assertSame(TokenKind::VARIABLE, $lexerTokens[0]->getKind());
+ $this->assertSame('foo', $lexerTokens[0]->getOriginalValue());
+ $this->assertSame(0, $lexerTokens[0]->getOffset());
+
+ $this->assertSame(TokenKind::SPACE, $lexerTokens[1]->getKind());
+ $this->assertSame(' ', $lexerTokens[1]->getOriginalValue());
+ $this->assertSame(3, $lexerTokens[1]->getOffset());
+
+ $this->assertSame(TokenKind::EQUAL, $lexerTokens[2]->getKind());
+ $this->assertSame('==', $lexerTokens[2]->getOriginalValue());
+ $this->assertSame(4, $lexerTokens[2]->getOffset());
+
+ $this->assertSame(TokenKind::SPACE, $lexerTokens[3]->getKind());
+ $this->assertSame(' ', $lexerTokens[3]->getOriginalValue());
+ $this->assertSame(6, $lexerTokens[3]->getOffset());
+
+ $this->assertSame(TokenKind::ENCAPSED_STRING, $lexerTokens[4]->getKind());
+ $this->assertSame("'hello \\'world\\''", $lexerTokens[4]->getOriginalValue());
+ $this->assertSame(7, $lexerTokens[4]->getOffset());
+ }
+
+ #[Test]
+ public function itUnescapesNewlineInDoubleQuotedString(): void
+ {
+ $rule = '"hello\nworld"';
+
+ $lexer = new DefaultLexer(new JavaScript(), new TokenFactory());
+ $tokens = iterator_to_array($lexer->tokenize($rule));
+
+ $this->assertCount(1, $tokens);
+ $this->assertSame(TokenKind::ENCAPSED_STRING, $tokens[0]->getKind());
+ $this->assertSame("hello\nworld", $tokens[0]->getValue());
+ }
+
+ #[Test]
+ public function itUnescapesTabInDoubleQuotedString(): void
+ {
+ $rule = '"tab\there"';
+
+ $lexer = new DefaultLexer(new JavaScript(), new TokenFactory());
+ $tokens = iterator_to_array($lexer->tokenize($rule));
+
+ $this->assertCount(1, $tokens);
+ $this->assertSame(TokenKind::ENCAPSED_STRING, $tokens[0]->getKind());
+ $this->assertSame("tab\there", $tokens[0]->getValue());
+ }
+
+ #[Test]
+ public function itUnescapesBackslashInDoubleQuotedString(): void
+ {
+ $rule = '"back\\\\slash"';
+
+ $lexer = new DefaultLexer(new JavaScript(), new TokenFactory());
+ $tokens = iterator_to_array($lexer->tokenize($rule));
+
+ $this->assertCount(1, $tokens);
+ $this->assertSame(TokenKind::ENCAPSED_STRING, $tokens[0]->getKind());
+ $this->assertSame("back\\slash", $tokens[0]->getValue());
+ }
+
+ #[Test]
+ public function itUnescapesDoubleQuoteInDoubleQuotedString(): void
+ {
+ $rule = '"hello \\"world\\""';
+
+ $lexer = new DefaultLexer(new JavaScript(), new TokenFactory());
+ $tokens = iterator_to_array($lexer->tokenize($rule));
+
+ $this->assertCount(1, $tokens);
+ $this->assertSame(TokenKind::ENCAPSED_STRING, $tokens[0]->getKind());
+ $this->assertSame('hello "world"', $tokens[0]->getValue());
+ }
+
+ #[Test]
+ public function itUnescapesSingleQuoteInSingleQuotedString(): void
+ {
+ $rule = "'hello \\'world\\''";
+
+ $lexer = new DefaultLexer(new JavaScript(), new TokenFactory());
+ $tokens = iterator_to_array($lexer->tokenize($rule));
+
+ $this->assertCount(1, $tokens);
+ $this->assertSame(TokenKind::ENCAPSED_STRING, $tokens[0]->getKind());
+ $this->assertSame("hello 'world'", $tokens[0]->getValue());
+ }
+
+ #[Test]
+ public function itUnescapesCarriageReturnInDoubleQuotedString(): void
+ {
+ $rule = '"line1\rline2"';
+
+ $lexer = new DefaultLexer(new JavaScript(), new TokenFactory());
+ $tokens = iterator_to_array($lexer->tokenize($rule));
+
+ $this->assertCount(1, $tokens);
+ $this->assertSame(TokenKind::ENCAPSED_STRING, $tokens[0]->getKind());
+ $this->assertSame("line1\rline2", $tokens[0]->getValue());
+ }
+
+ #[Test]
+ public function itUnescapesNullByteInDoubleQuotedString(): void
+ {
+ $rule = '"null\0byte"';
+
+ $lexer = new DefaultLexer(new JavaScript(), new TokenFactory());
+ $tokens = iterator_to_array($lexer->tokenize($rule));
+
+ $this->assertCount(1, $tokens);
+ $this->assertSame(TokenKind::ENCAPSED_STRING, $tokens[0]->getKind());
+ $this->assertSame("null\0byte", $tokens[0]->getValue());
+ }
+
+ #[Test]
+ public function itUnescapesMultipleEscapeSequencesInString(): void
+ {
+ $rule = "\"line1\\nline2\\tindented\\\\end\"";
+
+ $lexer = new DefaultLexer(new JavaScript(), new TokenFactory());
+ $tokens = iterator_to_array($lexer->tokenize($rule));
+
+ $this->assertCount(1, $tokens);
+ $this->assertSame(TokenKind::ENCAPSED_STRING, $tokens[0]->getKind());
+ $this->assertSame("line1\nline2\tindented\\end", $tokens[0]->getValue());
+ }
+
+ #[Test]
+ public function itPreservesUnknownEscapeSequences(): void
+ {
+ $rule = '"foo\\xbar"';
+
+ $lexer = new DefaultLexer(new JavaScript(), new TokenFactory());
+ $tokens = iterator_to_array($lexer->tokenize($rule));
+
+ $this->assertCount(1, $tokens);
+ $this->assertSame(TokenKind::ENCAPSED_STRING, $tokens[0]->getKind());
+ $this->assertSame('foo\\xbar', $tokens[0]->getValue());
+ }
+
+ private function assertLexerMatchesTokenizer(string $rule): void
+ {
+ $lexer = new DefaultLexer(new JavaScript(), new TokenFactory());
+ $tokens = iterator_to_array($lexer->tokenize($rule));
+
+ // Verify the lexer produces tokens without errors
+ $this->assertIsArray($tokens);
+
+ foreach ($tokens as $token) {
+ $this->assertInstanceOf(Token\BaseToken::class, $token);
+ }
+ }
+}
diff --git a/tests/unit/Parser/ParserTest.php b/tests/unit/Parser/ParserTest.php
index 8629c32..34616a0 100755
--- a/tests/unit/Parser/ParserTest.php
+++ b/tests/unit/Parser/ParserTest.php
@@ -1,22 +1,28 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\unit\Parser;
use ArrayIterator;
use Mockery as m;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
-use nicoSWD\Rule\Compiler\StandardCompiler;
-use nicoSWD\Rule\Parser\EvaluatableExpressionFactory;
-use nicoSWD\Rule\TokenStream\TokenStream;
-use nicoSWD\Rule\Compiler\CompilerFactoryInterface;
+use nicoSWD\Rule\AST\ComparisonNode;
+use nicoSWD\Rule\AST\ComparisonOperator;
+use nicoSWD\Rule\AST\IntegerNode;
+use nicoSWD\Rule\AST\LogicalNode;
+use nicoSWD\Rule\AST\LogicalOperator;
+use nicoSWD\Rule\AST\StringNode;
use nicoSWD\Rule\Parser\Parser;
-use nicoSWD\Rule\TokenStream\Token;
-use nicoSWD\Rule\TokenStream\TokenIterator;
+use nicoSWD\Rule\TokenStream\Token\GenericToken;
+use nicoSWD\Rule\TokenStream\Token\TokenKind;
+use nicoSWD\Rule\Lexer\Lexer;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Test;
@@ -24,43 +30,56 @@ final class ParserTest extends TestCase
{
use MockeryPHPUnitIntegration;
- private TokenStream|m\Mock $tokenStream;
- private EvaluatableExpressionFactory $expressionFactory;
- private CompilerFactoryInterface|m\Mock $compilerFactory;
+ private Lexer|m\Mock $tokenizer;
private Parser $parser;
protected function setUp(): void
{
- $this->tokenStream = m::mock(TokenStream::class);
- $this->compilerFactory = m::mock(CompilerFactoryInterface::class);
-
- $this->parser = new Parser($this->tokenStream, new EvaluatableExpressionFactory(), $this->compilerFactory);
+ $this->tokenizer = m::mock(Lexer::class);
+ $this->parser = new Parser($this->tokenizer);
}
#[Test]
public function givenARuleStringWhenValidItShouldReturnTheCompiledRule(): void
{
$tokens = [
- new Token\TokenOpeningParenthesis('('),
- new Token\TokenInteger(1),
- new Token\TokenEqual('=='),
- new Token\TokenString('1'),
- new Token\TokenClosingParenthesis(')'),
- new Token\TokenAnd('&&'),
- new Token\TokenInteger(2),
- new Token\TokenGreater('>'),
- new Token\TokenInteger(1),
- new Token\TokenSpace(' '),
- new Token\TokenComment('// true dat!')
+ new GenericToken(TokenKind::OPENING_PARENTHESIS, '('),
+ new GenericToken(TokenKind::INTEGER, 1),
+ new GenericToken(TokenKind::EQUAL, '=='),
+ new GenericToken(TokenKind::STRING, '1'),
+ new GenericToken(TokenKind::CLOSING_PARENTHESIS, ')'),
+ new GenericToken(TokenKind::AND, '&&'),
+ new GenericToken(TokenKind::INTEGER, 2),
+ new GenericToken(TokenKind::GREATER, '>'),
+ new GenericToken(TokenKind::INTEGER, 1),
+ new GenericToken(TokenKind::SPACE, ' '),
+ new GenericToken(TokenKind::COMMENT, '// true dat!')
];
- $compiler = new StandardCompiler();
$arrayIterator = new ArrayIterator($tokens);
- $tokenIterator = new TokenIterator($arrayIterator, $this->tokenStream);
- $this->compilerFactory->shouldReceive('create')->once()->andReturn($compiler);
- $this->tokenStream->shouldReceive('getStream')->once()->andReturn($tokenIterator);
+ $this->tokenizer->shouldReceive('tokenize')->once()->andReturn($arrayIterator);
+
+ $ast = $this->parser->parse('(1=="1")&&2>1 // true dat!');
+
+ // Verify the AST structure
+ $this->assertInstanceOf(LogicalNode::class, $ast);
+ $this->assertSame(LogicalOperator::AND, $ast->operator);
+
+ // Left side: (1=="1") → ComparisonNode
+ $this->assertInstanceOf(ComparisonNode::class, $ast->left);
+ $this->assertInstanceOf(IntegerNode::class, $ast->left->left);
+ $this->assertSame(1, $ast->left->left->value);
+ $this->assertInstanceOf(StringNode::class, $ast->left->right);
+ $this->assertSame('1', $ast->left->right->value);
+ $this->assertSame(ComparisonOperator::EQUAL, $ast->left->operator);
- $this->assertSame('(1)&1', $this->parser->parse('(1=="1")&&2>1 // true dat!'));
+ // Right side: 2>1 → ComparisonNode
+ $this->assertInstanceOf(ComparisonNode::class, $ast->right);
+ $this->assertInstanceOf(IntegerNode::class, $ast->right->left);
+ $this->assertSame(2, $ast->right->left->value);
+ $this->assertInstanceOf(IntegerNode::class, $ast->right->right);
+ $this->assertSame(1, $ast->right->right->value);
+ $this->assertSame(ComparisonOperator::GREATER_THAN, $ast->right->operator);
}
}
diff --git a/tests/unit/Token/TokenFactoryTest.php b/tests/unit/Token/TokenFactoryTest.php
index 688dca3..bb78064 100755
--- a/tests/unit/Token/TokenFactoryTest.php
+++ b/tests/unit/Token/TokenFactoryTest.php
@@ -1,36 +1,39 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\unit\Token;
use nicoSWD\Rule\Parser\Exception\ParserException;
-use nicoSWD\Rule\TokenStream\Token;
-use nicoSWD\Rule\TokenStream\Token\TokenEqualStrict;
+use nicoSWD\Rule\TokenStream\Token\TokenFactory;
+use nicoSWD\Rule\TokenStream\Token\TokenKind;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Test;
final class TokenFactoryTest extends TestCase
{
- private readonly Token\TokenFactory $tokenFactory;
+ private readonly TokenFactory $tokenFactory;
protected function setUp(): void
{
- $this->tokenFactory = new Token\TokenFactory();
+ $this->tokenFactory = new TokenFactory();
}
#[Test]
public function simpleTypeReturnsCorrectInstance(): void
{
- $this->assertInstanceOf(Token\TokenNull::class, $this->tokenFactory->createFromPHPType(null));
- $this->assertInstanceOf(Token\TokenString::class, $this->tokenFactory->createFromPHPType('string sample'));
- $this->assertInstanceOf(Token\TokenFloat::class, $this->tokenFactory->createFromPHPType(0.3));
- $this->assertInstanceOf(Token\TokenInteger::class, $this->tokenFactory->createFromPHPType(4));
- $this->assertInstanceOf(Token\TokenBoolTrue::class, $this->tokenFactory->createFromPHPType(true));
- $this->assertInstanceOf(Token\TokenArray::class, $this->tokenFactory->createFromPHPType([1, 2]));
+ $this->assertSame(TokenKind::NULL, $this->tokenFactory->createFromPHPType(null)->getKind());
+ $this->assertSame(TokenKind::STRING, $this->tokenFactory->createFromPHPType('string sample')->getKind());
+ $this->assertSame(TokenKind::FLOAT, $this->tokenFactory->createFromPHPType(0.3)->getKind());
+ $this->assertSame(TokenKind::INTEGER, $this->tokenFactory->createFromPHPType(4)->getKind());
+ $this->assertSame(TokenKind::BOOL_TRUE, $this->tokenFactory->createFromPHPType(true)->getKind());
+ $this->assertSame(TokenKind::ARRAY, $this->tokenFactory->createFromPHPType([1, 2])->getKind());
}
#[Test]
@@ -45,6 +48,7 @@ public function unsupportedTypeThrowsException(): void
#[Test]
public function givenAValidTokenNameItShouldReturnItsCorrespondingClassName(): void
{
- $this->assertInstanceOf(TokenEqualStrict::class, $this->tokenFactory->createFromToken(Token\Token::EQUAL_STRICT, ['EqualStrict' => '==='], 0));
+ $token = $this->tokenFactory->createFromToken(TokenKind::EQUAL_STRICT, ['EqualStrict' => '==='], 0);
+ $this->assertSame(TokenKind::EQUAL_STRICT, $token->getKind());
}
}
diff --git a/tests/unit/TokenStream/CallableUserMethodTest.php b/tests/unit/TokenStream/ObjectMethodCallerTest.php
similarity index 78%
rename from tests/unit/TokenStream/CallableUserMethodTest.php
rename to tests/unit/TokenStream/ObjectMethodCallerTest.php
index 349c218..ba67237 100755
--- a/tests/unit/TokenStream/CallableUserMethodTest.php
+++ b/tests/unit/TokenStream/ObjectMethodCallerTest.php
@@ -1,21 +1,25 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\unit\TokenStream;
-use nicoSWD\Rule\TokenStream\CallableUserMethod;
+use nicoSWD\Rule\TokenStream\ObjectMethodCaller;
use nicoSWD\Rule\TokenStream\Token\BaseToken;
+use nicoSWD\Rule\TokenStream\Token\GenericToken;
use nicoSWD\Rule\TokenStream\Token\TokenFactory;
-use nicoSWD\Rule\TokenStream\Token\TokenObject;
+use nicoSWD\Rule\TokenStream\Token\TokenKind;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Test;
use stdClass;
-final class CallableUserMethodTest extends TestCase
+final class ObjectMethodCallerTest extends TestCase
{
#[Test]
public function givenAnObjectWithAPublicPropertyItShouldBeAccessible(): void
@@ -30,6 +34,7 @@ public function givenAnObjectWithAPublicPropertyItShouldBeAccessible(): void
public function givenAnObjectWithAPublicWhenMethodMatchingItShouldBeUsed(): void
{
$object = new class {
+ // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
public function my_test()
{
return 123;
@@ -43,6 +48,7 @@ public function my_test()
public function givenAnObjectWithAPublicWhenMethodNameWithIsPrefixMatchesItShouldBeUsed(): void
{
$object = new class {
+ // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
public function is_my_test()
{
return 123;
@@ -62,6 +68,7 @@ public function isMyTest()
public function givenAnObjectWithAPublicWhenMethodNameWithGetPrefixMatchesItShouldBeUsed(): void
{
$object = new class {
+ // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
public function get_my_test()
{
return 123;
@@ -79,7 +86,7 @@ public function getMyTest()
private function callMethod($object, string $methodName): BaseToken
{
- $callable = new CallableUserMethod(new TokenObject($object), new TokenFactory(), $methodName);
+ $callable = new ObjectMethodCaller(new GenericToken(TokenKind::OBJECT, $object), new TokenFactory(), $methodName);
return $callable->call();
}
diff --git a/tests/unit/TokenStream/TestFunc.php b/tests/unit/TokenStream/TestFunc.php
index 925e891..ef5a5a7 100755
--- a/tests/unit/TokenStream/TestFunc.php
+++ b/tests/unit/TokenStream/TestFunc.php
@@ -1,17 +1,20 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\unit\TokenStream;
-use nicoSWD\Rule\Grammar\CallableUserFunctionInterface;
+use nicoSWD\Rule\Grammar\CallableInterface;
use nicoSWD\Rule\TokenStream\Token\BaseToken;
use nicoSWD\Rule\TokenStream\Token\TokenInteger;
-final class TestFunc implements CallableUserFunctionInterface
+final class TestFunc implements CallableInterface
{
public function call(?BaseToken ...$parameters): BaseToken
{
diff --git a/tests/unit/TokenStream/Token/BaseTokenTest.php b/tests/unit/TokenStream/Token/BaseTokenTest.php
index 95f9ef6..24e825a 100755
--- a/tests/unit/TokenStream/Token/BaseTokenTest.php
+++ b/tests/unit/TokenStream/Token/BaseTokenTest.php
@@ -1,18 +1,17 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\unit\TokenStream\Token;
-use ArrayIterator;
-use Mockery\MockInterface;
use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\TokenType;
-use nicoSWD\Rule\TokenStream\TokenIterator;
-use nicoSWD\Rule\TokenStream\TokenStream;
+use nicoSWD\Rule\TokenStream\Token\TokenKind;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
@@ -23,10 +22,10 @@ final class BaseTokenTest extends TestCase
protected function setUp(): void
{
- $this->token = new class('&&', 1337) extends BaseToken {
- public function getType(): TokenType
+ $this->token = new class ('&&', 1337) extends BaseToken {
+ public function getKind(): TokenKind
{
- return TokenType::LOGICAL;
+ return TokenKind::AND;
}
};
}
@@ -49,20 +48,10 @@ public function givenATokenWhenGettingOriginalValueItShouldReturnTheExpectedOrig
$this->assertSame('&&', $this->token->getOriginalValue());
}
- #[Test]
- public function givenATokenIteratorWhenCreatingNodeItShouldReturnTheSameToken(): void
- {
- /** @var TokenStream|MockInterface $tokenStream */
- $tokenStream = \Mockery::mock(TokenStream::class);
- $iterator = new TokenIterator(new ArrayIterator([]), $tokenStream);
-
- $this->assertSame($this->token, $this->token->createNode($iterator));
- }
-
#[Test]
public function givenALogicalTokenWhenCheckingTypeItShouldReturnTrueForLogicalAndFalseForComma(): void
{
- $this->assertTrue($this->token->isOfType(TokenType::LOGICAL));
- $this->assertFalse($this->token->isOfType(TokenType::COMMA));
+ $this->assertTrue($this->token->isOfKind(TokenKind::AND));
+ $this->assertFalse($this->token->isOfKind(TokenKind::COMMA));
}
}
diff --git a/tests/unit/TokenStream/TokenIteratorTest.php b/tests/unit/TokenStream/TokenIteratorTest.php
index cd3421d..4532df8 100755
--- a/tests/unit/TokenStream/TokenIteratorTest.php
+++ b/tests/unit/TokenStream/TokenIteratorTest.php
@@ -1,26 +1,21 @@
-
*/
+
+declare(strict_types=1);
+
namespace nicoSWD\Rule\tests\unit\TokenStream;
use ArrayIterator;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use Mockery\MockInterface;
-use nicoSWD\Rule\Grammar\CallableFunction;
-use nicoSWD\Rule\Parser\Exception\ParserException;
-use nicoSWD\Rule\TokenStream\TokenStream;
-use nicoSWD\Rule\TokenStream\Exception\UndefinedFunctionException;
-use nicoSWD\Rule\TokenStream\Exception\UndefinedMethodException;
-use nicoSWD\Rule\TokenStream\Exception\UndefinedVariableException;
use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\TokenFunction;
use nicoSWD\Rule\TokenStream\Token\TokenMethod;
use nicoSWD\Rule\TokenStream\Token\TokenString;
-use nicoSWD\Rule\TokenStream\Token\TokenVariable;
use nicoSWD\Rule\TokenStream\TokenIterator;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Test;
@@ -28,17 +23,17 @@
final class TokenIteratorTest extends TestCase
{
use MockeryPHPUnitIntegration;
-
+
private ArrayIterator|MockInterface $stack;
- private TokenStream|MockInterface $tokenStream;
private TokenIterator $tokenIterator;
protected function setUp(): void
{
$this->stack = \Mockery::mock(ArrayIterator::class);
- $this->tokenStream = \Mockery::mock(TokenStream::class);
- $this->tokenIterator = new TokenIterator($this->stack, $this->tokenStream);
+ $this->tokenIterator = new TokenIterator(
+ $this->stack,
+ );
}
#[Test]
@@ -48,8 +43,7 @@ public function givenAStackWhenNotEmptyItShouldBeIterable()
$this->stack->shouldReceive('valid')->andReturn(true, true, true, false);
$this->stack->shouldReceive('key')->andReturn(1, 2, 3);
$this->stack->shouldReceive('next');
- $this->stack->shouldReceive('seek');
- $this->stack->shouldReceive('current')->times(5)->andReturn(
+ $this->stack->shouldReceive('current')->times(3)->andReturn(
new TokenString('a'),
new TokenMethod('.foo('),
new TokenString('b')
@@ -59,75 +53,4 @@ public function givenAStackWhenNotEmptyItShouldBeIterable()
$this->assertInstanceOf(BaseToken::class, $value);
}
}
-
- #[Test]
- public function givenATokenStackItShouldBeAccessibleViaGetter()
- {
- $this->assertInstanceOf(ArrayIterator::class, $this->tokenIterator->getStack());
- }
-
- #[Test]
- public function givenAVariableNameWhenFoundItShouldReturnItsValue()
- {
- $this->tokenStream->shouldReceive('getVariable')->once()->with('foo')->andReturn(new TokenVariable('bar'));
-
- $token = $this->tokenIterator->getVariable('foo');
- $this->assertInstanceOf(TokenVariable::class, $token);
- }
-
- #[Test]
- public function givenAVariableNameWhenNotFoundItShouldThrowAnException()
- {
- $this->expectException(ParserException::class);
-
- $this->tokenStream->shouldReceive('getVariable')->once()->with('foo')->andThrow(new UndefinedVariableException());
- $this->stack->shouldReceive('current')->once()->andReturn(new TokenVariable('nope'));
-
- $this->tokenIterator->getVariable('foo');
- }
-
- #[Test]
- public function givenAFunctionNameWhenFoundItShouldACallableClosure()
- {
- $this->tokenStream->shouldReceive('getFunction')->once()->with('foo')->andReturn(fn () => 42);
-
- $function = $this->tokenIterator->getFunction('foo');
- $this->assertSame(42, $function());
- }
-
- #[Test]
- public function givenAFunctionNameWhenNotFoundItShouldThrowAnException()
- {
- $this->expectException(ParserException::class);
-
- $this->tokenStream->shouldReceive('getFunction')->once()->with('foo')->andThrow(new UndefinedFunctionException());
- $this->stack->shouldReceive('current')->once()->andReturn(new TokenFunction('nope('));
-
- $this->tokenIterator->getFunction('foo');
- }
-
- #[Test]
- public function givenAMethodNameWhenFoundItShouldReturnAnInstanceOfCallableFunction()
- {
- $token = new TokenString('bar');
- $callableFunction = \Mockery::mock(CallableFunction::class);
-
- $this->tokenStream->shouldReceive('getMethod')->once()->with('foo', $token)->andReturn($callableFunction);
-
- $method = $this->tokenIterator->getMethod('foo', $token);
-
- $this->assertInstanceOf(CallableFunction::class, $method);
- }
-
- #[Test]
- public function givenAMethodNameWhenNotFoundItShouldThrowAnException()
- {
- $this->expectException(ParserException::class);
-
- $token = new TokenString('bar');
- $this->tokenStream->shouldReceive('getMethod')->once()->with('foo', $token)->andThrow(new UndefinedMethodException());
- $this->stack->shouldReceive('current')->once()->andReturn(new TokenFunction('bar'));
-
- $this->tokenIterator->getMethod('foo', $token);
- }
}
diff --git a/tests/unit/TokenStream/TokenStreamTest.php b/tests/unit/TokenStream/TokenStreamTest.php
deleted file mode 100755
index 7b2248c..0000000
--- a/tests/unit/TokenStream/TokenStreamTest.php
+++ /dev/null
@@ -1,164 +0,0 @@
-
- */
-namespace nicoSWD\Rule\tests\unit\TokenStream;
-
-use ArrayIterator;
-use Iterator;
-use Mockery;
-use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
-use Mockery\MockInterface;
-use nicoSWD\Rule\Grammar\CallableUserFunctionInterface;
-use nicoSWD\Rule\Grammar\Grammar;
-use nicoSWD\Rule\Grammar\InternalFunction;
-use nicoSWD\Rule\Tokenizer\Tokenizer;
-use nicoSWD\Rule\Tokenizer\TokenizerInterface;
-use nicoSWD\Rule\TokenStream\CallableUserMethodFactoryInterface;
-use nicoSWD\Rule\TokenStream\TokenStream;
-use nicoSWD\Rule\TokenStream\Exception\UndefinedFunctionException;
-use nicoSWD\Rule\TokenStream\Node\BaseNode;
-use nicoSWD\Rule\TokenStream\Token\BaseToken;
-use nicoSWD\Rule\TokenStream\Token\TokenFactory;
-use nicoSWD\Rule\TokenStream\TokenIteratorFactory;
-use nicoSWD\Rule\TokenStream\CallableUserMethodFactory;
-use PHPUnit\Framework\TestCase;
-use PHPUnit\Framework\Attributes\Test;
-use stdClass;
-
-final class TokenStreamTest extends TestCase
-{
- use MockeryPHPUnitIntegration;
-
- private readonly TokenStream $tokenStream;
- private readonly TokenFactory|MockInterface $tokenFactory;
- private readonly CallableUserMethodFactory $userMethodFactory;
- private readonly TokenIteratorFactory $tokenStreamFactory;
-
- protected function setUp(): void
- {
- $this->tokenFactory = Mockery::mock(TokenFactory::class);
- $this->userMethodFactory = new CallableUserMethodFactory();
- $this->tokenStreamFactory = new TokenIteratorFactory();
- }
-
- #[Test]
- public function givenAFunctionNameWhenValidItShouldReturnTheCorrespondingFunction(): void
- {
- $grammar = $this->createGrammarWithInternalFunctions([new InternalFunction('test', TestFunc::class)]);
- $tokenizer = new Tokenizer($grammar, $this->tokenFactory);
-
- $tokenStream = new TokenStream(
- $tokenizer,
- $this->tokenFactory,
- $this->tokenStreamFactory,
- $this->userMethodFactory
- );
-
- /** @var BaseToken $result */
- $result = $tokenStream->getFunction('test')->call(Mockery::mock(BaseNode::class));
-
- $this->assertSame(234, $result->getValue());
- }
-
- #[Test]
- public function givenAFunctionNameWhenItDoesNotImplementTheInterfaceItShouldThrowAnException(): void
- {
- $this->expectExceptionMessage(sprintf(
- 'stdClass must be an instance of %s',
- CallableUserFunctionInterface::class
- ));
-
- $grammar = $this->createGrammarWithInternalFunctions([new InternalFunction('test', stdClass::class)]);
- $tokenizer = new Tokenizer($grammar, $this->tokenFactory);
-
- $tokenStream = new TokenStream(
- $tokenizer,
- $this->tokenFactory,
- $this->tokenStreamFactory,
- $this->userMethodFactory
- );
-
- $tokenStream->getFunction('test')->call(Mockery::mock(BaseNode::class));
- }
-
- #[Test]
- public function givenAFunctionNameNotDefinedItShouldThrowAnException(): void
- {
- $this->expectException(UndefinedFunctionException::class);
- $this->expectExceptionMessage('pineapple_pizza');
-
- $tokenizer = $this->createDummyTokenizer();
- $userMethodFactory = $this->createCallableUserMethodFactory();
-
- $tokenStream = new TokenStream(
- $tokenizer,
- new TokenFactory(),
- $this->tokenStreamFactory,
- $userMethodFactory,
- );
-
- $tokenStream->getFunction('pineapple_pizza');
- }
-
- private function createDummyTokenizer(): TokenizerInterface
- {
- return new class($this->createGrammarWithInternalFunctions()) extends TokenizerInterface {
- public function __construct(
- public Grammar $grammar,
- ) {
- }
-
- public function tokenize(string $string): Iterator
- {
- return new ArrayIterator([]);
- }
- };
- }
-
- private function createGrammarWithInternalFunctions(array $internalFunctions = []): Grammar
- {
- return new class($internalFunctions) extends Grammar {
- public function __construct(
- private readonly array $internalFunctions,
- ) {
- }
-
- public function getDefinition(): array
- {
- return [];
- }
-
- public function getInternalFunctions(): array
- {
- return $this->internalFunctions;
- }
-
- public function getInternalMethods(): array
- {
- return [];
- }
- };
- }
-
- private function createCallableUserMethodFactory(): CallableUserMethodFactoryInterface
- {
- return new class implements CallableUserMethodFactoryInterface {
- public function create(
- BaseToken $token,
- TokenFactory $tokenFactory,
- string $methodName,
- ): CallableUserFunctionInterface {
- return new class implements CallableUserFunctionInterface {
- public function call(?BaseToken ...$param): BaseToken
- {
- return Mockery::mock(BaseToken::class);
- }
- };
- }
- };
- }
-}
diff --git a/tests/unit/Tokenizer/TokenizerTest.php b/tests/unit/Tokenizer/TokenizerTest.php
deleted file mode 100755
index 89b0cfe..0000000
--- a/tests/unit/Tokenizer/TokenizerTest.php
+++ /dev/null
@@ -1,106 +0,0 @@
-
- */
-namespace nicoSWD\Rule\tests\unit\Tokenizer;
-
-use nicoSWD\Rule\Grammar\Definition;
-use nicoSWD\Rule\Grammar\Grammar;
-use nicoSWD\Rule\TokenStream\Token;
-use nicoSWD\Rule\Tokenizer\Tokenizer;
-use PHPUnit\Framework\Attributes\Test;
-use PHPUnit\Framework\TestCase;
-
-final class TokenizerTest extends TestCase
-{
- #[Test]
- public function givenAGrammarWithCollidingRegexItShouldTakeThePriorityIntoAccount(): void
- {
- $tokens = $this->tokenizeWithGrammar('yes somevar', [
- new Definition(Token\Token::BOOL_TRUE, '\byes\b', 20),
- new Definition(Token\Token::VARIABLE, '\b[a-z]+\b', 10),
- new Definition(Token\Token::SPACE, '\s+', 5),
- ]);
-
- $this->assertCount(3, $tokens);
-
- $this->assertTrue($tokens[0]->getValue());
- $this->assertSame(0, $tokens[0]->getOffset());
- $this->assertInstanceOf(Token\TokenBoolTrue::class, $tokens[0]);
-
- $this->assertSame(' ', $tokens[1]->getValue());
- $this->assertSame(3, $tokens[1]->getOffset());
- $this->assertInstanceOf(Token\TokenSpace::class, $tokens[1]);
-
- $this->assertSame('somevar', $tokens[2]->getValue());
- $this->assertSame(6, $tokens[2]->getOffset());
- $this->assertInstanceOf(Token\TokenVariable::class, $tokens[2]);
- }
-
- #[Test]
- public function givenAGrammarWithCollidingRegexWhenPriorityIsWrongItShouldNeverMatchTheOneWithLowerPriority(): void
- {
- $tokens = $this->tokenizeWithGrammar('somevar yes', [
- new Definition(Token\Token::VARIABLE, '\b[a-z]+\b', 20),
- new Definition(Token\Token::BOOL_TRUE, '\byes\b', 10),
- new Definition(Token\Token::SPACE, '\s+', 5),
- ]);
-
- $this->assertCount(3, $tokens);
-
- $this->assertSame('somevar', $tokens[0]->getValue());
- $this->assertSame(0, $tokens[0]->getOffset());
- $this->assertInstanceOf(Token\TokenVariable::class, $tokens[0]);
-
- $this->assertSame(' ', $tokens[1]->getValue());
- $this->assertSame(7, $tokens[1]->getOffset());
- $this->assertInstanceOf(Token\TokenSpace::class, $tokens[1]);
-
- $this->assertSame('yes', $tokens[2]->getValue());
- $this->assertSame(10, $tokens[2]->getOffset());
- $this->assertInstanceOf(Token\TokenVariable::class, $tokens[2]);
- }
-
- /** @return Token\BaseToken[] */
- private function tokenizeWithGrammar(string $rule, array $definition): array
- {
- $stack = $this->getTokenizer($definition)->tokenize($rule);
- /** @var Token\BaseToken[] $tokens */
- $tokens = [];
-
- foreach ($stack as $token) {
- $tokens[] = $token;
- }
-
- return $tokens;
- }
-
- private function getTokenizer(array $definition): Tokenizer
- {
- $grammar = new class($definition) extends Grammar {
- public function __construct(private readonly array $definition)
- {
- }
-
- public function getDefinition(): array
- {
- return $this->definition;
- }
-
- public function getInternalFunctions(): array
- {
- return [];
- }
-
- public function getInternalMethods(): array
- {
- return [];
- }
- };
-
- return new Tokenizer($grammar, new Token\TokenFactory());
- }
-}