From 15ead6a05bfc5d3c620679f1c59692cc66515164 Mon Sep 17 00:00:00 2001 From: Dan Mosedale Date: Mon, 27 Jun 2022 19:13:29 -0700 Subject: [PATCH] Add mode to throw on invalid property access, fixes #25 --- lib/Jexl.js | 10 ++++++-- lib/evaluator/Evaluator.js | 18 +++++++++++-- lib/evaluator/handlers.js | 23 ++++++++++++++--- test/evaluator/Evaluator.js | 14 +++++++++- vendor/mozjexl.jsm | 51 +++++++++++++++++++++++++++++++------ 5 files changed, 99 insertions(+), 17 deletions(-) diff --git a/lib/Jexl.js b/lib/Jexl.js index 14fc6ce0..19cfbf34 100644 --- a/lib/Jexl.js +++ b/lib/Jexl.js @@ -14,10 +14,11 @@ var Evaluator = require("./evaluator/Evaluator"), * xpath-like drilldown into native Javascript objects. * @constructor */ -function Jexl() { +function Jexl(throwOnMissingProp) { this._customGrammar = null; this._lexer = null; this._transforms = {}; + this._throwOnMissingProp = throwOnMissingProp || true; } /** @@ -174,7 +175,12 @@ Jexl.prototype._eval = function(exp, context) { var self = this, grammar = this._getGrammar(), parser = new Parser(grammar), - evaluator = new Evaluator(grammar, this._transforms, context); + evaluator = new Evaluator( + grammar, + this._transforms, + context, + this._throwOnMissingProp + ); return Promise.resolve().then(function() { parser.addTokens(self._getLexer().tokenize(exp)); return evaluator.eval(parser.complete()); diff --git a/lib/evaluator/Evaluator.js b/lib/evaluator/Evaluator.js index 2eae6af7..3a784805 100644 --- a/lib/evaluator/Evaluator.js +++ b/lib/evaluator/Evaluator.js @@ -35,11 +35,20 @@ var handlers = require("./handlers"); * to resolve the value of a relative identifier. * @constructor */ -var Evaluator = function(grammar, transforms, context, relativeContext) { +var Evaluator = function( + grammar, + transforms, + context, + relativeContext, + throwOnMissingProp +) { this._grammar = grammar; this._transforms = transforms || {}; this._context = context || {}; this._relContext = relativeContext || this._context; + this._throwOnMissingProp = true; + throwOnMissingProp || false; + console.log("_tOMP", this._throwOnMissingProp); }; /** @@ -50,7 +59,12 @@ var Evaluator = function(grammar, transforms, context, relativeContext) { Evaluator.prototype.eval = function(ast) { var self = this; return Promise.resolve().then(function() { - return handlers[ast.type].call(self, ast); + try { + var retVal = handlers[ast.type].call(self, ast); + } catch (ex) { + console.log("handler threw: ", ex); + } + return retVal; }); }; diff --git a/lib/evaluator/handlers.js b/lib/evaluator/handlers.js index 5ffe32bb..9b3d08ae 100644 --- a/lib/evaluator/handlers.js +++ b/lib/evaluator/handlers.js @@ -81,16 +81,31 @@ exports.FilterExpression = function(ast) { * @private */ exports.Identifier = function(ast) { + let throwOnMissingProp = true; // this._throwOnMissingProp; + if (ast.from) { return this.eval(ast.from).then(function(context) { if (Array.isArray(context)) context = context[0]; - if (context === undefined) return undefined; + if (context === undefined) return undefined; // XXX deleteme? testme? + if (throwOnMissingProp && !(ast.value in context)) { + throw new Error( + `stemmed context does not have an identifier named ${ast.value}` + ); + } + return context[ast.value]; }); } else { - return ast.relative - ? this._relContext[ast.value] - : this._context[ast.value]; + const contextToCheck = ast.relative ? this._relContext : this._context; + + console.log("b4"); + if (throwOnMissingProp && !(ast.value in contextToCheck)) { + throw new Error( + `default context does not have an identifier named ${ast.value}` + ); + } + + return contextToCheck[ast.value]; } }; diff --git a/test/evaluator/Evaluator.js b/test/evaluator/Evaluator.js index 49f5300a..5a439f76 100644 --- a/test/evaluator/Evaluator.js +++ b/test/evaluator/Evaluator.js @@ -107,7 +107,19 @@ describe("Evaluator", function() { }); it("should throw when transform does not exist", function() { var e = new Evaluator(grammar); - return e.eval(toTree('"hello"|world')).should.reject; + return e.eval(toTree('"hello"|world')).should.be.rejected; + }); + it("should throw when top-level identifier doesn't exist in throw mode", function() { + console.log("about to pass in true"); + var context = { foo: { baz: { bar: "dog" } } }, + e = new Evaluator(grammar, null, context, null, true); + return e.eval(toTree("monkey")).should.be.rejected; + }); + it("should throw when child identifier doesn't exist in throw mode", function() { + console.log("about to pass in true"); + var context = { foo: { baz: { bar: "cat" } } }, + e = new Evaluator(grammar, null, context, null, true); + return e.eval(toTree("foo.baz.monkey")).should.be.rejected; }); it("should apply the DivFloor operator", function() { var e = new Evaluator(grammar); diff --git a/vendor/mozjexl.jsm b/vendor/mozjexl.jsm index a51767fd..b4098749 100644 --- a/vendor/mozjexl.jsm +++ b/vendor/mozjexl.jsm @@ -300,10 +300,11 @@ var Evaluator = __webpack_require__(2), * xpath-like drilldown into native Javascript objects. * @constructor */ -function Jexl() { +function Jexl(throwOnMissingProp) { this._customGrammar = null; this._lexer = null; this._transforms = {}; + this._throwOnMissingProp = throwOnMissingProp || true; } /** @@ -460,7 +461,12 @@ Jexl.prototype._eval = function(exp, context) { var self = this, grammar = this._getGrammar(), parser = new Parser(grammar), - evaluator = new Evaluator(grammar, this._transforms, context); + evaluator = new Evaluator( + grammar, + this._transforms, + context, + this._throwOnMissingProp + ); return Promise.resolve().then(function() { parser.addTokens(self._getLexer().tokenize(exp)); return evaluator.eval(parser.complete()); @@ -552,11 +558,20 @@ var handlers = __webpack_require__(3); * to resolve the value of a relative identifier. * @constructor */ -var Evaluator = function(grammar, transforms, context, relativeContext) { +var Evaluator = function( + grammar, + transforms, + context, + relativeContext, + throwOnMissingProp +) { this._grammar = grammar; this._transforms = transforms || {}; this._context = context || {}; this._relContext = relativeContext || this._context; + this._throwOnMissingProp = true; + throwOnMissingProp || false; + console.log("_tOMP", this._throwOnMissingProp); }; /** @@ -567,7 +582,12 @@ var Evaluator = function(grammar, transforms, context, relativeContext) { Evaluator.prototype.eval = function(ast) { var self = this; return Promise.resolve().then(function() { - return handlers[ast.type].call(self, ast); + try { + var retVal = handlers[ast.type].call(self, ast); + } catch (ex) { + console.log("handler threw: ", ex); + } + return retVal; }); }; @@ -762,16 +782,31 @@ exports.FilterExpression = function(ast) { * @private */ exports.Identifier = function(ast) { + let throwOnMissingProp = true; // this._throwOnMissingProp; + if (ast.from) { return this.eval(ast.from).then(function(context) { if (Array.isArray(context)) context = context[0]; - if (context === undefined) return undefined; + if (context === undefined) return undefined; // XXX deleteme? testme? + if (throwOnMissingProp && !(ast.value in context)) { + throw new Error( + `stemmed context does not have an identifier named ${ast.value}` + ); + } + return context[ast.value]; }); } else { - return ast.relative - ? this._relContext[ast.value] - : this._context[ast.value]; + const contextToCheck = ast.relative ? this._relContext : this._context; + + console.log("b4"); + if (throwOnMissingProp && !(ast.value in contextToCheck)) { + throw new Error( + `default context does not have an identifier named ${ast.value}` + ); + } + + return contextToCheck[ast.value]; } };