From bdd2655e6e93beb082ee2f780fb0cb0da648002f Mon Sep 17 00:00:00 2001
From: David Grudl <david@grudl.com>
Date: Wed, 19 Oct 2022 04:59:48 +0200
Subject: [PATCH] operator 'in' supports strings

- InRangeNode renamed to InNode
---
 .../{InRangeNode.php => InNode.php}           |  6 ++---
 src/Latte/Compiler/TagParserData.php          |  2 +-
 src/Latte/Runtime/Filters.php                 | 11 ++++++++
 tests/common/TagParser.parseArguments().phpt  |  8 +++---
 tests/filters/contains.phpt                   | 26 +++++++++++++++++++
 tests/phpParser/in.phpt                       |  8 +++---
 tests/phpPrint/operators.phpt                 |  4 +--
 7 files changed, 51 insertions(+), 14 deletions(-)
 rename src/Latte/Compiler/Nodes/Php/Expression/{InRangeNode.php => InNode.php} (90%)
 create mode 100644 tests/filters/contains.phpt

diff --git a/src/Latte/Compiler/Nodes/Php/Expression/InRangeNode.php b/src/Latte/Compiler/Nodes/Php/Expression/InNode.php
similarity index 90%
rename from src/Latte/Compiler/Nodes/Php/Expression/InRangeNode.php
rename to src/Latte/Compiler/Nodes/Php/Expression/InNode.php
index 8bf51ef6bd..c865cf9b2f 100644
--- a/src/Latte/Compiler/Nodes/Php/Expression/InRangeNode.php
+++ b/src/Latte/Compiler/Nodes/Php/Expression/InNode.php
@@ -14,7 +14,7 @@
 use Latte\Compiler\PrintContext;
 
 
