import { VmExtern } from '@mirascript/mirascript'; import { Evaluator, Expression, isExpression, Scope } from '../dist/index.js'; import dedent from 'dedent'; const e = new Evaluator(); describe('Evaluator should work correctly', () => { it('should eval deep', () => { const s = new Scope( (k) => ({ a: Expression('b+d'), b: Expression('c+d'), c: 12, d: Expression('c'), })[k], false, ); const result = e.evaluate(Expression('a'), s); expect(result).toBe(36); }); it('should eval deep with object', () => { const s = new Scope( { a: { a: Expression('b.b + d.d') }, b: { b: Expression('c.c + d.d') }, c: { c: 12 }, d: { d: Expression('c.c') }, }, false, ); const result = e.evaluate(Expression('a.a'), s); expect(result).toBe(36); }); it('should eval deep with array', () => { const s = new Scope( { a: [Expression('b.0 + d.0')], b: [Expression('c.0 + d.0')], c: [12], d: [Expression('c.0')], }, false, ); const result = e.evaluate(Expression('a.0'), s); expect(result).toBe(36); }); it('should not eval deep with extern', () => { const s = new Scope( { a: new VmExtern([Expression('1')]), }, false, ); const result = e.evaluate(Expression('a.0'), s); expect(result?.value).toStrictEqual(Expression('1')); }); it('should eval deep with compiled expression', () => { const b = e.compile(Expression('c+d')); expect(isExpression(b)).toBe(true); const s = new Scope( { a: Expression('b+d'), b: b, c: 12, d: Expression('c'), }, false, ); const result = e.evaluate(Expression('a'), s); expect(result).toBe(36); }); it('should eval deep with object and array', () => { const s = new Scope( { arr: [1, 2, Expression('arr.0 + obj.x'), Expression('arr.2 + obj.y')], obj: { x: 3, y: Expression('arr.1 + obj.x') }, arr_copy: Expression('[..arr]'), }, false, ); const result = e.evaluate<{ arr: number[]; obj: Record; }>(Expression('(arr: [..arr], obj: (..obj))'), s)!; expect(result).toStrictEqual({ arr: [1, 2, 4, 9], obj: { x: 3, y: 5 } }); expect(result).not.toBeProxy(); expect(result.arr).not.toBeProxy(); expect(result.obj).not.toBeProxy(); const copy = e.evaluate(Expression('[..arr_copy]'), s); expect(copy).toStrictEqual([1, 2, 4, 9]); expect(copy).not.toBeProxy(); }); it('should eval with arr expr', () => { const s = new Scope( { arr: Expression('[a0, 2, 3..5]'), a0: Expression('0.4 + 0.6'), start: Expression('0'), end: Expression('start + 2'), }, false, ); const result = e.evaluate(Expression('arr'), s); expect(result).toStrictEqual([1, 2, 3, 4, 5]); expect(result).not.toBeProxy(); const slice = e.evaluate(Expression('arr[start..end]'), s); expect(slice).toStrictEqual([1, 2, 3]); expect(slice).not.toBeProxy(); const field = e.evaluate(Expression('arr.0'), s); expect(field).toBe(1); }); it('should eval with obj expr', () => { const s = new Scope( { o0: Expression('"s$(arr)"'), arr: Expression('[1, 2, 3..5]'), obj: Expression('(x: o0, y: 2 + arr.0, z: arr[1..arr.1 + 2])'), }, false, ); const result = e.evaluate<{ x: string; y: number; z: number[] }>(Expression('obj'), s)!; expect(result).toStrictEqual({ x: 's1, 2, 3, 4, 5', y: 3, z: [2, 3, 4, 5] }); expect(result).not.toBeProxy(); expect(result.z).not.toBeProxy(); const field = e.evaluate(Expression('obj.x'), s); expect(field).toStrictEqual('s1, 2, 3, 4, 5'); }); it('should eval complex', () => { const s = new Scope( { a: Expression('b'), b: Expression('c'), c: Expression('d'), d: Expression('e'), e: Expression('f'), f: Expression('g'), g: Expression('h'), h: Expression('i'), i: Expression('j'), j: Expression('k'), k: Expression('l'), l: Expression('m'), m: Expression('n'), n: Expression('o'), o: Expression('p'), p: Expression('q'), q: Expression('r'), r: Expression('s'), s: Expression('t'), t: Expression('u'), u: Expression('v'), v: Expression('w'), w: Expression('x'), x: Expression('y'), y: Expression('z'), z: 1, }, false, ); const result = e.evaluate(Expression('a + a + a + a + a + a + a + a + a + a'), s); expect(result).toBe(10); }); it('should report error from deps', () => { const s = new Scope( { a: { e: Expression('x'), x: Expression('a.e'), }, b: Expression('c'), c: Expression('a.x'), }, true, ); expect(() => e.evaluate(Expression('b'), s)).toThrow( dedent` Global variable 'x' is not defined. In : @ a/e In : @ a/x In : @ c In : @ b`, ); }); it('should report loop by scope', () => { const s = new Scope( { a: Expression('b'), b: Expression('c'), c: Expression('a'), }, true, ); expect(() => e.evaluate(Expression('a'), s)).toThrow(/Execution recursion exceeds limit/); }); it('should report loop by mirascript', () => { const s = new Scope( { a: Expression('fn x{b}x()'), b: Expression('fn x{c}x()'), c: Expression('fn x{a}x()'), }, true, ); expect(() => e.evaluate(Expression('a'), s)).toThrow(/Maximum call depth exceeded/); }); it('should not pass some special as proxy', () => { expect.assertions(9); const o = new VmExtern({ x: { p: 2 }, y: 1, z: Expression('o.y+o.x.p'), d: new Date(), r: /r/, f() { return this.z; }, p: Promise.resolve(1), a: new ArrayBuffer(0), v: new DataView(new ArrayBuffer(0)), test() { expect(this).not.toBeProxy(); expect(this.d).not.toBeProxy(); expect(this.r).not.toBeProxy(); // eslint-disable-next-line @typescript-eslint/unbound-method expect(this.f).not.toBeProxy(); expect(this.p).not.toBeProxy(); expect(this.a).not.toBeProxy(); expect(this.v).not.toBeProxy(); return this.f(); }, }); const s = new Scope({ o }, true); expect(e.evaluate(Expression('o.test()'), s)?.value).toStrictEqual(Expression('o.y+o.x.p')); expect(e.evaluate(Expression('o.z'), s)?.value).toStrictEqual(Expression('o.y+o.x.p')); }); });