UNPKG

12.1 kBJavaScriptView Raw
1// MIT License
2//
3// Copyright 2016-2017 Electric Imp
4//
5// SPDX-License-Identifier: MIT
6//
7// Permission is hereby granted, free of charge, to any person obtaining a copy
8// of this software and associated documentation files (the "Software"), to deal
9// in the Software without restriction, including without limitation the rights
10// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11// copies of the Software, and to permit persons to whom the Software is
12// furnished to do so, subject to the following conditions:
13//
14// The above copyright notice and this permission notice shall be
15// included in all copies or substantial portions of the Software.
16//
17// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
20// EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
21// OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
22// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23// OTHER DEALINGS IN THE SOFTWARE.
24
25/**
26 * Supported syntax:
27 *
28 * Binary operators:
29 * =================
30 *
31 * || && == != < > <= >= + - * / %
32 *
33 * Unary operators:
34 * ================
35 *
36 * + - !
37 *
38 * Filter operator
39 * ===============
40 *
41 * |
42 *
43 * value|filter === filter(value)
44 * value|filter(arg) === filter(value, arg)
45 *
46 * Member expressions:
47 * ===================
48 *
49 * foo.bar
50 * foo["bar"]
51 * ([1, 2, 3])[1]
52 *
53 * Types:
54 * ======
55 *
56 * true
57 * false
58 * null
59 * "string" 'literals'
60 * numbers: 1, 2, 3, 1.1005000, 1E6 1E-6, 1e6
61 *
62 * Conditional expressions:
63 * ========================
64 *
65 * result = test ? consequent : alternate
66 *
67 * Array expressions:
68 * ==================
69 *
70 * [1, 2, 3]
71 *
72 * Also @see https://github.com/soney/jsep/blob/master/src/jsep.js
73 */
74
75'use strict';
76
77const jsep = require('jsep');
78const merge = require('./merge');
79
80// <editor-fold desc="Errors" defaultstate="collapsed">
81const Errors = {};
82
83Errors.ExpressionError = class ExpressionError extends Error {
84};
85
86Errors.MacroDeclarationError = class MacroDeclarationError extends Errors.ExpressionError {
87};
88
89Errors.FunctionCallError = class FunctionCallError extends Errors.ExpressionError {
90};
91// </editor-fold>
92
93/**
94 * Expression evaluator
95 */
96class Expression {
97
98 constructor(machine) {
99 this.functions = {};
100 this._initParser();
101 this._machine = machine || {};
102 }
103
104 /**
105 * Configure parser
106 * @private
107 */
108 _initParser() {
109 this._jsep = jsep;
110
111 // remove binary ops
112 this._jsep.removeBinaryOp('!==');
113 this._jsep.removeBinaryOp('===');
114 this._jsep.removeBinaryOp('>>');
115 this._jsep.removeBinaryOp('<<');
116 this._jsep.removeBinaryOp('>>>');
117 this._jsep.removeBinaryOp('&');
118 this._jsep.removeBinaryOp('^');
119
120 // remove unary ops
121 this._jsep.removeUnaryOp('~');
122 }
123
124 /**
125 * Evaluate an expression
126 * @param {string} expression
127 * @param context
128 * @return {*}
129 */
130 evaluate(expression, context) {
131 try {
132 return this._evaluate(this._jsep(expression), context || {});
133 } catch (e) {
134
135 // rethrow errors with a custom type
136 if (!(e instanceof Errors.ExpressionError)) {
137 throw new Errors.ExpressionError(e.message);
138 }
139
140 throw e;
141 }
142 }
143
144 /**
145 * Parse macro call expression
146 * @param {string} text - expression text
147 * @param {{}} context - context
148 * @param {{}} macros - defined macroses
149 * @return {{name, args: []}|null}
150 */
151 parseMacroCall(text, context, definedMacros) {
152 let root;
153
154 try {
155 root = this._jsep(text);
156 } catch (e) {
157 return null;
158 }
159
160 if (root.type !== 'CallExpression'
161 || root.callee.type !== 'Identifier'
162 || !definedMacros.hasOwnProperty(root.callee.name)) {
163 // not a macro
164 return null;
165 }
166
167 return {
168 name: root.callee.name,
169 args: root['arguments'].map(v => this._evaluate(v, context))
170 };
171 }
172
173 /**
174 * Parse macro declartion
175 * @param text - declaration text
176 * @return {{name, args: []}}
177 * @throws {Errors.MacroDeclarationError}
178 */
179 parseMacroDeclaration(text) {
180 let root;
181
182 try {
183 root = this._jsep(text);
184 } catch (e) {
185 // rethrow as custom error type
186 throw new Errors.ExpressionError(e.message);
187 }
188
189 if (root.type !== 'CallExpression' || root.callee.type !== 'Identifier') {
190 throw new Errors.MacroDeclarationError(`Syntax error in macro declaration`);
191 }
192
193 for (const arg of root['arguments']) {
194 if (arg.type !== 'Identifier') {
195 throw new Errors.MacroDeclarationError(`Syntax error in macro declaration`);
196 }
197 }
198
199 return {
200 name: root.callee.name,
201 args: root['arguments'].map(v => v.name)
202 };
203 }
204
205 /**
206 * @param {{}} node
207 * @param {{}} context - defined variables
208 * @private
209 */
210 _evaluate(node, context) {
211
212 let res = null;
213
214 // walk through the AST
215
216 switch (node.type) {
217
218 case 'BinaryExpression':
219 case 'LogicalExpression':
220
221 // check that we have both left and right parts
222 if (node.left === false || node.right === false) {
223 throw new Errors.ExpressionError('Syntax error in "' + node.operator + '" operator');
224 }
225
226 if ('|' === node.operator /* filter operator */) {
227
228 if (node.right.type === 'CallExpression' /* value|filter() */) {
229
230 // set left-hand expression as the first argument
231 node.right.arguments.unshift(node.left);
232 res = this._evaluate(node.right, context);
233
234 } else /* value|filter */{
235
236 // construct call expression
237 const filterCallExpression = {
238 type: 'CallExpression',
239 arguments: [node.left],
240 callee: node.right
241 };
242
243 res = this._evaluate(filterCallExpression, context);
244 }
245
246 } else {
247
248 const left = this._evaluate(node.left, context);
249 const right = this._evaluate(node.right, context);
250
251 switch (node.operator) {
252
253 case '-':
254 res = left - right;
255 break;
256
257 case '+':
258 res = left + right;
259 break;
260
261 case '*':
262 res = left * right;
263 break;
264
265 case '/':
266
267 if (0 === right) {
268 throw new Errors.ExpressionError('Division by zero');
269 }
270
271 res = left / right;
272 break;
273
274 case '%':
275
276 if (0 === right) {
277 throw new Errors.ExpressionError('Division by zero');
278 }
279
280 res = left % right;
281 break;
282
283 case '||':
284 res = left || right;
285 break;
286
287 case '&&':
288 res = left && right;
289 break;
290
291 case '==':
292 res = left == right;
293 break;
294
295 case '!=':
296 res = left != right;
297 break;
298
299 case '>':
300 res = left > right;
301 break;
302
303 case '<':
304 res = left < right;
305 break;
306
307 case '>=':
308 res = left >= right;
309 break;
310
311 case '<=':
312 res = left <= right;
313 break;
314
315 default:
316 throw new Errors.ExpressionError('Unknown binary operator: ' + node.operator);
317 }
318 }
319
320 break;
321
322 case 'Literal':
323
324 res = node.value;
325 break;
326
327 case 'Identifier':
328
329 if /* call expression callee name */ (
330 'defined' === node.name ||
331 context.hasOwnProperty(node.name) && typeof context[node.name] === 'function'
332 ) {
333 res = node.name;
334 } else /* variable */ if (context.hasOwnProperty(node.name)) {
335 res = context[node.name];
336 } else if /* global call expression callee name */ (
337 'defined' === node.name ||
338 this._globalContext.hasOwnProperty(node.name) && typeof this._globalContext[node.name] === 'function'
339 ) {
340 res = node.name;
341 } else /* global variable */ if (this._globalContext.hasOwnProperty(node.name)) {
342 res = this._globalContext[node.name];
343 } else /* environment */ if (process.env.hasOwnProperty(node.name)) {
344 res = process.env[node.name];
345 } else {
346 // undefined, that's fine, just leave it null
347 }
348
349 break;
350
351 case 'UnaryExpression':
352
353 const argument = this._evaluate(node.argument, context);
354
355 switch (node.operator) {
356
357 case '+':
358 res = argument;
359 break;
360
361 case '!':
362 res = !argument;
363 break;
364
365 case '-':
366 res = -argument;
367 break;
368
369 default:
370 throw new Errors.ExpressionError('Unknown unary operator: ' + node.operator);
371 }
372
373 break;
374
375 case 'Compound':
376 throw new Errors.ExpressionError('Syntax error');
377
378 case 'MemberExpression':
379
380 const object = this._evaluate(node.object, context);
381 const property = node.computed ? this._evaluate(node.property, context) : node.property.name;
382
383 if (!object) {
384 throw new Errors.ExpressionError(`Owner of "${property}" property is undefined`);
385 }
386
387 if (!(property in object)) {
388 throw new Errors.ExpressionError(`Property "${property}" is not defined`);
389 }
390
391 res = object[property];
392
393 break;
394
395 case 'ThisExpression':
396 throw new Errors.ExpressionError('`this` keyword is not supported');
397
398 case 'ConditionalExpression':
399 const test = this._evaluate(node.test, context);
400
401 if (test) {
402 res = this._evaluate(node.consequent, context);
403 } else {
404 res = this._evaluate(node.alternate, context);
405 }
406
407 break;
408
409 case 'ArrayExpression':
410
411 res = node.elements.map(v => this._evaluate(v, context));
412 break;
413
414 case 'CallExpression':
415
416 const callee = this._evaluate(node.callee, context);
417
418 // "defined" is not a function, but a syntactic construction
419 if ('defined' === callee) {
420
421 // defined(varName) should not evaluate variable
422 if ('Identifier' !== node.arguments[0].type) {
423 throw new Errors.ExpressionError('defined() can only be called with an identifier as an argument');
424 }
425
426 res = context.hasOwnProperty(node.arguments[0].name)
427 || this._globalContext.hasOwnProperty(node.arguments[0].name);
428
429 } else {
430
431 const args = node.arguments.map(v => this._evaluate(v, context));
432
433 if (context.hasOwnProperty(callee) && typeof context[callee] === 'function') {
434 res = context[callee].apply(merge(context, { globals: this._globalContext }), args);
435 } else if (typeof callee === 'function') {
436 res = callee.apply(merge(context, { globals: this._globalContext }), args);
437 } else if (this._globalContext.hasOwnProperty(callee) && typeof this._globalContext[callee] === 'function') {
438 res = this._globalContext[callee].apply(merge(context, { globals: this._globalContext }), args);
439 } else {
440
441 if (node.callee.type === 'Identifier') {
442 throw new Errors.FunctionCallError(`Function "${node.callee.name}" is not defined`);
443 } else if (typeof callee === 'string' || callee instanceof String) {
444 throw new Errors.FunctionCallError(`Function "${callee}" is not defined`);
445 } else {
446 throw new Errors.FunctionCallError(`Can't call a non-callable expression`);
447 }
448 }
449
450 }
451
452 break;
453
454 default:
455 throw new Errors.ExpressionError('Unknown node type: "' + node.type + '"');
456 }
457
458 return res;
459
460 }
461
462 get _globalContext() {
463 return this._machine._globalContext || {};
464 }
465}
466
467module.exports = Expression;
468module.exports.Errors = Errors;