Skip to content

Commit

Permalink
Add mode to throw on invalid property access, fixes mozilla#25
Browse files Browse the repository at this point in the history
  • Loading branch information
dmose committed Jul 15, 2022
1 parent 8228a67 commit e88d7b4
Show file tree
Hide file tree
Showing 9 changed files with 515 additions and 453 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,10 +282,12 @@ resolve and use that value!

### API

#### mozjexl.Jexl
#### mozjexl.Jexl(throwOnMissingProp)
A reference to the Jexl constructor. To maintain separate instances of Jexl
with each maintaining its own set of transforms, simply re-instantiate with
`new mozjexl.Jexl()`.
`new mozjexl.Jexl()`. If `throwOnMissingProp` is set to `true`, the evaluator
will throw an exception if an expression attempts to access a property that does
not exist, otherwise it will silently continue.

#### mozjexl.addBinaryOp(_{string} operator_, _{number} precedence_, _{function} fn_)
Adds a binary operator to the Jexl instance. A binary operator is one that
Expand Down
10 changes: 8 additions & 2 deletions lib/Jexl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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());
Expand Down
9 changes: 8 additions & 1 deletion lib/evaluator/Evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,18 @@ 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 = throwOnMissingProp || false;
};

/**
Expand Down
37 changes: 33 additions & 4 deletions lib/evaluator/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,45 @@ exports.FilterExpression = function(ast) {
* @private
*/
exports.Identifier = function(ast) {
function contextHasProp(context, prop) {
// If this is primitive object (eg "string") we can't check
// this, which is why we have to be careful not to look in things
// that aren't objects.
if (typeof context == "object" && !(prop in context)) {
return false;
}

return true;
}

let that = this;
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 (that._throwOnMissingProp && !contextHasProp(context, ast.value)) {
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 (
that._throwOnMissingProp &&
!contextHasProp(contextToCheck, ast.value)
) {
throw new Error(
`default context does not have an identifier named ${ast.value}`
);
}

return contextToCheck[ast.value];
}
};

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"eslint-plugin-prettier": "^2.6.0",
"license-webpack-plugin": "0.5.1",
"prettier": "^1.10.2",
"webpack": "3.1.0"
"webpack": "3.1.0",
"webpack-cli": "^4.10.0"
}
}
206 changes: 206 additions & 0 deletions test/evaluator/Evaluator-throw-on-missing-prop-mode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/*
* Jexl
* Copyright (c) 2015 TechnologyAdvice
*/

var chai = require("chai"),
chaiAsPromised = require("chai-as-promised"),
Lexer = require("../../lib/Lexer"),
Parser = require("../../lib/parser/Parser"),
Evaluator = require("../../lib/evaluator/Evaluator"),
grammar = require("../../lib/grammar").elements;

chai.should();
chai.use(chaiAsPromised);

var lexer = new Lexer(grammar);

function toTree(exp) {
var p = new Parser(grammar);
p.addTokens(lexer.tokenize(exp));
return p.complete();
}

