diff --git a/benchmark/index.js b/benchmark/index.js index d8c3e40..f97ecdb 100644 --- a/benchmark/index.js +++ b/benchmark/index.js @@ -7,8 +7,8 @@ */ import {performance} from 'node:perf_hooks' -import * as celJsLocal from '../index.js' -import {serialize} from '../serialize.js' +import * as celJsLocal from '../lib/index.js' +import {serialize} from '../lib/serialize.js' import * as celJsPackage from 'cel-js' // Benchmark configuration diff --git a/benchmark/memory.js b/benchmark/memory.js index cef4b46..e9269c0 100644 --- a/benchmark/memory.js +++ b/benchmark/memory.js @@ -9,8 +9,8 @@ */ import {performance} from 'perf_hooks' -import * as celJsLocal from '../index.js' -import {serialize} from '../serialize.js' +import * as celJsLocal from '../lib/index.js' +import {serialize} from '../lib/serialize.js' import * as celJsPackage from 'cel-js' // Test expressions of increasing complexity diff --git a/examples.js b/examples.js index 8c2c9f0..1be484f 100644 --- a/examples.js +++ b/examples.js @@ -2,7 +2,7 @@ /* eslint-disable no-console */ // Examples demonstrating My CEL JS usage -import {evaluate, parse} from './index.js' +import {evaluate, parse} from './lib/index.js' console.log('🚀 My CEL JS Examples\n') diff --git a/errors.js b/lib/errors.js similarity index 63% rename from errors.js rename to lib/errors.js index 63cb21a..0df69f3 100644 --- a/errors.js +++ b/lib/errors.js @@ -1,22 +1,44 @@ export const nodePositionCache = new WeakMap() export class ParseError extends Error { - constructor(message, node) { - super(message) + #wasConstructedWithAst = false + constructor(message, node, cause) { + super(message, {cause}) this.name = 'ParseError' + const pos = node && (node.input ? node : nodePositionCache.get(node)) + if (pos) { + this.#wasConstructedWithAst = true + this.message = formatErrorWithHighlight(this.message, pos) + } + } + + withAst(node) { + if (this.#wasConstructedWithAst) return this const pos = node && (node.input ? node : nodePositionCache.get(node)) if (pos) this.message = formatErrorWithHighlight(this.message, pos) + return this } } export class EvaluationError extends Error { - constructor(message, node) { - super(message) + #wasConstructedWithAst = false + constructor(message, node, cause) { + super(message, {cause}) this.name = 'EvaluationError' + const pos = node && (node.input ? node : nodePositionCache.get(node)) + if (pos) { + this.#wasConstructedWithAst = true + this.message = formatErrorWithHighlight(this.message, pos) + } + } + + withAst(node) { + if (this.#wasConstructedWithAst) return this const pos = node && (node.input ? node : nodePositionCache.get(node)) if (pos) this.message = formatErrorWithHighlight(this.message, pos) + return this } } diff --git a/evaluator.d.ts b/lib/evaluator.d.ts similarity index 100% rename from evaluator.d.ts rename to lib/evaluator.d.ts diff --git a/evaluator.js b/lib/evaluator.js similarity index 98% rename from evaluator.js rename to lib/evaluator.js index 1172672..797056c 100644 --- a/evaluator.js +++ b/lib/evaluator.js @@ -957,7 +957,12 @@ const handlers = new Map( } if (fn.macro) return fn.handler.call(s, receiver, ...ast[3]) - return fn.handler(receiver, ...ast[3].map((arg) => s.eval(arg))) + try { + return fn.handler(receiver, ...ast[3].map((arg) => s.eval(arg))) + } catch (err) { + if (err instanceof EvaluationError) throw err.withAst(ast) + throw new EvaluationError(err.message, ast, err) + } }, call(ast, s) { const functionName = ast[1] @@ -966,8 +971,13 @@ const handlers = new Map( throw new EvaluationError(`Function not found: '${functionName}'`, ast) } - if (fn.macro) return fn.handler.call(s, ...ast[2]) - return fn.handler(...ast[2].map((arg) => s.eval(arg))) + try { + if (fn.macro) return fn.handler.call(s, ...ast[2]) + return fn.handler(...ast[2].map((arg) => s.eval(arg))) + } catch (err) { + if (err instanceof EvaluationError) throw err.withAst(ast) + throw new EvaluationError(err.message, ast, err) + } }, array(ast, s) { const elements = ast[1] @@ -1012,13 +1022,7 @@ class Evaluator { // Allow numeric type cross-compatibility for equality/inequality operators // when at least one operand is dynamic (contains variable references) - if ( - (leftType === 'Double' || leftType === 'Integer') && - (rightType === 'Double' || rightType === 'Integer') && - (this.isDynamic(ast[1]) || this.isDynamic(ast[2])) - ) { - return - } + if (this.isDynamic(ast[1]) || this.isDynamic(ast[2])) return throw new EvaluationError(`no such overload: ${leftType} ${ast[0]} ${rightType}`, ast) } @@ -1076,9 +1080,11 @@ class Evaluator { switch (ast[0]) { case 'id': return true + case '.': + case '[]': + return this.isDynamic(ast[1]) case '+': case '-': - case '[]': return this.isDynamic(ast[1]) || this.isDynamic(ast[2]) case '?:': return this.isDynamic(ast[2]) || this.isDynamic(ast[3]) @@ -1283,7 +1289,7 @@ function isEqual(a, b) { const arr = a instanceof Set ? b : a const set = a instanceof Set ? a : b if (!Array.isArray(arr)) return false - if (arr.length !== set.size) return false + if (arr.length !== set?.size) return false for (let i = 0; i < arr.length; i++) if (!set.has(arr[i])) return false return true } diff --git a/functions.js b/lib/functions.js similarity index 100% rename from functions.js rename to lib/functions.js diff --git a/index.d.ts b/lib/index.d.ts similarity index 100% rename from index.d.ts rename to lib/index.d.ts diff --git a/index.js b/lib/index.js similarity index 100% rename from index.js rename to lib/index.js diff --git a/serialize.d.ts b/lib/serialize.d.ts similarity index 100% rename from serialize.d.ts rename to lib/serialize.d.ts diff --git a/serialize.js b/lib/serialize.js similarity index 100% rename from serialize.js rename to lib/serialize.js diff --git a/package.json b/package.json index 58eb02c..16e4f70 100644 --- a/package.json +++ b/package.json @@ -23,27 +23,22 @@ "type": "module", "exports": { ".": { - "types": "./index.d.ts", - "default": "./index.js" + "types": "./lib/index.d.ts", + "default": "./lib/index.js" }, "./evaluator": { - "types": "./evaluator.d.ts", - "default": "./evaluator.js" + "types": "./lib/evaluator.d.ts", + "default": "./lib/evaluator.js" }, "./serialize": { - "types": "./serialize.d.ts", - "default": "./serialize.js" + "types": "./lib/serialize.d.ts", + "default": "./lib/serialize.js" } }, - "main": "index.js", - "types": "index.d.ts", + "main": "lib/index.js", + "types": "lib/index.d.ts", "files": [ - "index.js", - "evaluator.js", - "serialize.js", - "errors.js", - "functions.js", - "*.d.ts", + "lib/*", "LICENSE", "README.md" ], diff --git a/test/addition.test.js b/test/addition.test.js index f49969e..1ed6c35 100644 --- a/test/addition.test.js +++ b/test/addition.test.js @@ -1,5 +1,5 @@ import {test, describe} from 'node:test' -import {evaluate} from '../index.js' +import {evaluate} from '../lib/index.js' describe('addition and subtraction', () => { test('should evaluate addition', (t) => { diff --git a/test/atomic-expression.test.js b/test/atomic-expression.test.js index ea029f6..0f4489c 100644 --- a/test/atomic-expression.test.js +++ b/test/atomic-expression.test.js @@ -1,5 +1,5 @@ import {test, describe} from 'node:test' -import {evaluate, parse} from '../index.js' +import {evaluate, parse} from '../lib/index.js' describe('atomic expressions', () => { test('should evaluate a number', (t) => { diff --git a/test/built-in-functions.test.js b/test/built-in-functions.test.js index 9be636f..198a256 100644 --- a/test/built-in-functions.test.js +++ b/test/built-in-functions.test.js @@ -1,5 +1,5 @@ import {test, describe} from 'node:test' -import {evaluate} from '../index.js' +import {evaluate} from '../lib/index.js' describe('built-in functions', () => { describe('size function', () => { diff --git a/test/comments.test.js b/test/comments.test.js index 3ecfe21..42a7012 100644 --- a/test/comments.test.js +++ b/test/comments.test.js @@ -1,5 +1,5 @@ import {test, describe} from 'node:test' -import {evaluate, parse} from '../index.js' +import {evaluate, parse} from '../lib/index.js' describe('comments', () => { test('should ignore single line comments at the end', (t) => { diff --git a/test/comparisons.test.js b/test/comparisons.test.js index 62872e7..f1862e0 100644 --- a/test/comparisons.test.js +++ b/test/comparisons.test.js @@ -1,5 +1,5 @@ import {test, describe} from 'node:test' -import {evaluate} from '../index.js' +import {evaluate} from '../lib/index.js' describe('comparison operators', () => { describe('equality', () => { @@ -26,6 +26,51 @@ describe('comparison operators', () => { test('should compare unequal booleans', (t) => { t.assert.strictEqual(evaluate('true == false'), false) }) + + test('does not support non-dynamic comparisons', (t) => { + const err = /no such overload:/ + t.assert.throws(() => evaluate('true == null'), err) + t.assert.throws(() => evaluate('false == null'), err) + t.assert.throws(() => evaluate('1.0 == null'), err) + t.assert.throws(() => evaluate('1 == null'), err) + t.assert.throws(() => evaluate('true == null'), err) + t.assert.throws(() => evaluate('true == 1'), err) + }) + + test('supports dynamic comparisons', (t) => { + const ctx = { + v: { + true: true, + false: false, + null: null, + int: 1n, + double: 1, + string: 'hello' + } + } + + for (const key of Object.keys(ctx.v)) { + t.assert.strictEqual(evaluate(`v["${key}"] == ""`, ctx), false) + t.assert.strictEqual(evaluate(`v["${key}"] == 999`, ctx), false) + t.assert.strictEqual(evaluate(`v["${key}"] == 999.1`, ctx), false) + t.assert.strictEqual(evaluate(`v["${key}"] == []`, ctx), false) + t.assert.strictEqual(evaluate(`v["${key}"] == {}`, ctx), false) + t.assert.strictEqual(evaluate(`"" == v["${key}"]`, ctx), false) + t.assert.strictEqual(evaluate(`999 == v["${key}"]`, ctx), false) + t.assert.strictEqual(evaluate(`999.1 == v["${key}"]`, ctx), false) + t.assert.strictEqual(evaluate(`[] == v["${key}"]`, ctx), false) + t.assert.strictEqual(evaluate(`{} == v["${key}"]`, ctx), false) + } + + t.assert.strictEqual(evaluate('v["null"] == null', ctx), true) + t.assert.strictEqual(evaluate('v["true"] == true', ctx), true) + t.assert.strictEqual(evaluate('v["true"] == true', ctx), true) + t.assert.strictEqual(evaluate('v["false"] == false', ctx), true) + t.assert.strictEqual(evaluate('v["int"] == 1', ctx), true) + t.assert.strictEqual(evaluate('v["double"] == 1.0', ctx), true) + t.assert.strictEqual(evaluate('v["double"] == 1', ctx), true) + t.assert.strictEqual(evaluate('v["null"] == null', ctx), true) + }) }) describe('inequality', () => { @@ -36,6 +81,51 @@ describe('comparison operators', () => { test('should compare equal numbers', (t) => { t.assert.strictEqual(evaluate('1 != 1'), false) }) + + test('does not support non-dynamic comparisons', (t) => { + const err = /no such overload:/ + t.assert.throws(() => evaluate('true != null'), err) + t.assert.throws(() => evaluate('false != null'), err) + t.assert.throws(() => evaluate('1.0 != null'), err) + t.assert.throws(() => evaluate('1 != null'), err) + t.assert.throws(() => evaluate('true != null'), err) + t.assert.throws(() => evaluate('true != 1'), err) + }) + + test('supports dynamic comparisons', (t) => { + const ctx = { + v: { + true: true, + false: false, + null: null, + int: 1n, + double: 1, + string: 'hello' + } + } + + for (const key of Object.keys(ctx.v)) { + t.assert.strictEqual(evaluate(`v["${key}"] != ""`, ctx), true) + t.assert.strictEqual(evaluate(`v["${key}"] != 999`, ctx), true) + t.assert.strictEqual(evaluate(`v["${key}"] != 999.1`, ctx), true) + t.assert.strictEqual(evaluate(`v["${key}"] != []`, ctx), true) + t.assert.strictEqual(evaluate(`v["${key}"] != {}`, ctx), true) + t.assert.strictEqual(evaluate(`"" != v["${key}"]`, ctx), true) + t.assert.strictEqual(evaluate(`999 != v["${key}"]`, ctx), true) + t.assert.strictEqual(evaluate(`999.1 != v["${key}"]`, ctx), true) + t.assert.strictEqual(evaluate(`[] != v["${key}"]`, ctx), true) + t.assert.strictEqual(evaluate(`{} != v["${key}"]`, ctx), true) + } + + t.assert.strictEqual(evaluate('v["null"] != null', ctx), false) + t.assert.strictEqual(evaluate('v["true"] != true', ctx), false) + t.assert.strictEqual(evaluate('v["true"] != true', ctx), false) + t.assert.strictEqual(evaluate('v["false"] != false', ctx), false) + t.assert.strictEqual(evaluate('v["int"] != 1', ctx), false) + t.assert.strictEqual(evaluate('v["double"] != 1.0', ctx), false) + t.assert.strictEqual(evaluate('v["double"] != 1', ctx), false) + t.assert.strictEqual(evaluate('v["null"] != null', ctx), false) + }) }) describe('less than', () => { diff --git a/test/conditional-ternary.test.js b/test/conditional-ternary.test.js index 8d411f8..6511f38 100644 --- a/test/conditional-ternary.test.js +++ b/test/conditional-ternary.test.js @@ -1,5 +1,5 @@ import {test, describe} from 'node:test' -import {evaluate} from '../index.js' +import {evaluate} from '../lib/index.js' describe('conditional ternary operator', () => { test('should handle simple ternary expressions', (t) => { diff --git a/test/custom-functions.test.js b/test/custom-functions.test.js index c9e2a88..e75b01e 100644 --- a/test/custom-functions.test.js +++ b/test/custom-functions.test.js @@ -1,5 +1,5 @@ import {test, describe} from 'node:test' -import {evaluate} from '../index.js' +import {evaluate} from '../lib/index.js' describe('custom functions', () => { describe('single argument functions', () => { diff --git a/test/identifiers.test.js b/test/identifiers.test.js index b67ad3b..4bae6a3 100644 --- a/test/identifiers.test.js +++ b/test/identifiers.test.js @@ -1,5 +1,5 @@ import {test, describe} from 'node:test' -import {evaluate} from '../index.js' +import {evaluate} from '../lib/index.js' describe('identifiers', () => { describe('dot notation', () => { diff --git a/test/in-operator.test.js b/test/in-operator.test.js index b5c46ba..0f3fe92 100644 --- a/test/in-operator.test.js +++ b/test/in-operator.test.js @@ -1,5 +1,5 @@ import {test, describe} from 'node:test' -import {evaluate} from '../index.js' +import {evaluate} from '../lib/index.js' describe('in operator and membership tests', () => { describe('arrays', () => { diff --git a/test/index.test.js b/test/index.test.js index 526ee6f..3593acd 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,5 +1,5 @@ import {test, describe} from 'node:test' -import {parse, evaluate, ParseError, EvaluationError} from '../index.js' +import {parse, evaluate, ParseError, EvaluationError} from '../lib/index.js' describe('CEL Implementation Integration Tests', () => { test('should export all required functions and classes', (t) => { diff --git a/test/integers.test.js b/test/integers.test.js index 2f2b55c..8494ff6 100644 --- a/test/integers.test.js +++ b/test/integers.test.js @@ -1,5 +1,5 @@ import {test, describe} from 'node:test' -import {evaluate} from '../index.js' +import {evaluate} from '../lib/index.js' describe('integer literals', () => { test('should parse decimal integers', (t) => { diff --git a/test/lists.test.js b/test/lists.test.js index ca30c37..ebaab3a 100644 --- a/test/lists.test.js +++ b/test/lists.test.js @@ -1,5 +1,5 @@ import {test, describe} from 'node:test' -import {evaluate} from '../index.js' +import {evaluate} from '../lib/index.js' describe('lists expressions', () => { describe('literals', () => { diff --git a/test/logical-operators.test.js b/test/logical-operators.test.js index f5b595b..b7f445e 100644 --- a/test/logical-operators.test.js +++ b/test/logical-operators.test.js @@ -1,5 +1,5 @@ import {describe, test} from 'node:test' -import {evaluate} from '../index.js' +import {evaluate} from '../lib/index.js' // simulate an evaluation error const divByZero = '(1 / 0)' diff --git a/test/macros.test.js b/test/macros.test.js index c95d36e..8000cde 100644 --- a/test/macros.test.js +++ b/test/macros.test.js @@ -1,5 +1,5 @@ import {test, describe} from 'node:test' -import {evaluate} from '../index.js' +import {evaluate} from '../lib/index.js' describe('macros', () => { describe('has macro', () => { diff --git a/test/maps.test.js b/test/maps.test.js index e0fc9c7..c2c169e 100644 --- a/test/maps.test.js +++ b/test/maps.test.js @@ -1,5 +1,5 @@ import {test, describe} from 'node:test' -import {evaluate} from '../index.js' +import {evaluate} from '../lib/index.js' describe('maps/objects expressions', () => { describe('literals', () => { diff --git a/test/miscellaneous.test.js b/test/miscellaneous.test.js index ce267c8..93dcba0 100644 --- a/test/miscellaneous.test.js +++ b/test/miscellaneous.test.js @@ -1,5 +1,5 @@ import {test, describe} from 'node:test' -import {evaluate} from '../index.js' +import {evaluate} from '../lib/index.js' describe('miscellaneous', () => { test('order of arithmetic operations', (t) => { diff --git a/test/multiplication.test.js b/test/multiplication.test.js index 43b6fd0..41ae842 100644 --- a/test/multiplication.test.js +++ b/test/multiplication.test.js @@ -1,5 +1,5 @@ import {test, describe} from 'node:test' -import {evaluate} from '../index.js' +import {evaluate} from '../lib/index.js' describe('multiplication and division', () => { test('should multiply two numbers', (t) => { diff --git a/test/precedence.test.js b/test/precedence.test.js index 27e603b..227eaa4 100644 --- a/test/precedence.test.js +++ b/test/precedence.test.js @@ -1,5 +1,5 @@ import {test, describe} from 'node:test' -import {evaluate, parse} from '../index.js' +import {evaluate, parse} from '../lib/index.js' describe('operator precedence', () => { test('ternary should have lower precedence than logical AND', (t) => { diff --git a/test/serialize.test.js b/test/serialize.test.js index 1d1cc94..6c6546d 100644 --- a/test/serialize.test.js +++ b/test/serialize.test.js @@ -1,7 +1,7 @@ import {test, describe} from 'node:test' import assert from 'node:assert' -import {parse} from '../index.js' -import {serialize} from '../serialize.js' +import {parse} from '../lib/index.js' +import {serialize} from '../lib/serialize.js' function equalResult(expr) { assert.strictEqual(serialize(parse(expr).ast), expr) diff --git a/test/string-literals.test.js b/test/string-literals.test.js index e3575ce..89dff01 100644 --- a/test/string-literals.test.js +++ b/test/string-literals.test.js @@ -1,5 +1,5 @@ import {test, describe} from 'node:test' -import {evaluate, parse} from '../index.js' +import {evaluate, parse} from '../lib/index.js' describe('string literals and escapes', () => { describe('basic string literals', () => { diff --git a/test/unary-operators.test.js b/test/unary-operators.test.js index f31f534..40f1995 100644 --- a/test/unary-operators.test.js +++ b/test/unary-operators.test.js @@ -1,5 +1,5 @@ import {test, describe} from 'node:test' -import {evaluate} from '../index.js' +import {evaluate} from '../lib/index.js' function strictEqualTest(expr, expected) { test(expr, (t) => {