-class InRangeNode extends ExpressionNode
+class InNode extends ExpressionNode
 {
 	public function __construct(
 		public ExpressionNode $needle,
@@ -26,11 +26,11 @@ public function __construct(
 
 	public function print(PrintContext $context): string
 	{
-		return 'in_array('
+		return 'LR\Filters::contains('
 			. $this->needle->print($context)
 			. ', '
 			. $this->haystack->print($context)
-			. ', true)';
+			. ')';
 	}
 
 
diff --git a/src/Latte/Compiler/TagParserData.php b/src/Latte/Compiler/TagParserData.php
index 1850a4f24d..af658697fe 100644
--- a/src/Latte/Compiler/TagParserData.php
+++ b/src/Latte/Compiler/TagParserData.php
@@ -482,7 +482,7 @@ protected function reduce(int $rule, int $pos): void
 			124 => fn() => $this->semValue = new Expression\BinaryOpNode($this->semStack[$pos - 2], '<<', $this->semStack[$pos], $this->startTokenStack[$pos - 2]->position),
 			125 => fn() => $this->semValue = new Expression\BinaryOpNode($this->semStack[$pos - 2], '>>', $this->semStack[$pos], $this->startTokenStack[$pos - 2]->position),
 			126 => fn() => $this->semValue = new Expression\BinaryOpNode($this->semStack[$pos - 2], '**', $this->semStack[$pos], $this->startTokenStack[$pos - 2]->position),
-			127 => fn() => $this->semValue = new Expression\InRangeNode($this->semStack[$pos - 2], $this->semStack[$pos], $this->startTokenStack[$pos - 2]->position),
+			127 => fn() => $this->semValue = new Expression\InNode($this->semStack[$pos - 2], $this->semStack[$pos], $this->startTokenStack[$pos - 2]->position),
 			128 => fn() => $this->semValue = new Expression\UnaryOpNode($this->semStack[$pos], '+', $this->startTokenStack[$pos - 1]->position),
 			129 => fn() => $this->semValue = new Expression\UnaryOpNode($this->semStack[$pos], '-', $this->startTokenStack[$pos - 1]->position),
 			130, 131 => fn() => $this->semValue = new Expression\NotNode($this->semStack[$pos], $this->startTokenStack[$pos - 1]->position),
diff --git a/src/Latte/Runtime/Filters.php b/src/Latte/Runtime/Filters.php
index 6509ce1e05..1e128e914c 100644
--- a/src/Latte/Runtime/Filters.php
+++ b/src/Latte/Runtime/Filters.php
@@ -174,6 +174,17 @@ public static function escapeHtmlRawText($s): string
 	}
 
 
+	/**
+	 * Determine if a string or array contains a given needle.
+	 */
+	public static function contains(mixed $needle, array|string $haystack): bool
+	{
+		return is_array($haystack)
+			? in_array($needle, $haystack, true)
+			: str_contains($haystack, (string) $needle);
+	}
+
+
 	/**
 	 * Converts ... to ...
 	 */
diff --git a/tests/common/TagParser.parseArguments().phpt b/tests/common/TagParser.parseArguments().phpt
index 28420d5e8b..f1c6a226ea 100644
--- a/tests/common/TagParser.parseArguments().phpt
+++ b/tests/common/TagParser.parseArguments().phpt
@@ -94,10 +94,10 @@ test('inline modifiers', function () {
 
 
 test('in operator', function () {
-	Assert::same("in_array(\$a, ['a', 'b'], true), 1", formatArgs('$a in [a, b], 1'));
-	Assert::same('$a, in_array($b->func(), [1, 2], true)', formatArgs('$a, $b->func() in [1, 2]'));
-	Assert::same('$a, in_array($b[1], [1, 2], true)', formatArgs('$a, $b[1] in [1, 2]'));
-	Assert::same('in_array($b, [1, [2], 3], true)', formatArgs('$b in [1, [2], 3]'));
+	Assert::same("LR\\Filters::contains(\$a, ['a', 'b']), 1", formatArgs('$a in [a, b], 1'));
+	Assert::same('$a, LR\Filters::contains($b->func(), [1, 2])', formatArgs('$a, $b->func() in [1, 2]'));
+	Assert::same('$a, LR\Filters::contains($b[1], [1, 2])', formatArgs('$a, $b[1] in [1, 2]'));
+	Assert::same('LR\Filters::contains($b, [1, [2], 3])', formatArgs('$b in [1, [2], 3]'));
 });
 
 
diff --git a/tests/filters/contains.phpt b/tests/filters/contains.phpt
new file mode 100644
index 0000000000..17f7002931
--- /dev/null
+++ b/tests/filters/contains.phpt
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * Test: Latte\Runtime\Filters::contains
+ */
+
+declare(strict_types=1);
+
+use Latte\Runtime\Filters;
+use Tester\Assert;
+
+require __DIR__ . '/../bootstrap.php';
+
+
+Assert::false(Filters::contains(null, []));
+Assert::true(Filters::contains(null, [null]));
+Assert::false(Filters::contains(1, ['1']));
+Assert::true(Filters::contains(1, [1]));
+
+Assert::true(Filters::contains('', ''));
+Assert::true(Filters::contains('', 'abcd'));
+Assert::false(Filters::contains('bc', ''));
+Assert::true(Filters::contains('bc', 'abcd'));
+Assert::true(Filters::contains(null, ''));
+Assert::true(Filters::contains(1, '123'));
+Assert::false(Filters::contains(1, '23'));
diff --git a/tests/phpParser/in.phpt b/tests/phpParser/in.phpt
index 64fed9435d..46345add6e 100644
--- a/tests/phpParser/in.phpt
+++ b/tests/phpParser/in.phpt
@@ -27,7 +27,7 @@ __halt_compiler();
 Latte\Compiler\Nodes\Php\Expression\ArrayNode
    items: array (3)
    |  0 => Latte\Compiler\Nodes\Php\Expression\ArrayItemNode
-   |  |  value: Latte\Compiler\Nodes\Php\Expression\InRangeNode
+   |  |  value: Latte\Compiler\Nodes\Php\Expression\InNode
    |  |  |  needle: Latte\Compiler\Nodes\Php\Expression\VariableNode
    |  |  |  |  name: 'a'
    |  |  |  |  position: 1:1 (offset 0)
@@ -41,7 +41,7 @@ Latte\Compiler\Nodes\Php\Expression\ArrayNode
    |  |  position: 1:1 (offset 0)
    |  1 => Latte\Compiler\Nodes\Php\Expression\ArrayItemNode
    |  |  value: Latte\Compiler\Nodes\Php\Expression\BinaryOpNode
-   |  |  |  left: Latte\Compiler\Nodes\Php\Expression\InRangeNode
+   |  |  |  left: Latte\Compiler\Nodes\Php\Expression\InNode
    |  |  |  |  needle: Latte\Compiler\Nodes\Php\Expression\VariableNode
    |  |  |  |  |  name: 'a'
    |  |  |  |  |  position: 4:1 (offset 28)
@@ -50,7 +50,7 @@ Latte\Compiler\Nodes\Php\Expression\ArrayNode
    |  |  |  |  |  position: 4:7 (offset 34)
    |  |  |  |  position: 4:1 (offset 28)
    |  |  |  operator: '||'
-   |  |  |  right: Latte\Compiler\Nodes\Php\Expression\InRangeNode
+   |  |  |  right: Latte\Compiler\Nodes\Php\Expression\InNode
    |  |  |  |  needle: Latte\Compiler\Nodes\Php\Expression\VariableNode
    |  |  |  |  |  name: 'c'
    |  |  |  |  |  position: 4:13 (offset 40)
@@ -69,7 +69,7 @@ Latte\Compiler\Nodes\Php\Expression\ArrayNode
    |  |  |  |  name: 'a'
    |  |  |  |  position: 5:1 (offset 50)
    |  |  |  expr: Latte\Compiler\Nodes\Php\Expression\NotNode
-   |  |  |  |  expr: Latte\Compiler\Nodes\Php\Expression\InRangeNode
+   |  |  |  |  expr: Latte\Compiler\Nodes\Php\Expression\InNode
    |  |  |  |  |  needle: Latte\Compiler\Nodes\Php\Expression\BinaryOpNode
    |  |  |  |  |  |  left: Latte\Compiler\Nodes\Php\Scalar\IntegerNode
    |  |  |  |  |  |  |  value: 10
diff --git a/tests/phpPrint/operators.phpt b/tests/phpPrint/operators.phpt
index 4e646846c9..40c912dfc2 100644
--- a/tests/phpPrint/operators.phpt
+++ b/tests/phpPrint/operators.phpt
@@ -154,7 +154,7 @@ $a xor $b,
 $a or $b,
 $a instanceof Foo,
 $a instanceof $b,
-in_array($a, $b, true),
-in_array(!$a, $b, true) && !in_array($a + 2, $b, true),
+LR\Filters::contains($a, $b),
+LR\Filters::contains(!$a, $b) && !LR\Filters::contains($a + 2, $b),
 !$a,
 !($a > $b) && !($c == !$d)