describe("Evaluator in throwOnMissingProp mode", function() {
it("should evaluate an arithmetic expression", function() {
const e = new Evaluator(grammar, null, null, null, true);
return e.eval(toTree("(2 + 3) * 4")).should.become(20);
});
it("should evaluate a string concat", function() {
var e = new Evaluator(grammar, null, null, null, true);
return e
.eval(toTree('"Hello" + (4+4) + "Wo\\"rld"'))
.should.become('Hello8Wo"rld');
});
it("should evaluate a true comparison expression", function() {
var e = new Evaluator(grammar, null, null, null, true);
return e.eval(toTree("2 > 1")).should.become(true);
});
it("should evaluate a false comparison expression", function() {
var e = new Evaluator(grammar, null, null, null, true);
return e.eval(toTree("2 <= 1")).should.become(false);
});
it("should evaluate a complex expression", function() {
var e = new Evaluator(grammar, null, null, null, true);
return e
.eval(toTree('"foo" && 6 >= 6 && 0 + 1 && true'))
.should.become(true);
});
it("should evaluate an identifier chain", function() {
var context = { foo: { baz: { bar: "tek" } } },
e = new Evaluator(grammar, null, context, null, true);
return e.eval(toTree("foo.baz.bar")).should.become(context.foo.baz.bar);
});
it("should apply transforms", function() {
var context = { foo: 10 },
half = function(val) {
return val / 2;
},
e = new Evaluator(grammar, { half: half }, context, null, true);
return e.eval(toTree("foo|half + 3")).should.become(8);
});
it("should filter arrays", function() {
var context = {
foo: {
bar: [{ tek: "hello" }, { tek: "baz" }, { tok: "baz" }]
}
},
e = new Evaluator(grammar, null, context, null, true);
return e
.eval(toTree('foo.bar[.tek == "baz"]'))
.should.eventually.deep.equal([{ tek: "baz" }]);
});
it("should assume array index 0 when traversing", function() {
var context = {
foo: {
bar: [{ tek: { hello: "world" } }, { tek: { hello: "universe" } }]
}
},
e = new Evaluator(grammar, null, context, null, true);
return e.eval(toTree("foo.bar.tek.hello")).should.become("world");
});
it("should make array elements addressable by index", function() {
var context = {
foo: {
bar: [{ tek: "tok" }, { tek: "baz" }, { tek: "foz" }]
}
},
e = new Evaluator(grammar, null, context, null, true);
return e.eval(toTree("foo.bar[1].tek")).should.become("baz");
});
it("should allow filters to select object properties", function() {
var context = { foo: { baz: { bar: "tek" } } },
e = new Evaluator(grammar, null, context, null, true);
return e
.eval(toTree('foo["ba" + "z"].bar'))
.should.become(context.foo.baz.bar);
});
it("should reject simple filters on undefined objects", function() {
var context = { foo: {} },
e = new Evaluator(grammar, null, context, null, true);
return e.eval(toTree('foo.bar["baz"].tok')).should.be.rejected;
});
it("should reject complex filters on undefined objects", function() {
var context = { foo: {} },
e = new Evaluator(grammar, null, context, null, true);
return e.eval(toTree("foo.bar[.size > 1].baz")).should.be.rejected;
});
it("should throw when transform does not exist", function() {
var e = new Evaluator(grammar, null, null, null, true);
return e.eval(toTree('"hello"|world')).should.be.rejected;
});
it("should throw when top-level identifier doesn't exist", function() {
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", function() {
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, null, null, null, true);
return e.eval(toTree("7 // 2")).should.become(3);
});
it("should evaluate an object literal", function() {
var e = new Evaluator(grammar, null, null, null, true);
return e
.eval(toTree('{foo: {bar: "tek"}}'))
.should.eventually.deep.equal({ foo: { bar: "tek" } });
});
it("should evaluate an empty object literal", function() {
var e = new Evaluator(grammar, null, null, null, true);
return e.eval(toTree("{}")).should.eventually.deep.equal({});
});
it("should evaluate a transform with multiple args", function() {
var e = new Evaluator(
grammar,
{
concat: function(val, a1, a2, a3) {
return val + ": " + a1 + a2 + a3;
}
},
null,
null,
true
);
return e
.eval(toTree('"foo"|concat("baz", "bar", "tek")'))
.should.become("foo: bazbartek");
});
it("should evaluate dot notation for object literals", function() {
var e = new Evaluator(grammar, null, null, null, true);
return e.eval(toTree('{foo: "bar"}.foo')).should.become("bar");
});
it("should allow access to literal properties", function() {
var e = new Evaluator(grammar, null, null, null, true);
return e.eval(toTree('"foo".length')).should.become(3);
});
it("should evaluate array literals", function() {
var e = new Evaluator(grammar, null, null, null, true);
return e
.eval(toTree('["foo", 1+2]'))
.should.eventually.deep.equal(["foo", 3]);
});
it("should allow properties on empty arrays", function() {
var context = { foo: {} },
e = new Evaluator(grammar, null, context, null, true);
return e.eval(toTree("[].baz")).should.become(undefined);
});
it('should apply the "in" operator to strings', function() {
var e = new Evaluator(grammar, null, null, null, true);
return Promise.all([
e.eval(toTree('"bar" in "foobartek"')).should.become(true),
e.eval(toTree('"baz" in "foobartek"')).should.become(false)
]);
});
it('should apply the "in" operator to arrays', function() {
var e = new Evaluator(grammar, null, null, null, true);
return Promise.all([
e.eval(toTree('"bar" in ["foo","bar","tek"]')).should.become(true),
e.eval(toTree('"baz" in ["foo","bar","tek"]')).should.become(false)
]);
});
it("should evaluate a conditional expression", function() {
var e = new Evaluator(grammar, null, null, null, true);
return Promise.all([
e.eval(toTree('"foo" ? 1 : 2')).should.become(1),
e.eval(toTree('"" ? 1 : 2')).should.become(2)
]);
});
it("should allow missing consequent in ternary", function() {
var e = new Evaluator(grammar, null, null, null, true);
return e.eval(toTree('"foo" ?: "bar"')).should.become("foo");
});
it("does not treat falsey properties as undefined", function() {
const e = new Evaluator(grammar, null, null, null, true);
return e.eval(toTree('"".length')).should.become(0);
});
it("should handle an expression with arbitrary whitespace", function() {
var e = new Evaluator(grammar, null, null, null, true);
return e.eval(toTree("(\t2\n+\n3) *\n4\n\r\n")).should.become(20);
});
});
2 changes: 1 addition & 1 deletion test/evaluator/Evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ 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 apply the DivFloor operator", function() {
var e = new Evaluator(grammar);
Expand Down
Loading

0 comments on commit e88d7b4

Please sign in to comment.