From deaab007e1deb706249d3fcb9d27c961861e19a6 Mon Sep 17 00:00:00 2001 From: dualfroz Date: Thu, 3 Sep 2026 14:47:14 +0200 Subject: [PATCH] fix: keep the original error attribution for and/or operands To allow short-circuiting, the RHS of `and`/`or` is evaluated lazily inside evaluateBooleanExpression, so it runs inside the try block of evaluateBinary. Every error raised while evaluating that operand was then stamped with the operator's own token and position, hiding what actually failed: 1=1 and $contains([''], '') // Argument 1 of function "and" does not match function signature The same expression inside a user-defined function reports "contains" correctly, because there the operand is evaluated at the call site. The other binary operators are unaffected: they evaluate the RHS before the try block, so `1 + $contains([''], '')` already reports "contains". Only claim the error for the operator when nothing else has attributed it yet, which is the same guard already used when applying a procedure. fn.boolean cannot throw, so an error reaching this catch always comes from the operand. Errors that carry no token (a duplicate group key, for example) are still reported against the operator exactly as before. Fixes #765 --- src/jsonata.js | 10 ++- test/implementation-tests.js | 72 ++++++++++++++++++- .../groups/boolean-expresssions/case031.json | 10 +++ .../groups/boolean-expresssions/case032.json | 10 +++ .../groups/boolean-expresssions/case033.json | 13 ++++ 5 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 test/test-suite/groups/boolean-expresssions/case031.json create mode 100644 test/test-suite/groups/boolean-expresssions/case032.json create mode 100644 test/test-suite/groups/boolean-expresssions/case033.json diff --git a/src/jsonata.js b/src/jsonata.js index 184de40..693df56 100644 --- a/src/jsonata.js +++ b/src/jsonata.js @@ -459,8 +459,14 @@ var jsonata = (function() { try { return await evaluateBooleanExpression(lhs, evalrhs, op); } catch(err) { - err.position = expr.position; - err.token = op; + // The RHS is evaluated inside evaluateBooleanExpression so that it can be + // short-circuited, which means an error raised by the RHS expression itself + // surfaces here. If that error already names what raised it, keep that + // attribution instead of blaming this operator for it. + if (typeof err.token === 'undefined') { + err.position = expr.position; + err.token = op; + } throw err; } } diff --git a/test/implementation-tests.js b/test/implementation-tests.js index e213ed7..4f9f586 100644 --- a/test/implementation-tests.js +++ b/test/implementation-tests.js @@ -1204,4 +1204,74 @@ describe("Concurrency tests", () => { {"id": "c", "delayed": "done:c"} ]) }) -}) \ No newline at end of file +}) + +describe("Error reporting for the `and`/`or` operators", () => { + describe("error raised while evaluating the right hand side of `and`", function() { + it("should be reported against the failing function, not the operator", async function() { + var expr = jsonata("(1=1 and $contains([''], ''))"); + var error; + try { + await expr.evaluate({}); + } catch (err) { + error = err; + } + expect(error).to.not.equal(undefined); + expect(error.code).to.equal("T0410"); + expect(error.token).to.equal("contains"); + }); + }); + + describe("error raised while evaluating the right hand side of `or`", function() { + it("should be reported against the failing function, not the operator", async function() { + var expr = jsonata("(1=0 or $contains([''], ''))"); + var error; + try { + await expr.evaluate({}); + } catch (err) { + error = err; + } + expect(error).to.not.equal(undefined); + expect(error.code).to.equal("T0410"); + expect(error.token).to.equal("contains"); + }); + }); + + describe("the same expression inline and inside a user-defined function", function() { + it("should report the same error", async function() { + var inline = jsonata("(1=1 and $contains([''], ''))"); + var wrapped = jsonata("( $fn := function($a, $b) { $a and $b }; $fn(1=1, $contains([''], '')) )"); + var inlineError; + var wrappedError; + try { + await inline.evaluate({}); + } catch (err) { + inlineError = err; + } + try { + await wrapped.evaluate({}); + } catch (err) { + wrappedError = err; + } + expect(inlineError).to.not.equal(undefined); + expect(wrappedError).to.not.equal(undefined); + expect(inlineError.code).to.equal(wrappedError.code); + expect(inlineError.token).to.equal(wrappedError.token); + }); + }); + + describe("error that nothing else has attributed", function() { + it("should still be reported against the operator", async function() { + var expr = jsonata('(true and {"k": $.a, "k": $.b})'); + var error; + try { + await expr.evaluate({ a: 1, b: 2 }); + } catch (err) { + error = err; + } + expect(error).to.not.equal(undefined); + expect(error.code).to.equal("D1009"); + expect(error.token).to.equal("and"); + }); + }); +}); diff --git a/test/test-suite/groups/boolean-expresssions/case031.json b/test/test-suite/groups/boolean-expresssions/case031.json new file mode 100644 index 0000000..e14a4cd --- /dev/null +++ b/test/test-suite/groups/boolean-expresssions/case031.json @@ -0,0 +1,10 @@ +{ + "description": "error on the right of and is reported against the failing function, not the operator", + "expr": "(1=1 and $contains([''], ''))", + "dataset": null, + "bindings": {}, + "error": { + "code": "T0410", + "token": "contains" + } +} diff --git a/test/test-suite/groups/boolean-expresssions/case032.json b/test/test-suite/groups/boolean-expresssions/case032.json new file mode 100644 index 0000000..d057617 --- /dev/null +++ b/test/test-suite/groups/boolean-expresssions/case032.json @@ -0,0 +1,10 @@ +{ + "description": "error on the right of or is reported against the failing function, not the operator", + "expr": "(1=0 or $contains([''], ''))", + "dataset": null, + "bindings": {}, + "error": { + "code": "T0410", + "token": "contains" + } +} diff --git a/test/test-suite/groups/boolean-expresssions/case033.json b/test/test-suite/groups/boolean-expresssions/case033.json new file mode 100644 index 0000000..35635de --- /dev/null +++ b/test/test-suite/groups/boolean-expresssions/case033.json @@ -0,0 +1,13 @@ +{ + "description": "error that nothing else has attributed is still reported against the boolean operator", + "expr": "(true and {\"k\": $.a, \"k\": $.b})", + "data": { + "a": 1, + "b": 2 + }, + "bindings": {}, + "error": { + "code": "D1009", + "token": "and" + } +}