Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* Fixed to_number() to parse number strings using the JSON number grammar.
* Fixed reverse() and string slicing to operate on UTF-8 characters rather than bytes.
* Fixed slicing of array-like (ArrayAccess + Countable) values.
* Fixed equality and contains() to use JSON semantics, e.g. 1 == 1.0 is now true.
* Fixed the compiled runtime to apply JMESPath truthiness to || and &&.
* Fixed @(foo), foo[-] and oversized index literals to throw syntax errors.
* Fixed PHP warnings emitted while parsing certain invalid expressions.
Expand Down
16 changes: 11 additions & 5 deletions src/FnDispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,18 @@ private function fn_contains(array $args)
{
$this->validate('contains', $args, [['string', 'array'], ['any']]);
if (is_array($args[0])) {
return in_array($args[1], $args[0]);
} elseif (is_string($args[1])) {
return mb_strpos($args[0], $args[1], 0, 'UTF-8') !== false;
} else {
return null;
foreach ($args[0] as $value) {
if (Utils::isEqual($value, $args[1])) {
return true;
}
}

return false;
}

return is_string($args[1])
? mb_strpos($args[0], $args[1], 0, 'UTF-8') !== false
: false;
}

private function fn_ends_with(array $args)
Expand Down
42 changes: 34 additions & 8 deletions src/Utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,10 @@ public static function isArray($value)
}

/**
* JSON aware value comparison function.
* JSON-semantic equality: one number type, structural comparison for arrays
* and objects, and no object key-order sensitivity.
* Empty arrays and empty objects compare equal because PHP cannot represent
* that distinction after associative JSON decoding.
*
* @param mixed $a First value to compare
* @param mixed $b Second value to compare
Expand All @@ -115,15 +118,38 @@ public static function isArray($value)
*/
public static function isEqual($a, $b)
{
if ($a === $b) {
$typeA = self::type($a);
$typeB = self::type($b);

if ($typeA !== $typeB) {
return ($typeA === 'array' || $typeA === 'object')
&& ($typeB === 'array' || $typeB === 'object')
&& (array) $a === []
&& (array) $b === [];
}

if ($typeA === 'number') {
return $a == $b;
}

if ($typeA === 'array' || $typeA === 'object') {
$a = (array) $a;
$b = (array) $b;

if (count($a) !== count($b)) {
return false;
}

foreach ($a as $key => $value) {
if (!array_key_exists($key, $b) || !self::isEqual($value, $b[$key])) {
return false;
}
}

return true;
} elseif ($a instanceof \stdClass) {
return self::isEqual((array) $a, $b);
} elseif ($b instanceof \stdClass) {
return self::isEqual($a, (array) $b);
} else {
return false;
}

return $a === $b;
}

/**
Expand Down
13 changes: 13 additions & 0 deletions tests/CompilerRuntimeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,19 @@ public function testRuntimesUseJmesPathTruthinessForEmptyObjects(): void
}
}

public function testRuntimesUseJsonSemanticEqualityForNumbers(): void
{
$dir = $this->createTempDir();

try {
foreach ([new AstRuntime(), new CompilerRuntime($dir)] as $runtime) {
$this->assertTrue($runtime('`1` == `1.0`', null));
}
} finally {
$this->removeTempDir($dir);
}
}

private function createTempDir()
{
$dir = sys_get_temp_dir() . '/jmespath-compiler-' . bin2hex(random_bytes(12));
Expand Down
11 changes: 11 additions & 0 deletions tests/FnDispatcherTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ public function testReversesUtf8Strings(): void
$this->assertSame('☃ba', $fn('reverse', ['ab☃']));
}

public function testContainsUsesJsonSemantics(): void
{
$fn = new FnDispatcher();

$this->assertFalse($fn('contains', [[1, 2, 3], '2']));
$this->assertFalse($fn('contains', [['1', 8], 1]));
$this->assertTrue($fn('contains', [[1, 2], 2.0]));
$this->assertTrue($fn('contains', [[['a' => 1]], (object) ['a' => 1]]));
$this->assertFalse($fn('contains', ['foobar', 123]));
}

/**
* @dataProvider toNumberProvider
*/
Expand Down
17 changes: 17 additions & 0 deletions tests/UtilsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,23 @@ public function testHasStableSort(): void
$this->assertEquals(0, $result[4]);
}

public function testComparesValuesUsingJsonSemantics(): void
{
$this->assertTrue(Utils::isEqual(1, 1.0));
$this->assertTrue(Utils::isEqual(['a' => 1], (object) ['a' => 1.0]));
$this->assertTrue(Utils::isEqual(
(object) ['a' => (object) ['b' => 1]],
(object) ['a' => (object) ['b' => 1]]
));
$this->assertTrue(Utils::isEqual(['a' => 1, 'b' => 2], ['b' => 2, 'a' => 1]));
$this->assertTrue(Utils::isEqual([], new \stdClass()));
$this->assertFalse(Utils::isEqual(['a' => 1], ['a' => 2]));
$this->assertFalse(Utils::isEqual([1], [1, 1]));
$this->assertFalse(Utils::isEqual('1', 1));
$this->assertFalse(Utils::isEqual(1, true));
$this->assertFalse(Utils::isEqual(0, null));
}

public function testSlicesArrays(): void
{
$this->assertEquals([3, 2, 1], Utils::slice([1, 2, 3], null, null, -1));
Expand Down
Loading