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
This commit is contained in:
1 parent
71e11d602e
commit
deaab007e1
5 files changed
+112
-3
No files matched your search
+8
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1204,4 +1204,74 @@ describe("Concurrency tests", () => {
|
||||
{"id": "c", "delayed": "done:c"}
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user