feature #2966 Make the "in" operator and <,>,<=,>=,==,!= more strict when comparing strings and integers/floats (fabpot)

This PR was merged into the 3.x branch.

Discussion
----------

Make the "in" operator and <,>,<=,>=,==,!= more strict when comparing strings and integers/floats

The PHP non-strict comparison operator has some counter-intuitive behaviors, which we inherit in Twig. For instance, `{{ 'text' in [0] }}` returns `true`.

PHP is probably going to fix it in version 8: https://wiki.php.net/rfc/string_to_number_comparison

I propose to implement the same semantics in Twig for 3.0.

closes #2824, closes #341, closes #340

Commits
-------

58923b09 made the in, <, >, <=, >=, ==, and != operators more strict when comparing strings and integers/floats
This commit is contained in:
Fabien Potencier
2019-04-29 19:19:57 +02:00
11 changed files with 269 additions and 21 deletions
+1
View File
@@ -1,5 +1,6 @@
* 3.0.0 (2019-XX-XX)
* made the in, <, >, <=, >=, ==, and != operators more strict when comparing strings and integers/floats
* removed the "filter" tag
* added type hints everywhere
* changed Environment::resolveTemplate() to always return a TemplateWrapper instance
+87 -17
View File
@@ -907,31 +907,101 @@ function twig_in_filter($value, $compare)
$value = (string) $value;
}
if (\is_array($compare)) {
return \in_array($value, $compare, \is_object($value) || \is_resource($value));
} elseif (\is_string($compare) && (\is_string($value) || \is_int($value) || \is_float($value))) {
return '' === $value || false !== strpos($compare, (string) $value);
} elseif ($compare instanceof \Traversable) {
if (\is_object($value) || \is_resource($value)) {
foreach ($compare as $item) {
if ($item === $value) {
return true;
}
}
} else {
foreach ($compare as $item) {
if ($item == $value) {
return true;
}
}
if (\is_string($compare)) {
if (\is_string($value) || \is_int($value) || \is_float($value)) {
return '' === $value || false !== strpos($compare, (string) $value);
}
return false;
}
if (!is_iterable($compare)) {
return false;
}
if (\is_object($value) || \is_resource($value)) {
if (!is_array($compare)) {
foreach ($compare as $item) {
if ($item === $value) {
return true;
}
}
return false;
}
return in_array($value, $compare, true);
}
foreach ($compare as $item) {
if (0 === twig_compare($value, $item)) {
return true;
}
}
return false;
}
/**
* Compares two values using a more strict version of the PHP non-strict comparison operator.
*
* @see https://wiki.php.net/rfc/string_to_number_comparison
* @see https://wiki.php.net/rfc/trailing_whitespace_numerics
*
* @internal
*/
function twig_compare($a, $b)
{
// int <=> string
if (is_int($a) && is_string($b)) {
$b = trim($b);
if (!is_numeric($b)) {
return (string) $a <=> $b;
}
if ((int) $b == $b) {
return $a <=> (int) $b;
} else {
return (float) $a <=> (float) $b;
}
}
if (is_string($a) && is_int($b)) {
$a = trim($a);
if (!is_numeric($a)) {
return $a <=> (string) $b;
}
if ((int) $a == $a) {
return (int) $a <=> $b;
} else {
return (float) $a <=> (float) $b;
}
}
// float <=> string
if (is_float($a) && is_string($b)) {
if (is_nan($a)) {
return 1;
}
if (!is_numeric($b)) {
return (string) $a <=> $b;
}
return (float) $a <=> $b;
}
if (is_float($b) && is_string($a)) {
if (is_nan($b)) {
return 1;
}
if (!is_numeric($a)) {
return $a <=> (string) $b;
}
return (float) $a <=> $b;
}
// fallback to <=>
return $b <=> $a;
}
/**
* Returns a trimmed string.
*
@@ -15,6 +15,23 @@ use Twig\Compiler;
class EqualBinary extends AbstractBinary
{
public function compile(Compiler $compiler): void
{
if (\PHP_VERSION_ID >= 80000) {
parent::compile($compiler);
return;
}
$compiler
->raw('0 === twig_compare(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))
->raw(')')
;
}
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('==');
@@ -15,6 +15,23 @@ use Twig\Compiler;
class GreaterBinary extends AbstractBinary
{
public function compile(Compiler $compiler): void
{
if (\PHP_VERSION_ID >= 80000) {
parent::compile($compiler);
return;
}
$compiler
->raw('-1 === twig_compare(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))
->raw(')')
;
}
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('>');
@@ -15,6 +15,23 @@ use Twig\Compiler;
class GreaterEqualBinary extends AbstractBinary
{
public function compile(Compiler $compiler): void
{
if (\PHP_VERSION_ID >= 80000) {
parent::compile($compiler);
return;
}
$compiler
->raw('0 >= twig_compare(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))
->raw(')')
;
}
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('>=');
+17
View File
@@ -15,6 +15,23 @@ use Twig\Compiler;
class LessBinary extends AbstractBinary
{
public function compile(Compiler $compiler): void
{
if (\PHP_VERSION_ID >= 80000) {
parent::compile($compiler);
return;
}
$compiler
->raw('1 === twig_compare(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))
->raw(')')
;
}
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('<');
@@ -15,6 +15,23 @@ use Twig\Compiler;
class LessEqualBinary extends AbstractBinary
{
public function compile(Compiler $compiler): void
{
if (\PHP_VERSION_ID >= 80000) {
parent::compile($compiler);
return;
}
$compiler
->raw('0 <= twig_compare(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))
->raw(')')
;
}
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('<=');
@@ -15,6 +15,23 @@ use Twig\Compiler;
class NotEqualBinary extends AbstractBinary
{
public function compile(Compiler $compiler): void
{
if (\PHP_VERSION_ID >= 80000) {
parent::compile($compiler);
return;
}
$compiler
->raw('0 !== twig_compare(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))
->raw(')')
;
}
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('!=');
+61
View File
@@ -278,6 +278,67 @@ class Twig_Tests_Extension_CoreTest extends \PHPUnit\Framework\TestCase
[[], new \ArrayIterator([1, 2]), 3],
];
}
/**
* @dataProvider provideCompareCases
*/
public function testCompare($expected, $a, $b)
{
$this->assertSame($expected, twig_compare($a, $b));
$this->assertSame($expected, -twig_compare($b, $a));
}
public function testCompareNAN()
{
$this->assertSame(1, twig_compare(NAN, 'NAN'));
$this->assertSame(1, twig_compare('NAN', NAN));
$this->assertSame(1, twig_compare(NAN, 'foo'));
$this->assertSame(1, twig_compare('foo', NAN));
}
public function provideCompareCases()
{
return [
[0, 'a', 'a'],
// from https://wiki.php.net/rfc/string_to_number_comparison
[0, 0, '0'],
[0, 0, '0.0'],
[-1, 0, 'foo'],
[1, 0, ''],
[0, 42, ' 42'],
[-1, 42, '42foo'],
[0, '0', '0'],
[0, '0', '0.0'],
[1, '0', 'foo'],
[-1, '0', ''],
[0, '42', ' 42'],
[1, '42', '42foo'],
[0, 42, '000042'],
[0, 42, '42.0'],
[0, 42.0, '+42.0E0'],
[0, 0, '0e214987142012'],
[0, '42', '000042'],
[0, '42', '42.0'],
[0, '42.0', '+42.0E0'],
[0, '0', '0e214987142012'],
[0, 42, ' 42'],
[0, 42, '42 '],
[-1, 42, '42abc'],
[-1, 42, 'abc42'],
[-1, 0, 'abc42'],
[0, INF, 'INF'],
[0, -INF, '-INF'],
[0, INF, '1e1000'],
[0, -INF,'-1e1000'],
];
}
}
function foo_escaper_for_test(Environment $env, $string, $charset)
+4 -4
View File
@@ -16,10 +16,10 @@ Twig supports the in operator
{{ true in [0, 1] ? 'OK' : 'KO' }}
{{ '0' in [0, 1] ? 'OK' : 'KO' }}
{{ '0' in [1, 0] ? 'OK' : 'KO' }}
{{ '' in [0, 1] ? 'OK' : 'KO' }}
{{ '' in [1, 0] ? 'OK' : 'KO' }}
{{ 0 in ['', 1] ? 'OK' : 'KO' }}
{{ 0 in [1, ''] ? 'OK' : 'KO' }}
{{ '' in [0, 1] ? 'KO' : 'OK' }}
{{ '' in [1, 0] ? 'KO' : 'OK' }}
{{ 0 in ['', 1] ? 'KO' : 'OK' }}
{{ 0 in [1, ''] ? 'KO' : 'OK' }}
{{ '' in 'foo' ? 'OK' : 'KO' }}
{{ 0 in 'foo' ? 'KO' : 'OK' }}
@@ -0,0 +1,14 @@
--TEST--
Twig supports the in operator when using iterators
--TEMPLATE--
{{ foo in iter ? 'OK' : 'KO' }}
--DATA--
$foo = new TwigTestFoo();
$bar = new TwigTestFoo();
$foo->position = $bar;
$bar->position = $foo;
return ['foo' => $foo, 'iter' => new \ArrayIterator([$bar, $foo])]
--EXPECT--
OK