Skip to content

Commit

Permalink
Enhances the evaluate expression library that enables full support fo…
Browse files Browse the repository at this point in the history
…r assignment and sequence expressions (cncjs#442)

* Enables full support for assignment expression and sequence expression

* eslint fixes
  • Loading branch information
cheton authored Apr 2, 2019
1 parent 74e3f5b commit b349124
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 49 deletions.
125 changes: 101 additions & 24 deletions src/app/lib/evaluate-expression.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,119 @@
import get from 'lodash/get';
import _set from 'lodash/set';
import { parse } from 'esprima';
import evaluate from 'static-eval';
import logger from './logger';

const log = logger('evaluateExpression');
const log = logger('evaluate-expression');

const evaluateExpression = (src, context) => {
if (!src || typeof context !== 'object') {
return;
const isStaticMemberExpression = (node) => typeof node === 'object' && node.type === 'MemberExpression' && !node.computed;
const isComputedMemberExpression = (node) => typeof node === 'object' && node.type === 'MemberExpression' && !!node.computed;

const lookupObjectPath = (node, context) => {
if (!node) {
return [];
}

/*
* Expression: 'x = value'
*
* Identifier { type: 'Identifier', name: 'x' }
*/
if (node.type === 'Identifier') {
return [node.name];
}

if (isComputedMemberExpression(node)) {
return [...lookupObjectPath(node.object, context), evaluate(node.property, context)];
}

if (isStaticMemberExpression(node)) {
/*
* Expression: 'x.y = value'
*
* StaticMemberExpression {
* type: 'MemberExpression',
* computed: false,
* object: Identifier { type: 'Identifier', name: 'x' },
* property: Identifier { type: 'Identifier', name: 'y' }
* }
*
* Expression: 'x[y] = value'
*
* ComputedMemberExpression {
* type: 'MemberExpression',
* computed: true,
* object: Identifier { type: 'Identifier', name: 'x' },
* property: Identifier { type: 'Identifier', name: 'y' }
* }
*/
if (node.property.type === 'Identifier') {
return [...lookupObjectPath(node.object, context), node.property.name];
}

/*
* Expression: 'x["y"] = value'
*
* ComputedMemberExpression {
* type: 'MemberExpression',
* computed: true,
* object: Identifier { type: 'Identifier', name: 'x' },
* property: Literal { type: 'Literal', value: 'y', raw: '"y"' }
* }
*/
if (node.property.type === 'Literal') {
return [...lookupObjectPath(node.object, context), node.property.value];
}

return [...lookupObjectPath(node.object, context), evaluate(node.property, context)];
}

return [node.name];
};

const evaluateAssignmentExpressionWithContext = (node, context) => {
console.assert(node && node.type === 'AssignmentExpression');

const path = lookupObjectPath(node.left, context);
if (path) {
const value = evaluate(node.right, context);
_set(context, path, value);
}
};

const evaluateSequenceExpressionWithContext = (node, context) => {
console.assert(node && node.type === 'SequenceExpression');

node.expressions.forEach(expr => {
if (expr.type === 'AssignmentExpression') {
evaluateAssignmentExpressionWithContext(expr, context);
return;
}

evaluate(expr, context);
});
};

const evaluateExpression = (src, context = {}) => {
if (!src) {
return context;
}

try {
const ast = parse(src).body[0].expression;

if (ast.type === 'SequenceExpression') {
ast.expressions.forEach((expr) => {
if (get(expr, 'left.type') === 'Identifier') {
const name = get(expr, 'left.name') || '';
if (name) {
const value = evaluate(expr.right, context);
context[name] = value;
}
}
});
} else if (ast.type === 'AssignmentExpression') {
const expr = ast;
if (get(expr, 'left.type') === 'Identifier') {
const name = get(expr, 'left.name') || '';
if (name) {
const value = evaluate(expr.right, context);
context[name] = value;
}
}
if (ast.type === 'AssignmentExpression') {
evaluateAssignmentExpressionWithContext(ast, context);
} else if (ast.type === 'SequenceExpression') {
evaluateSequenceExpressionWithContext(ast, context);
} else {
evaluate(ast, context);
}
} catch (e) {
log.error(`evaluateExpression: src="${src}", context=${JSON.stringify(context)}`);
log.error(e);
}

return context;
};

export default evaluateExpression;
2 changes: 1 addition & 1 deletion src/app/lib/translate-expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { parse } from 'esprima';
import evaluate from 'static-eval';
import logger from './logger';

const log = logger('translateExpression');
const log = logger('translate-expression');
const re = new RegExp(/\[[^\]]+\]/g);

const translateExpression = (data, context = {}) => {
Expand Down
100 changes: 76 additions & 24 deletions test/evaluate-expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,82 @@ test('exceptions', (t) => {
});

test('expressions', (t) => {
const context = {
wposx: 10,
wposy: 20,
wposz: 30
};

evaluateExpression(' _a = 0.1', context);
t.same(context, {
wposx: 10,
wposy: 20,
wposz: 30,
_a: 0.1
});

evaluateExpression(' _x=(wposx+5), _y = wposy + 5, _z = (wposz+1*5) ', context);
t.same(context, {
wposx: 10,
wposy: 20,
wposz: 30,
_x: 15,
_y: 25,
_z: 35,
_a: 0.1
});
{ // Evaluates expressions with variables
const vars = {
wposx: 10,
wposy: 20,
wposz: 30,
};

// Evaluate assignment expression
evaluateExpression('value = 0.1', vars);
t.same(vars, {
wposx: 10,
wposy: 20,
wposz: 30,
value: 0.1,
});

// Evaluate sequence expression
evaluateExpression(' _x=(wposx+5), _y = wposy + 5, _z = (wposz+1*5) ', vars);
t.same(vars, {
wposx: 10,
wposy: 20,
wposz: 30,
value: 0.1,
_x: 15,
_y: 25,
_z: 35,
});
}

{ // Evaluates expressions containing template literals
const bar = '0';
const baz = 1;

t.test(t => {
const vars = evaluateExpression('bar = "0", baz = 1, foo[bar][baz] = `${bar}${baz}`', { bar, baz }); // eslint-disable-line
t.equal(vars.bar, bar);
t.equal(vars.baz, baz);
t.equal(vars.foo[bar][baz], '01');
t.end();
});

t.test(t => {
const vars = evaluateExpression('bar = "0", baz = 1, foo[1][`baz`] = baz', { bar, baz });
t.equal(vars.foo[1].baz, baz);
t.end();
});

t.test(t => {
const vars = evaluateExpression('bar = "0", baz = 1, foo[bar][baz] = `${bar}${baz}`', { bar, baz }); // eslint-disable-line
t.equal(vars.foo[bar][baz], `${bar}${baz}`);
t.end();
});

t.test(t => {
const vars = evaluateExpression('bar = "0", baz = 1, foo.bar.baz = `${bar}${baz}`', { bar, baz }); // eslint-disable-line
t.equal(vars.foo.bar.baz, `${bar}${baz}`);
t.end();
});
}

{ // Sets the value at path of object
t.test(t => {
const vars = evaluateExpression('x.y.z.a.b.c = 1');
t.equal(vars.x.y.z.a.b.c, 1);
t.end();
});

t.test(t => {
const vars = evaluateExpression('dx = 4000, dy = 3000, dz = 1000, global.volume = dx * dy * dz');
t.equal(vars.dx, 4000);
t.equal(vars.dy, 3000);
t.equal(vars.dz, 1000);
t.equal(vars.global.volume, 4000 * 3000 * 1000);
t.end();
});
}

t.end();
});

0 comments on commit b349124

Please sign in to comment.