This commit is contained in:
Javier Casares 2026-08-18 07:00:18 +00:00
commit 77d0cea926
995 changed files with 28142 additions and 8603 deletions

View file

@ -0,0 +1,102 @@
# CHANGELOG
## 2.9.2 - 2026-07-06
* Pass explicit trim characters ahead of the PHP 8.6 trim default change.
## 2.9.1 - 2026-06-11
* Fixed the compiled runtime to emit function names as string literals, preventing arbitrary code execution.
* Fixed the parser to reject non-identifier function callees, such as literal and raw string callees.
## 2.9.0 - 2026-06-10
* Added PHP 8.5 support.
* 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 multi-select hashes to end projections, so following tokens apply to the projected list.
* Fixed sort() and sort_by() to compare numbers numerically.
* Changed sort(), sort_by(), max(), min(), max_by() and min_by() to order strings by code point.
* Fixed max_by() and min_by() to error on mixed-type keys instead of returning arbitrary elements.
* Fixed max() returning null or erroring when the first array element is falsy, e.g. max([0, 1]).
* Fixed sum() and join() to return 0 and an empty string respectively for empty arrays.
* Fixed 0.0 to be truthy in filters and logical operators, like every other number.
* 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.
* Fixed the caret position in syntax error messages for errors at the end of an expression.
* Fixed map() to error on non-array second arguments instead of returning [].
* Fixed Env::cleanCompileDir() when JP_PHP_COMPILE=on.
## 2.8.0 - 2024-09-04
* Add support for PHP 8.4.
## 2.7.0 - 2023-08-15
* Fixed flattening in arrays starting with null.
* Drop support for HHVM and PHP earlier than 7.2.5.
* Add support for PHP 8.1, 8.2, and 8.3.
## 2.6.0 - 2020-07-31
* Support for PHP 8.0.
## 2.5.0 - 2019-12-30
* Full support for PHP 7.0-7.4.
* Fixed autoloading when run from within vendor folder.
* Full multibyte (UTF-8) string support.
## 2.4.0 - 2016-12-03
* Added support for floats when interpreting data.
* Added a function_exists check to work around redeclaration issues.
## 2.3.0 - 2016-01-05
* Added support for [JEP-9](https://github.com/jmespath/jmespath.site/blob/master/docs/proposals/improved-filters.rst),
including unary filter expressions, and `&&` filter expressions.
* Fixed various parsing issues, including not removing escaped single quotes
from raw string literals.
* Added support for the `map` function.
* Fixed several issues with code generation.
## 2.2.0 - 2015-05-27
* Added support for [JEP-12](https://github.com/jmespath/jmespath.site/blob/master/docs/proposals/raw-string-literals.rst)
and raw string literals (e.g., `'foo'`).
## 2.1.0 - 2014-01-13
* Added `JmesPath\Env::cleanCompileDir()` to delete any previously compiled
JMESPath expressions.
## 2.0.0 - 2014-01-11
* Moving to a flattened namespace structure.
* Runtimes are now only PHP callables.
* Fixed an error in the way empty JSON literals are parsed so that they now
return an empty string to match the Python and JavaScript implementations.
* Removed functions from runtimes. Instead there is now a function dispatcher
class, FnDispatcher, that provides function implementations behind a single
dispatch function.
* Removed ExprNode in lieu of just using a PHP callable with bound variables.
* Removed debug methods from runtimes and instead into a new Debugger class.
* Heavily cleaned up function argument validation.
* Slice syntax is now properly validated (i.e., colons are followed by the
appropriate value).
* Lots of code cleanup and performance improvements.
* Added a convenient `JmesPath\search()` function.
* **IMPORTANT**: Relocating the project to https://github.com/jmespath/jmespath.php
## 1.1.1 - 2014-10-08
* Added support for using ArrayAccess and Countable as arrays and objects.
## 1.1.0 - 2014-08-06
* Added the ability to search data returned from json_decode() where JSON
objects are returned as stdClass objects.

125
vendor/mtdowling/jmespath.php/README.md vendored Normal file
View file

@ -0,0 +1,125 @@
# jmespath.php
JMESPath (pronounced "jaymz path") allows you to declaratively specify how to
extract elements from a JSON document. *jmespath.php* allows you to use JMESPath
in PHP applications with PHP data structures. It requires PHP 7.2.5 or greater
and can be installed through [Composer](https://getcomposer.org/doc/00-intro.md)
using the `mtdowling/jmespath.php` package.
```php
require 'vendor/autoload.php';
$expression = 'foo.*.baz';
$data = [
'foo' => [
'bar' => ['baz' => 1],
'bam' => ['baz' => 2],
'boo' => ['baz' => 3]
]
];
JmesPath\search($expression, $data);
// Returns: [1, 2, 3]
```
- [JMESPath Tutorial](https://jmespath.org/tutorial.html)
- [JMESPath Grammar](https://jmespath.org/specification.html#grammar)
- [JMESPath Python library](https://github.com/jmespath/jmespath.py)
## PHP Usage
The `JmesPath\search` function can be used in most cases when using the library.
This function utilizes a JMESPath runtime based on your environment. The runtime
utilized can be configured using environment variables.
```php
$result = JmesPath\search($expression, $data);
// or, if you require PSR-4 compliance.
$result = JmesPath\Env::search($expression, $data);
```
### Runtimes
jmespath.php utilizes *runtimes*. There are currently two runtimes: AstRuntime
and CompilerRuntime.
AstRuntime is utilized by `JmesPath\search()` and `JmesPath\Env::search()` by
default.
#### AstRuntime
The AstRuntime will parse an expression, cache the resulting AST in memory, and
interpret the AST using an external tree visitor. AstRuntime provides a good
general approach for interpreting JMESPath expressions that have a low to
moderate level of reuse.
```php
$runtime = new JmesPath\AstRuntime();
$runtime('foo.bar', ['foo' => ['bar' => 'baz']]);
// > 'baz'
```
#### CompilerRuntime
`JmesPath\CompilerRuntime` provides the most performance for applications that
have a moderate to high level of reuse of JMESPath expressions. The
CompilerRuntime will walk a JMESPath AST and emit PHP source code, resulting in
anywhere from 7x to 60x speed improvements.
Compiling JMESPath expressions to source code is a slower process than just
walking and interpreting a JMESPath AST (via the AstRuntime). However, running
the compiled JMESPath code results in much better performance than walking an
AST. This essentially means that there is a warm-up period when using the
`CompilerRuntime`, but after the warm-up period, it will provide much better
performance.
Use the CompilerRuntime if you know that you will be executing JMESPath
expressions more than once or if you can pre-compile JMESPath expressions before
executing them (for example, server-side applications).
```php
// Note: The cache directory argument is optional.
$runtime = new JmesPath\CompilerRuntime('/path/to/compile/folder');
$runtime('foo.bar', ['foo' => ['bar' => 'baz']]);
// > 'baz'
```
##### Environment Variables
You can utilize the CompilerRuntime in `JmesPath\search()` by setting the
`JP_PHP_COMPILE` environment variable to "on" or to a directory on disk used to
store cached expressions.
## Testing
A comprehensive list of test cases can be found at
https://github.com/jmespath/jmespath.php/tree/master/tests/compliance. These
compliance tests are utilized by jmespath.php to ensure consistency with other
implementations, and can serve as examples of the language.
jmespath.php is tested using PHPUnit. In order to run the tests, you need to
first install the dependencies using Composer, then you just need to run the
tests via make:
```bash
make test
```
You can run a suite of performance tests as well:
```bash
make perf
```
## Security
If you discover a security vulnerability within this package, follow the
reporting process from our
[Security Policy](https://github.com/jmespath/jmespath.php/security/policy).
## License
jmespath.php is made available under the MIT License (MIT). Please see
[License File](LICENSE) for more information.

View file

@ -1,123 +0,0 @@
============
jmespath.php
============
JMESPath (pronounced "jaymz path") allows you to declaratively specify how to
extract elements from a JSON document. *jmespath.php* allows you to use
JMESPath in PHP applications with PHP data structures. It requires PHP 7.2.5 or
greater and can be installed through `Composer <http://getcomposer.org/doc/00-intro.md>`_
using the ``mtdowling/jmespath.php`` package.
.. code-block:: php
require 'vendor/autoload.php';
$expression = 'foo.*.baz';
$data = [
'foo' => [
'bar' => ['baz' => 1],
'bam' => ['baz' => 2],
'boo' => ['baz' => 3]
]
];
JmesPath\search($expression, $data);
// Returns: [1, 2, 3]
- `JMESPath Tutorial <http://jmespath.org/tutorial.html>`_
- `JMESPath Grammar <http://jmespath.org/specification.html#grammar>`_
- `JMESPath Python library <https://github.com/jmespath/jmespath.py>`_
PHP Usage
=========
The ``JmesPath\search`` function can be used in most cases when using the
library. This function utilizes a JMESPath runtime based on your environment.
The runtime utilized can be configured using environment variables and may at
some point in the future automatically utilize a C extension if available.
.. code-block:: php
$result = JmesPath\search($expression, $data);
// or, if you require PSR-4 compliance.
$result = JmesPath\Env::search($expression, $data);
Runtimes
--------
jmespath.php utilizes *runtimes*. There are currently two runtimes:
AstRuntime and CompilerRuntime.
AstRuntime is utilized by ``JmesPath\search()`` and ``JmesPath\Env::search()``
by default.
AstRuntime
~~~~~~~~~~
The AstRuntime will parse an expression, cache the resulting AST in memory,
and interpret the AST using an external tree visitor. AstRuntime provides a
good general approach for interpreting JMESPath expressions that have a low to
moderate level of reuse.
.. code-block:: php
$runtime = new JmesPath\AstRuntime();
$runtime('foo.bar', ['foo' => ['bar' => 'baz']]);
// > 'baz'
CompilerRuntime
~~~~~~~~~~~~~~~
``JmesPath\CompilerRuntime`` provides the most performance for
applications that have a moderate to high level of reuse of JMESPath
expressions. The CompilerRuntime will walk a JMESPath AST and emit PHP source
code, resulting in anywhere from 7x to 60x speed improvements.
Compiling JMESPath expressions to source code is a slower process than just
walking and interpreting a JMESPath AST (via the AstRuntime). However,
running the compiled JMESPath code results in much better performance than
walking an AST. This essentially means that there is a warm-up period when
using the ``CompilerRuntime``, but after the warm-up period, it will provide
much better performance.
Use the CompilerRuntime if you know that you will be executing JMESPath
expressions more than once or if you can pre-compile JMESPath expressions
before executing them (for example, server-side applications).
.. code-block:: php
// Note: The cache directory argument is optional.
$runtime = new JmesPath\CompilerRuntime('/path/to/compile/folder');
$runtime('foo.bar', ['foo' => ['bar' => 'baz']]);
// > 'baz'
Environment Variables
^^^^^^^^^^^^^^^^^^^^^
You can utilize the CompilerRuntime in ``JmesPath\search()`` by setting
the ``JP_PHP_COMPILE`` environment variable to "on" or to a directory
on disk used to store cached expressions.
Testing
=======
A comprehensive list of test cases can be found at
https://github.com/jmespath/jmespath.php/tree/master/tests/compliance.
These compliance tests are utilized by jmespath.php to ensure consistency with
other implementations, and can serve as examples of the language.
jmespath.php is tested using PHPUnit. In order to run the tests, you need to
first install the dependencies using Composer as described in the *Installation*
section. Next you just need to run the tests via make:
.. code-block:: bash
make test
You can run a suite of performance tests as well:
.. code-block:: bash
make perf

View file

@ -0,0 +1,14 @@
# SECURITY POLICY
## Supported Versions
After each new major release, the previous release will be supported for no
less than 24 months, unless explicitly stated otherwise. This may mean that
there are multiple supported versions at any given time.
## Reporting a Vulnerability
If you discover a security vulnerability within this package, please send an
email to security@gjcampbell.co.uk. All security vulnerabilities will be
promptly addressed. Please do not disclose security-related issues publicly
until a fix has been announced.

View file

@ -9,7 +9,6 @@ class AstRuntime
private $parser;
private $interpreter;
private $cache = [];
private $cachedCount = 0;
public function __construct(
?Parser $parser = null,
@ -34,10 +33,9 @@ class AstRuntime
public function __invoke($expression, $data)
{
if (!isset($this->cache[$expression])) {
// Clear the AST cache when it hits 1024 entries
if (++$this->cachedCount > 1024) {
// Clear the AST cache when it already holds 1024 entries.
if (count($this->cache) >= 1024) {
$this->cache = [];
$this->cachedCount = 0;
}
$this->cache[$expression] = $this->parser->parse($expression);
}

View file

@ -8,11 +8,14 @@ namespace JmesPath;
* logic to determine the filename:
*
* 1. Start with the string "jmespath_"
* 2. Append the MD5 checksum of the expression.
* 2. Append the MD5 checksum of the expression salted with the PHP version
* and compiled-cache epoch.
* 3. Append ".php"
*/
class CompilerRuntime
{
const CACHE_VERSION = 5;
private $parser;
private $compiler;
private $cacheDir;
@ -51,7 +54,7 @@ class CompilerRuntime
*/
public function __invoke($expression, $data)
{
$functionName = 'jmespath_' . md5($expression);
$functionName = self::functionName($expression);
if (!function_exists($functionName)) {
$filename = "{$this->cacheDir}/{$functionName}.php";
@ -64,6 +67,23 @@ class CompilerRuntime
return $functionName($this->interpreter, $data);
}
/**
* @internal Shared with DebugRuntime so cache naming cannot drift.
*/
public static function functionName($expression)
{
return 'jmespath_' . md5(
'jmespath:' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION
. ':' . self::CACHE_VERSION . ':' . $expression
);
}
/** @internal */
public function getCacheDir()
{
return $this->cacheDir;
}
private function compile($filename, $expression, $functionName)
{
$code = $this->compiler->visit(
@ -72,12 +92,26 @@ class CompilerRuntime
$expression
);
if (!file_put_contents($filename, $code)) {
$tempFile = $filename . '.' . bin2hex(random_bytes(12)) . '.tmp';
if (!file_put_contents($tempFile, $code)) {
throw new \RuntimeException(sprintf(
'Unable to write the compiled PHP code to: %s (%s)',
$filename,
$tempFile,
var_export(error_get_last(), true)
));
}
if (!rename($tempFile, $filename)) {
@unlink($tempFile);
if (!file_exists($filename)) {
throw new \RuntimeException(
"Unable to move the compiled PHP code to: {$filename}"
);
}
// Another process won the race; its file is equivalent.
} elseif (function_exists('opcache_invalidate')) {
opcache_invalidate($filename, true);
}
}
}

View file

@ -83,12 +83,12 @@ class DebugRuntime
private function dumpCompiledCode($expression)
{
fwrite($this->out, "Code\n========\n\n");
$dir = sys_get_temp_dir();
$hash = md5($expression);
$functionName = "jmespath_{$hash}";
$filename = "{$dir}/{$functionName}.php";
$functionName = CompilerRuntime::functionName($expression);
$filename = $this->runtime->getCacheDir() . '/' . $functionName . '.php';
fwrite($this->out, "File: {$filename}\n\n");
fprintf($this->out, file_get_contents($filename));
fwrite($this->out, is_file($filename)
? file_get_contents($filename)
: "(not present: {$functionName} was already loaded in this process, likely from another cache directory)\n");
}
private function debugCallback(callable $debugFn, $expression, $data)

View file

@ -57,7 +57,10 @@ final class Env
public static function cleanCompileDir()
{
$total = 0;
$compileDir = self::getEnvVariable(self::COMPILE_DIR) ?: sys_get_temp_dir();
$compileDir = self::getEnvVariable(self::COMPILE_DIR);
if ($compileDir === 'on' || !$compileDir) {
$compileDir = sys_get_temp_dir();
}
foreach (glob("{$compileDir}/jmespath_*.php") as $file) {
$total++;

View file

@ -60,12 +60,18 @@ class FnDispatcher
{
$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)
@ -100,7 +106,7 @@ class FnDispatcher
$fn = function ($a, $b, $i) use ($args) {
return $i ? ($a . $args[0] . $b) : $b;
};
return $this->reduce('join:0', $args[1], ['string'], $fn);
return $args[1] ? $this->reduce('join:0', $args[1], ['string'], $fn) : '';
}
private function fn_keys(array $args)
@ -118,8 +124,8 @@ class FnDispatcher
private function fn_max(array $args)
{
$this->validate('max', $args, [['array']]);
$fn = function ($a, $b) {
return $a >= $b ? $a : $b;
$fn = function ($a, $b, $i) {
return $i && self::compareValues($a, $b) >= 0 ? $a : $b;
};
return $this->reduce('max:0', $args[0], ['number', 'string'], $fn);
}
@ -128,10 +134,21 @@ class FnDispatcher
{
$this->validate('max_by', $args, [['array'], ['expression']]);
$expr = $this->wrapExpression('max_by:1', $args[1], ['number', 'string']);
$fn = function ($carry, $item, $index) use ($expr) {
return $index
? ($expr($carry) >= $expr($item) ? $carry : $item)
: $item;
$carryKey = null;
$fn = function ($carry, $item, $index) use ($expr, &$carryKey) {
if (!$index) {
return $item;
}
if ($index === 1) {
$carryKey = $expr($carry);
}
$itemKey = $expr($item);
$this->validateSeq('max_by:0', ['number', 'string'], $carryKey, $itemKey);
if (self::compareValues($carryKey, $itemKey) >= 0) {
return $carry;
}
$carryKey = $itemKey;
return $item;
};
return $this->reduce('max_by:1', $args[0], ['any'], $fn);
}
@ -140,7 +157,7 @@ class FnDispatcher
{
$this->validate('min', $args, [['array']]);
$fn = function ($a, $b, $i) {
return $i && $a <= $b ? $a : $b;
return $i && self::compareValues($a, $b) <= 0 ? $a : $b;
};
return $this->reduce('min:0', $args[0], ['number', 'string'], $fn);
}
@ -149,9 +166,21 @@ class FnDispatcher
{
$this->validate('min_by', $args, [['array'], ['expression']]);
$expr = $this->wrapExpression('min_by:1', $args[1], ['number', 'string']);
$i = -1;
$fn = function ($a, $b) use ($expr, &$i) {
return ++$i ? ($expr($a) <= $expr($b) ? $a : $b) : $b;
$carryKey = null;
$fn = function ($carry, $item, $index) use ($expr, &$carryKey) {
if (!$index) {
return $item;
}
if ($index === 1) {
$carryKey = $expr($carry);
}
$itemKey = $expr($item);
$this->validateSeq('min_by:0', ['number', 'string'], $carryKey, $itemKey);
if (self::compareValues($carryKey, $itemKey) <= 0) {
return $carry;
}
$carryKey = $itemKey;
return $item;
};
return $this->reduce('min_by:1', $args[0], ['any'], $fn);
}
@ -162,7 +191,7 @@ class FnDispatcher
if (is_array($args[0])) {
return array_reverse($args[0]);
} elseif (is_string($args[0])) {
return strrev($args[0]);
return implode('', array_reverse(mb_str_split($args[0], 1, 'UTF-8')));
} else {
throw new \RuntimeException('Cannot reverse provided argument');
}
@ -174,7 +203,7 @@ class FnDispatcher
$fn = function ($a, $b) {
return Utils::add($a, $b);
};
return $this->reduce('sum:0', $args[0], ['number'], $fn);
return $args[0] ? $this->reduce('sum:0', $args[0], ['number'], $fn) : 0;
}
private function fn_sort(array $args)
@ -183,7 +212,7 @@ class FnDispatcher
$valid = ['string', 'number'];
return Utils::stableSort($args[0], function ($a, $b) use ($valid) {
$this->validateSeq('sort:0', $valid, $a, $b);
return strnatcmp($a, $b);
return self::compareValues($a, $b);
});
}
@ -198,7 +227,7 @@ class FnDispatcher
$va = $expr($a);
$vb = $expr($b);
$this->validateSeq('sort_by:0', $valid, $va, $vb);
return strnatcmp($va, $vb);
return self::compareValues($va, $vb);
}
);
}
@ -236,14 +265,53 @@ class FnDispatcher
{
$this->validateArity('to_number', count($args), 1);
$value = $args[0];
$type = Utils::type($value);
if ($type == 'number') {
if (Utils::type($value) == 'number') {
return $value;
} elseif ($type == 'string' && is_numeric($value)) {
return mb_strpos($value, '.', 0, 'UTF-8') ? (float) $value : (int) $value;
} else {
}
if (!is_string($value)) {
return null;
}
return $this->parseJsonNumber($value);
}
/**
* Parses a string conforming to the JSON number grammar (RFC 8259) into
* an int when exactly representable, otherwise a float. Returns null for
* non-conforming or non-finite input.
*/
private function parseJsonNumber($value)
{
if (!preg_match('/^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/D', $value)) {
return null;
}
if (preg_match('/^-?(?:0|[1-9][0-9]*)$/D', $value)) {
return $this->parseJsonInteger($value);
}
$number = (float) $value;
return is_finite($number) ? $number : null;
}
private function parseJsonInteger($value)
{
$negative = $value[0] === '-';
$digits = $negative ? substr($value, 1) : $value;
$limit = $negative ? substr((string) PHP_INT_MIN, 1) : (string) PHP_INT_MAX;
if (strlen($digits) < strlen($limit)
|| (strlen($digits) === strlen($limit) && strcmp($digits, $limit) <= 0)
) {
return (int) $value;
}
$number = (float) $value;
return is_finite($number) ? $number : null;
}
private function fn_values(array $args)
@ -272,7 +340,7 @@ class FnDispatcher
private function fn_map(array $args)
{
$this->validate('map', $args, [['expression'], ['any']]);
$this->validate('map', $args, [['expression'], ['array']]);
$result = [];
foreach ($args[1] as $a) {
$result[] = $args[0]($a);
@ -354,6 +422,21 @@ class FnDispatcher
}
}
/**
* Compares two values of the same JMESPath type.
*
* @param mixed $a Value A
* @param mixed $b Value B
*
* @return int Negative if $a < $b, zero if equal, positive if $a > $b.
*/
private static function compareValues($a, $b)
{
return Utils::type($a) === 'string'
? strcmp((string) $a, (string) $b)
: ($a <=> $b);
}
/**
* Reduces and validates an array of values to a single value using a fn.
*

View file

@ -298,9 +298,10 @@ class Lexer
$buffer .= $current;
$current = next($chars);
} while ($current !== false && isset($this->numbers[$current]));
$value = $this->parseIndexNumber($buffer);
$tokens[] = [
'type' => self::T_NUMBER,
'value' => (int)$buffer,
'type' => $value === null ? self::T_UNKNOWN : self::T_NUMBER,
'value' => $value === null ? $buffer : $value,
'pos' => $start
];
@ -417,6 +418,30 @@ class Lexer
return ['type' => $type, 'value' => $buffer, 'pos' => $position];
}
/**
* Parses a bare index/slice integer token ("-"? digit+). Returns null
* when the buffer is a lone "-" or the value cannot be represented as a
* PHP integer.
*/
private function parseIndexNumber($buffer)
{
if ($buffer === '-') {
return null;
}
$negative = $buffer[0] === '-';
$digits = ltrim($negative ? substr($buffer, 1) : $buffer, '0') ?: '0';
$limit = $negative ? substr((string) PHP_INT_MIN, 1) : (string) PHP_INT_MAX;
if (strlen($digits) > strlen($limit)
|| (strlen($digits) === strlen($limit) && strcmp($digits, $limit) > 0)
) {
return null;
}
return (int) $buffer;
}
/**
* Parses a JSON token or sets the token type to "unknown" on error.
*

View file

@ -5,7 +5,7 @@ use JmesPath\Lexer as T;
/**
* JMESPath Pratt parser
* @link http://hall.org.ua/halls/wizzard/pdf/Vaughan.Pratt.TDOP.pdf
* @link https://dl.acm.org/doi/10.1145/512927.512931
*/
class Parser
{
@ -22,6 +22,8 @@ class Parser
T::T_EOF => 0,
T::T_QUOTED_IDENTIFIER => 0,
T::T_IDENTIFIER => 0,
T::T_UNKNOWN => 0,
T::T_LITERAL => 0,
T::T_RBRACKET => 0,
T::T_RPAREN => 0,
T::T_COMMA => 0,
@ -272,6 +274,10 @@ class Parser
private function led_lparen(array $left)
{
if (!isset($left['type'], $left['value']) || $left['type'] !== 'field') {
throw $this->syntax('Invalid function name');
}
$args = [];
$this->next();
@ -347,6 +353,10 @@ class Parser
if ($this->token['type'] == T::T_LBRACKET) {
$this->next();
return $this->parseMultiSelectList();
} elseif ($this->token['type'] == T::T_LBRACE) {
// Like the multi-select list above, a multi-select hash ends any
// projection: tokens that follow apply to the projected list.
return $this->nud_lbrace();
}
return $this->expr($bp);

View file

@ -16,6 +16,7 @@ class SyntaxErrorException extends \InvalidArgumentException
array $token,
$expression
) {
$token += ['pos' => mb_strlen($expression, 'UTF-8'), 'value' => null];
$message = sprintf("Syntax error at character %d\n", max($token['pos'], 0))
. $expression . "\n" . str_repeat(' ', max($token['pos'], 0)) . "^\n";
$message .= !is_array($expectedTypesOrMessage)

View file

@ -107,7 +107,7 @@ class TreeCompiler
return $this
->write('%s = $value;', $a)
->dispatch($node['children'][0])
->write('if (!$value && $value !== "0" && $value !== 0) {')
->write('if (!Utils::isTruthy($value)) {')
->indent()
->write('$value = %s;', $a)
->dispatch($node['children'][1])
@ -121,7 +121,7 @@ class TreeCompiler
return $this
->write('%s = $value;', $a)
->dispatch($node['children'][0])
->write('if ($value || $value === "0" || $value === 0) {')
->write('if (Utils::isTruthy($value)) {')
->indent()
->write('$value = %s;', $a)
->dispatch($node['children'][1])
@ -257,8 +257,8 @@ class TreeCompiler
}
return $this->write(
'$value = Fd::getInstance()->__invoke("%s", %s);',
$node['value'], $args
'$value = Fd::getInstance()->__invoke(%s, %s);',
var_export($node['value'], true), $args
);
}
@ -332,7 +332,7 @@ class TreeCompiler
->write('');
if (!isset($node['from'])) {
$this->write('if (!is_array($value) || !($value instanceof \stdClass)) { $value = null; }');
$this->write('if (!is_array($value) && !($value instanceof \stdClass)) { $value = null; }');
} elseif ($node['from'] == 'object') {
$this->write('if (!Utils::isObject($value)) { $value = null; }');
} elseif ($node['from'] == 'array') {

View file

@ -69,7 +69,8 @@ class TreeInterpreter
case 'projection':
$left = $this->dispatch($node['children'][0], $value);
switch ($node['from']) {
$from = isset($node['from']) ? $node['from'] : null;
switch ($from) {
case 'object':
if (!Utils::isObject($left)) {
return null;
@ -81,7 +82,7 @@ class TreeInterpreter
}
break;
default:
if (!is_array($left) || !($left instanceof \stdClass)) {
if (!is_array($left) && !($left instanceof \stdClass)) {
return null;
}
}

View file

@ -22,7 +22,7 @@ class Utils
public static function isTruthy($value)
{
if (!$value) {
return $value === 0 || $value === '0';
return $value === 0 || $value === 0.0 || $value === '0';
} elseif ($value instanceof \stdClass) {
return (bool) get_object_vars($value);
} else {
@ -58,12 +58,18 @@ class Utils
return count($arg) == 0 || $arg->offsetExists(0)
? 'array'
: 'object';
} elseif (method_exists($arg, '__toString')) {
return 'string';
} elseif (is_object($arg)) {
if (method_exists($arg, '__toString')) {
return 'string';
}
throw new \InvalidArgumentException(
'Unable to determine JMESPath type from ' . get_class($arg)
);
}
throw new \InvalidArgumentException(
'Unable to determine JMESPath type from ' . get_class($arg)
'Unable to determine JMESPath type from ' . gettype($arg)
);
}
@ -106,7 +112,10 @@ class Utils
}
/**
* 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
@ -115,15 +124,38 @@ class Utils
*/
public static function isEqual($a, $b)
{
if ($a === $b) {
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;
$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;
}
return $a === $b;
}
/**
@ -160,7 +192,7 @@ class Utils
* @param callable $sortFn Callable used to sort values
*
* @return array Returns the sorted array
* @link http://en.wikipedia.org/wiki/Schwartzian_transform
* @link https://en.wikipedia.org/wiki/Schwartzian_transform
*/
public static function stableSort(array $data, callable $sortFn)
{
@ -192,7 +224,7 @@ class Utils
*/
public static function slice($value, $start = null, $stop = null, $step = 1)
{
if (!is_array($value) && !is_string($value)) {
if (!is_string($value) && !self::isArray($value)) {
throw new \InvalidArgumentException('Expects string or array');
}
@ -239,7 +271,10 @@ class Utils
private static function sliceIndices($subject, $start, $stop, $step)
{
$type = gettype($subject);
$len = $type == 'string' ? mb_strlen($subject, 'UTF-8') : count($subject);
if ($type == 'string') {
$subject = mb_str_split($subject, 1, 'UTF-8');
}
$len = count($subject);
list($start, $stop, $step) = self::adjustSlice($len, $start, $stop, $step);
$result = [];