From 88f89096661d21ff1db7118c7263436041a83917 Mon Sep 17 00:00:00 2001 From: simonkundrik <117163790+simonkundrik@users.noreply.github.com> Date: Wed, 4 Nov 2026 13:46:00 +0000 Subject: [PATCH] fix: stop [(...)] from throwing when its result is not a property A script expression that evaluated to a number or other non-string value which isn't a property of the current value threw "loc.indexOf is not a function". The README's idiom for the last item, $..book[(@.length-1)], hit this on any empty array (the result is -1), so a single empty array anywhere in the tree made the whole query throw. Results such as NaN, null and true did the same. An undefined result already matched nothing. _trace only follows a non-string location when it is an own property of the value, and every later branch calls string methods on it. A location that isn't such a property now matches nothing. Fixes #181 --- src/jsonpath.js | 3 +++ test/test.examples.js | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/jsonpath.js b/src/jsonpath.js index 4d036ee7..6fda16e3 100644 --- a/src/jsonpath.js +++ b/src/jsonpath.js @@ -650,6 +650,9 @@ class JSONPathClass { val, /** @type {string|number} */ (loc), callback, hasArrExpr )); + } else if (typeof loc !== 'string') { + // A number or other value (e.g., from `[(@.length-1)]`) which is + // not a property of this value, so there is nothing to follow // eslint-disable-next-line unicorn/prefer-switch -- Part of larger `if` } else if (loc === '*') { // all child properties this._walk(val, (m) => { diff --git a/test/test.examples.js b/test/test.examples.js index 22dc6cf7..ab9f004b 100644 --- a/test/test.examples.js +++ b/test/test.examples.js @@ -85,6 +85,43 @@ checkBuiltInVMAndNodeVM(function (vmType, setBuiltInState) { assert.deepEqual(result, expected); }); + it('last property of entire tree (index which does not exist)', () => { + // `-1` is not an index of an empty array, so nothing is found + assert.deepEqual(jsonpath({ + json: {items: []}, path: '$.items[(@.length-1)]' + }), []); + // Other arrays in the tree are still searched + assert.deepEqual(jsonpath({ + json: {items: [], other: {items: [5, 6]}}, + path: '$..items[(@.length-1)]' + }), [6]); + assert.deepEqual(jsonpath({ + json: {a: [], b: [1, 2]}, path: '$.*[(@.length-1)]' + }), [2]); + assert.deepEqual(jsonpath({ + json: {items: [1, 2]}, path: '$.items[(@.length+5)]' + }), []); + // Results which cannot be property names (here `NaN`, `null` + // and `true`) find nothing, including after a slice (#181) + const obj = {o: {a: 1}}; + assert.deepEqual(jsonpath({ + json: obj, path: '$.o[(@.length-1)]' + }), []); + assert.deepEqual(jsonpath({json: obj, path: '$.o[(null)]'}), []); + assert.deepEqual(jsonpath({json: obj, path: '$.o[(true)]'}), []); + assert.deepEqual(jsonpath({ + json: {stores: [{name: 'a'}, {name: 'b'}, {name: 'c'}]}, + path: '$.stores[0:3:2][(@.length-1)].name' + }), []); + // Properties with those names are still found + assert.deepEqual(jsonpath({ + json: {o: {null: 'N', true: 'T'}}, path: '$.o[(null)]' + }), ['N']); + assert.deepEqual(jsonpath({ + json: {o: {null: 'N', true: 'T'}}, path: '$.o[(true)]' + }), ['T']); + }); + it('range of property of entire tree', () => { const books = json.store.book; const expected = [books[0], books[1]];