zero-query 1.0.9 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +2 -0
  3. package/cli/args.js +33 -33
  4. package/cli/commands/build-api.js +443 -0
  5. package/cli/commands/build.js +254 -216
  6. package/cli/commands/bundle.js +1228 -1183
  7. package/cli/commands/create.js +137 -121
  8. package/cli/commands/dev/devtools/index.js +56 -56
  9. package/cli/commands/dev/devtools/js/components.js +49 -49
  10. package/cli/commands/dev/devtools/js/core.js +423 -423
  11. package/cli/commands/dev/devtools/js/elements.js +421 -421
  12. package/cli/commands/dev/devtools/js/network.js +166 -166
  13. package/cli/commands/dev/devtools/js/performance.js +73 -73
  14. package/cli/commands/dev/devtools/js/router.js +105 -105
  15. package/cli/commands/dev/devtools/js/source.js +132 -132
  16. package/cli/commands/dev/devtools/js/stats.js +35 -35
  17. package/cli/commands/dev/devtools/js/tabs.js +79 -79
  18. package/cli/commands/dev/devtools/panel.html +95 -95
  19. package/cli/commands/dev/devtools/styles.css +244 -244
  20. package/cli/commands/dev/index.js +107 -107
  21. package/cli/commands/dev/logger.js +75 -75
  22. package/cli/commands/dev/overlay.js +858 -858
  23. package/cli/commands/dev/server.js +220 -167
  24. package/cli/commands/dev/validator.js +94 -94
  25. package/cli/commands/dev/watcher.js +172 -172
  26. package/cli/help.js +114 -112
  27. package/cli/index.js +52 -52
  28. package/cli/scaffold/default/LICENSE +21 -21
  29. package/cli/scaffold/default/app/app.js +207 -207
  30. package/cli/scaffold/default/app/components/about.js +201 -201
  31. package/cli/scaffold/default/app/components/api-demo.js +143 -143
  32. package/cli/scaffold/default/app/components/contact-card.js +231 -231
  33. package/cli/scaffold/default/app/components/contacts/contacts.css +706 -706
  34. package/cli/scaffold/default/app/components/contacts/contacts.html +200 -200
  35. package/cli/scaffold/default/app/components/contacts/contacts.js +196 -196
  36. package/cli/scaffold/default/app/components/counter.js +127 -127
  37. package/cli/scaffold/default/app/components/home.js +249 -249
  38. package/cli/scaffold/default/app/components/not-found.js +16 -16
  39. package/cli/scaffold/default/app/components/playground/playground.css +115 -115
  40. package/cli/scaffold/default/app/components/playground/playground.html +161 -161
  41. package/cli/scaffold/default/app/components/playground/playground.js +116 -116
  42. package/cli/scaffold/default/app/components/todos.js +225 -225
  43. package/cli/scaffold/default/app/components/toolkit/toolkit.css +97 -97
  44. package/cli/scaffold/default/app/components/toolkit/toolkit.html +146 -146
  45. package/cli/scaffold/default/app/components/toolkit/toolkit.js +280 -280
  46. package/cli/scaffold/default/app/routes.js +15 -15
  47. package/cli/scaffold/default/app/store.js +101 -101
  48. package/cli/scaffold/default/global.css +552 -552
  49. package/cli/scaffold/default/index.html +99 -99
  50. package/cli/scaffold/minimal/app/app.js +85 -85
  51. package/cli/scaffold/minimal/app/components/about.js +68 -68
  52. package/cli/scaffold/minimal/app/components/counter.js +122 -122
  53. package/cli/scaffold/minimal/app/components/home.js +68 -68
  54. package/cli/scaffold/minimal/app/components/not-found.js +16 -16
  55. package/cli/scaffold/minimal/app/routes.js +9 -9
  56. package/cli/scaffold/minimal/app/store.js +36 -36
  57. package/cli/scaffold/minimal/global.css +300 -300
  58. package/cli/scaffold/minimal/index.html +44 -44
  59. package/cli/scaffold/ssr/app/app.js +41 -41
  60. package/cli/scaffold/ssr/app/components/about.js +55 -55
  61. package/cli/scaffold/ssr/app/components/blog/index.js +65 -65
  62. package/cli/scaffold/ssr/app/components/blog/post.js +86 -86
  63. package/cli/scaffold/ssr/app/components/home.js +37 -37
  64. package/cli/scaffold/ssr/app/components/not-found.js +15 -15
  65. package/cli/scaffold/ssr/app/routes.js +8 -8
  66. package/cli/scaffold/ssr/global.css +228 -228
  67. package/cli/scaffold/ssr/index.html +37 -37
  68. package/cli/scaffold/ssr/package.json +8 -8
  69. package/cli/scaffold/ssr/server/data/posts.js +144 -144
  70. package/cli/scaffold/ssr/server/index.js +213 -213
  71. package/cli/scaffold/webrtc/app/app.js +11 -0
  72. package/cli/scaffold/webrtc/app/components/video-room.js +295 -0
  73. package/cli/scaffold/webrtc/app/lib/room.js +252 -0
  74. package/cli/scaffold/webrtc/assets/.gitkeep +0 -0
  75. package/cli/scaffold/webrtc/global.css +250 -0
  76. package/cli/scaffold/webrtc/index.html +21 -0
  77. package/cli/utils.js +305 -287
  78. package/dist/API.md +7264 -0
  79. package/dist/zquery.dist.zip +0 -0
  80. package/dist/zquery.js +10313 -6252
  81. package/dist/zquery.min.js +8 -601
  82. package/index.d.ts +570 -365
  83. package/index.js +311 -232
  84. package/package.json +76 -69
  85. package/src/component.js +1709 -1454
  86. package/src/core.js +921 -921
  87. package/src/diff.js +497 -497
  88. package/src/errors.js +209 -209
  89. package/src/expression.js +922 -922
  90. package/src/http.js +242 -242
  91. package/src/package.json +1 -1
  92. package/src/reactive.js +255 -254
  93. package/src/router.js +843 -773
  94. package/src/ssr.js +418 -418
  95. package/src/store.js +318 -272
  96. package/src/utils.js +515 -515
  97. package/src/webrtc/e2ee.js +351 -0
  98. package/src/webrtc/errors.js +116 -0
  99. package/src/webrtc/ice.js +301 -0
  100. package/src/webrtc/index.js +131 -0
  101. package/src/webrtc/joinToken.js +119 -0
  102. package/src/webrtc/observe.js +172 -0
  103. package/src/webrtc/peer.js +351 -0
  104. package/src/webrtc/reactive.js +268 -0
  105. package/src/webrtc/room.js +625 -0
  106. package/src/webrtc/sdp.js +302 -0
  107. package/src/webrtc/sfu/index.js +43 -0
  108. package/src/webrtc/sfu/livekit.js +131 -0
  109. package/src/webrtc/sfu/mediasoup.js +150 -0
  110. package/src/webrtc/signaling.js +373 -0
  111. package/src/webrtc/turn.js +237 -0
  112. package/tests/_helpers/webrtcFakes.js +289 -0
  113. package/tests/audit.test.js +4158 -4158
  114. package/tests/cli.test.js +1136 -1023
  115. package/tests/compare.test.js +497 -0
  116. package/tests/component.test.js +3969 -3938
  117. package/tests/core.test.js +1910 -1910
  118. package/tests/dev-server.test.js +489 -0
  119. package/tests/diff.test.js +1416 -1416
  120. package/tests/docs.test.js +1664 -0
  121. package/tests/electron-features.test.js +864 -0
  122. package/tests/errors.test.js +619 -619
  123. package/tests/expression.test.js +1056 -1056
  124. package/tests/http.test.js +648 -648
  125. package/tests/reactive.test.js +819 -819
  126. package/tests/router.test.js +2327 -2327
  127. package/tests/ssr.test.js +870 -870
  128. package/tests/store.test.js +830 -830
  129. package/tests/test-minifier.js +153 -153
  130. package/tests/test-ssr.js +27 -27
  131. package/tests/utils.test.js +1377 -1377
  132. package/tests/webrtc/e2ee.test.js +283 -0
  133. package/tests/webrtc/ice.test.js +202 -0
  134. package/tests/webrtc/joinToken.test.js +89 -0
  135. package/tests/webrtc/observe.test.js +111 -0
  136. package/tests/webrtc/peer.test.js +373 -0
  137. package/tests/webrtc/reactive.test.js +235 -0
  138. package/tests/webrtc/room.test.js +406 -0
  139. package/tests/webrtc/sdp.test.js +151 -0
  140. package/tests/webrtc/sfu-livekit.test.js +119 -0
  141. package/tests/webrtc/sfu.test.js +160 -0
  142. package/tests/webrtc/signaling.test.js +251 -0
  143. package/tests/webrtc/turn.test.js +256 -0
  144. package/types/collection.d.ts +383 -383
  145. package/types/component.d.ts +186 -186
  146. package/types/errors.d.ts +135 -135
  147. package/types/http.d.ts +92 -92
  148. package/types/misc.d.ts +201 -201
  149. package/types/reactive.d.ts +98 -98
  150. package/types/router.d.ts +190 -190
  151. package/types/ssr.d.ts +102 -102
  152. package/types/store.d.ts +146 -145
  153. package/types/utils.d.ts +245 -245
  154. package/types/webrtc.d.ts +653 -0
@@ -1,1056 +1,1056 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { safeEval } from '../src/expression.js';
3
-
4
-
5
- // ---------------------------------------------------------------------------
6
- // Helpers
7
- // ---------------------------------------------------------------------------
8
- const eval_ = (expr, ...scopes) => safeEval(expr, scopes.length ? scopes : [{}]);
9
-
10
-
11
- // ---------------------------------------------------------------------------
12
- // Literals
13
- // ---------------------------------------------------------------------------
14
-
15
- describe('expression parser - literals', () => {
16
- it('numbers', () => {
17
- expect(eval_('42')).toBe(42);
18
- expect(eval_('3.14')).toBe(3.14);
19
- expect(eval_('0xFF')).toBe(255);
20
- expect(eval_('1e3')).toBe(1000);
21
- });
22
-
23
- it('strings', () => {
24
- expect(eval_("'hello'")).toBe('hello');
25
- expect(eval_('"world"')).toBe('world');
26
- expect(eval_("'it\\'s'")).toBe("it's");
27
- });
28
-
29
- it('booleans and null/undefined', () => {
30
- expect(eval_('true')).toBe(true);
31
- expect(eval_('false')).toBe(false);
32
- expect(eval_('null')).toBe(null);
33
- expect(eval_('undefined')).toBe(undefined);
34
- });
35
-
36
- it('empty expression returns undefined', () => {
37
- expect(eval_('')).toBe(undefined);
38
- expect(eval_(' ')).toBe(undefined);
39
- });
40
- });
41
-
42
-
43
- // ---------------------------------------------------------------------------
44
- // Arithmetic
45
- // ---------------------------------------------------------------------------
46
-
47
- describe('expression parser - arithmetic', () => {
48
- it('basic operations', () => {
49
- expect(eval_('2 + 3')).toBe(5);
50
- expect(eval_('10 - 4')).toBe(6);
51
- expect(eval_('3 * 7')).toBe(21);
52
- expect(eval_('15 / 3')).toBe(5);
53
- expect(eval_('10 % 3')).toBe(1);
54
- });
55
-
56
- it('operator precedence', () => {
57
- expect(eval_('2 + 3 * 4')).toBe(14);
58
- expect(eval_('(2 + 3) * 4')).toBe(20);
59
- });
60
-
61
- it('unary operators', () => {
62
- expect(eval_('-5')).toBe(-5);
63
- expect(eval_('+3')).toBe(3);
64
- });
65
- });
66
-
67
-
68
- // ---------------------------------------------------------------------------
69
- // Comparison & logical
70
- // ---------------------------------------------------------------------------
71
-
72
- describe('expression parser - comparison', () => {
73
- it('equality', () => {
74
- expect(eval_('1 === 1')).toBe(true);
75
- expect(eval_('1 !== 2')).toBe(true);
76
- expect(eval_("1 == '1'")).toBe(true);
77
- expect(eval_("1 != '2'")).toBe(true);
78
- });
79
-
80
- it('relational', () => {
81
- expect(eval_('3 > 2')).toBe(true);
82
- expect(eval_('3 < 2')).toBe(false);
83
- expect(eval_('3 >= 3')).toBe(true);
84
- expect(eval_('3 <= 2')).toBe(false);
85
- });
86
- });
87
-
88
-
89
- describe('expression parser - logical', () => {
90
- it('&& and ||', () => {
91
- expect(eval_('true && false')).toBe(false);
92
- expect(eval_('true || false')).toBe(true);
93
- expect(eval_('0 || 42')).toBe(42);
94
- expect(eval_("'' || 'default'")).toBe('default');
95
- });
96
-
97
- it('!', () => {
98
- expect(eval_('!true')).toBe(false);
99
- expect(eval_('!false')).toBe(true);
100
- expect(eval_('!0')).toBe(true);
101
- });
102
-
103
- it('nullish coalescing ??', () => {
104
- expect(eval_('null ?? 10')).toBe(10);
105
- expect(eval_('undefined ?? 20')).toBe(20);
106
- expect(eval_('0 ?? 30')).toBe(0);
107
- expect(eval_("'' ?? 'fallback'")).toBe('');
108
- });
109
- });
110
-
111
-
112
- // ---------------------------------------------------------------------------
113
- // Ternary
114
- // ---------------------------------------------------------------------------
115
-
116
- describe('expression parser - ternary', () => {
117
- it('evaluates truthy branch', () => {
118
- expect(eval_("true ? 'yes' : 'no'")).toBe('yes');
119
- });
120
-
121
- it('evaluates falsy branch', () => {
122
- expect(eval_("false ? 'yes' : 'no'")).toBe('no');
123
- });
124
-
125
- it('works with expressions', () => {
126
- expect(eval_('5 > 3 ? 10 : 20')).toBe(10);
127
- });
128
- });
129
-
130
-
131
- // ---------------------------------------------------------------------------
132
- // Property access & scope
133
- // ---------------------------------------------------------------------------
134
-
135
- describe('expression parser - property access', () => {
136
- it('reads scope variables', () => {
137
- expect(eval_('x', { x: 42 })).toBe(42);
138
- expect(eval_('name', { name: 'Tony' })).toBe('Tony');
139
- });
140
-
141
- it('dot access', () => {
142
- expect(eval_('user.name', { user: { name: 'Tony' } })).toBe('Tony');
143
- });
144
-
145
- it('computed access', () => {
146
- expect(eval_('items[0]', { items: ['a', 'b', 'c'] })).toBe('a');
147
- expect(eval_('obj[key]', { obj: { x: 1 }, key: 'x' })).toBe(1);
148
- });
149
-
150
- it('optional chaining ?.', () => {
151
- expect(eval_('user?.name', { user: null })).toBe(undefined);
152
- expect(eval_('user?.name', { user: { name: 'Tony' } })).toBe('Tony');
153
- });
154
-
155
- it('returns undefined for missing scope keys', () => {
156
- expect(eval_('missing')).toBe(undefined);
157
- expect(eval_('a.b.c', { a: {} })).toBe(undefined);
158
- });
159
- });
160
-
161
-
162
- // ---------------------------------------------------------------------------
163
- // Method calls
164
- // ---------------------------------------------------------------------------
165
-
166
- describe('expression parser - method calls', () => {
167
- it('string methods', () => {
168
- expect(eval_("'hello'.toUpperCase()")).toBe('HELLO');
169
- expect(eval_("'hello world'.split(' ')")).toEqual(['hello', 'world']);
170
- expect(eval_("'abc'.includes('b')")).toBe(true);
171
- });
172
-
173
- it('array methods', () => {
174
- expect(eval_('items.length', { items: [1, 2, 3] })).toBe(3);
175
- expect(eval_('items.includes(2)', { items: [1, 2, 3] })).toBe(true);
176
- expect(eval_("items.join(',')", { items: [1, 2, 3] })).toBe('1,2,3');
177
- });
178
-
179
- it('custom function calls', () => {
180
- const add = (a, b) => a + b;
181
- expect(eval_('add(1, 2)', { add })).toBe(3);
182
- });
183
- });
184
-
185
-
186
- // ---------------------------------------------------------------------------
187
- // Built-in globals
188
- // ---------------------------------------------------------------------------
189
-
190
- describe('expression parser - built-in globals', () => {
191
- it('Math', () => {
192
- expect(eval_('Math.PI')).toBeCloseTo(3.14159);
193
- expect(eval_('Math.max(1, 5, 3)')).toBe(5);
194
- expect(eval_('Math.abs(-7)')).toBe(7);
195
- });
196
-
197
- it('JSON', () => {
198
- expect(eval_("JSON.parse('{\"a\":1}')")).toEqual({ a: 1 });
199
- });
200
-
201
- it('parseInt/parseFloat', () => {
202
- expect(eval_("parseInt('42')")).toBe(42);
203
- expect(eval_("parseFloat('3.14')")).toBeCloseTo(3.14);
204
- });
205
-
206
- it('isNaN', () => {
207
- expect(eval_('isNaN(NaN)')).toBe(true);
208
- expect(eval_('isNaN(5)')).toBe(false);
209
- });
210
- });
211
-
212
-
213
- // ---------------------------------------------------------------------------
214
- // Template literals
215
- // ---------------------------------------------------------------------------
216
-
217
- describe('expression parser - template literals', () => {
218
- it('simple interpolation', () => {
219
- expect(eval_('`Hello ${name}`', { name: 'Tony' })).toBe('Hello Tony');
220
- });
221
-
222
- it('expression inside interpolation', () => {
223
- expect(eval_('`${a + b}`', { a: 1, b: 2 })).toBe('3');
224
- });
225
-
226
- it('no interpolation', () => {
227
- expect(eval_('`plain text`')).toBe('plain text');
228
- });
229
- });
230
-
231
-
232
- // ---------------------------------------------------------------------------
233
- // Array & object literals
234
- // ---------------------------------------------------------------------------
235
-
236
- describe('expression parser - array/object literals', () => {
237
- it('array literal', () => {
238
- expect(eval_('[1, 2, 3]')).toEqual([1, 2, 3]);
239
- expect(eval_('[]')).toEqual([]);
240
- });
241
-
242
- it('object literal', () => {
243
- expect(eval_("{ x: 1, y: 'two' }")).toEqual({ x: 1, y: 'two' });
244
- });
245
-
246
- it('shorthand property', () => {
247
- expect(eval_('{ x }', { x: 42 })).toEqual({ x: 42 });
248
- });
249
- });
250
-
251
-
252
- // ---------------------------------------------------------------------------
253
- // Arrow functions
254
- // ---------------------------------------------------------------------------
255
-
256
- describe('expression parser - arrow functions', () => {
257
- it('single-param arrow', () => {
258
- const fn = eval_('x => x * 2');
259
- expect(fn(3)).toBe(6);
260
- });
261
-
262
- it('multi-param arrow', () => {
263
- const fn = eval_('(a, b) => a + b');
264
- expect(fn(1, 2)).toBe(3);
265
- });
266
-
267
- it('no-param arrow', () => {
268
- const fn = eval_('() => 42');
269
- expect(fn()).toBe(42);
270
- });
271
-
272
- it('arrow with scope access', () => {
273
- const items = [1, 2, 3];
274
- expect(eval_('items.filter(x => x > 1)', { items })).toEqual([2, 3]);
275
- });
276
- });
277
-
278
-
279
- // ---------------------------------------------------------------------------
280
- // typeof
281
- // ---------------------------------------------------------------------------
282
-
283
- describe('expression parser - typeof', () => {
284
- it('typeof string', () => {
285
- expect(eval_("typeof 'hello'")).toBe('string');
286
- });
287
-
288
- it('typeof number', () => {
289
- expect(eval_('typeof 42')).toBe('number');
290
- });
291
-
292
- it('typeof undefined variable', () => {
293
- expect(eval_('typeof missing')).toBe('undefined');
294
- });
295
- });
296
-
297
-
298
- // ---------------------------------------------------------------------------
299
- // Safety / security
300
- // ---------------------------------------------------------------------------
301
-
302
- describe('expression parser - safety', () => {
303
- it('blocks constructor access', () => {
304
- expect(eval_("''.constructor")).toBe(undefined);
305
- });
306
-
307
- it('blocks __proto__ access', () => {
308
- expect(eval_("({}).__proto__", {})).toBe(undefined);
309
- });
310
-
311
- it('returns undefined for invalid expressions', () => {
312
- expect(eval_('!!!!')).toBe(undefined);
313
- });
314
-
315
- it('handles deeply nested safe access', () => {
316
- const data = { a: { b: { c: { d: 'deep' } } } };
317
- expect(eval_('a.b.c.d', data)).toBe('deep');
318
- });
319
- });
320
-
321
-
322
- // ---------------------------------------------------------------------------
323
- // Multi-scope resolution
324
- // ---------------------------------------------------------------------------
325
-
326
- describe('expression parser - multi-scope', () => {
327
- it('checks scope layers in order', () => {
328
- expect(safeEval('x', [{ x: 'first' }, { x: 'second' }])).toBe('first');
329
- });
330
-
331
- it('falls through to second scope', () => {
332
- expect(safeEval('y', [{ x: 1 }, { y: 2 }])).toBe(2);
333
- });
334
- });
335
-
336
-
337
- // ---------------------------------------------------------------------------
338
- // in operator
339
- // ---------------------------------------------------------------------------
340
-
341
- describe('expression parser - in operator', () => {
342
- it('checks property existence', () => {
343
- expect(eval_("'x' in obj", { obj: { x: 1 } })).toBe(true);
344
- expect(eval_("'y' in obj", { obj: { x: 1 } })).toBe(false);
345
- });
346
- });
347
-
348
-
349
- // ---------------------------------------------------------------------------
350
- // instanceof operator
351
- // ---------------------------------------------------------------------------
352
-
353
- describe('expression parser - instanceof', () => {
354
- it('checks instanceOf', () => {
355
- expect(eval_('arr instanceof Array', { arr: [1, 2] })).toBe(true);
356
- expect(eval_('obj instanceof Array', { obj: {} })).toBe(false);
357
- });
358
- });
359
-
360
-
361
- // ---------------------------------------------------------------------------
362
- // Nested ternary
363
- // ---------------------------------------------------------------------------
364
-
365
- describe('expression parser - nested ternary', () => {
366
- it('evaluates simple ternary correctly', () => {
367
- expect(eval_("x > 10 ? 'big' : 'small'", { x: 12 })).toBe('big');
368
- expect(eval_("x > 10 ? 'big' : 'small'", { x: 2 })).toBe('small');
369
- });
370
- });
371
-
372
-
373
- // ---------------------------------------------------------------------------
374
- // Chained method calls
375
- // ---------------------------------------------------------------------------
376
-
377
- describe('expression parser - chained calls', () => {
378
- it('chains array methods', () => {
379
- expect(eval_('items.filter(x => x > 1).map(x => x * 2)', { items: [1, 2, 3] })).toEqual([4, 6]);
380
- });
381
-
382
- it('chains string methods', () => {
383
- expect(eval_("name.trim().toUpperCase()", { name: ' hello ' })).toBe('HELLO');
384
- });
385
- });
386
-
387
-
388
- // ---------------------------------------------------------------------------
389
- // Spread operator
390
- // ---------------------------------------------------------------------------
391
-
392
- describe('expression parser - spread / rest', () => {
393
- it('spread is not supported - returns gracefully', () => {
394
- // The parser does not support spread syntax; verify it doesn't throw
395
- const result = eval_('[...items, 4]', { items: [1, 2, 3] });
396
- expect(result).toBeDefined();
397
- });
398
- });
399
-
400
-
401
- // ---------------------------------------------------------------------------
402
- // Destructuring assignment in arrow body
403
- // ---------------------------------------------------------------------------
404
-
405
- describe('expression parser - complex arrow', () => {
406
- it('arrow as callback in array method', () => {
407
- const items = [{ n: 'a' }, { n: 'b' }];
408
- expect(eval_('items.map(x => x.n)', { items })).toEqual(['a', 'b']);
409
- });
410
-
411
- it('arrow with ternary body', () => {
412
- const fn = eval_('x => x > 0 ? "pos" : "neg"');
413
- expect(fn(1)).toBe('pos');
414
- expect(fn(-1)).toBe('neg');
415
- });
416
- });
417
-
418
-
419
- // ---------------------------------------------------------------------------
420
- // Bitwise operators
421
- // ---------------------------------------------------------------------------
422
-
423
- describe('expression parser - bitwise', () => {
424
- it('bitwise operators are not supported - does not throw', () => {
425
- // The expression parser does not implement bitwise operators
426
- // Verify graceful fallback rather than crashes
427
- expect(() => eval_('5 | 3')).not.toThrow();
428
- expect(() => eval_('5 & 3')).not.toThrow();
429
- expect(() => eval_('5 ^ 3')).not.toThrow();
430
- });
431
- });
432
-
433
-
434
- // ---------------------------------------------------------------------------
435
- // Comma expressions
436
- // ---------------------------------------------------------------------------
437
-
438
- describe('expression parser - comma', () => {
439
- it('comma expressions are not supported - does not throw', () => {
440
- // The parser does not support comma expressions
441
- expect(() => eval_('(1, 2, 3)')).not.toThrow();
442
- });
443
- });
444
-
445
-
446
- // ---------------------------------------------------------------------------
447
- // Edge cases
448
- // ---------------------------------------------------------------------------
449
-
450
- describe('expression parser - edge cases', () => {
451
- it('handles very long dot chains', () => {
452
- const data = { a: { b: { c: { d: { e: 42 } } } } };
453
- expect(eval_('a.b.c.d.e', data)).toBe(42);
454
- });
455
-
456
- it('handles numeric string keys in bracket access', () => {
457
- expect(eval_("items['0']", { items: ['a', 'b'] })).toBe('a');
458
- });
459
-
460
- it('handles conditional access chains', () => {
461
- expect(eval_('a?.b?.c', { a: null })).toBe(undefined);
462
- expect(eval_('a?.b?.c', { a: { b: { c: 1 } } })).toBe(1);
463
- });
464
-
465
- it('handles string with special characters', () => {
466
- expect(eval_("'hello\\nworld'")).toContain('hello');
467
- });
468
-
469
- it('handles negative numbers in expressions', () => {
470
- expect(eval_('-1 + -2')).toBe(-3);
471
- });
472
-
473
- it('exponentiation ** is not supported - does not throw', () => {
474
- // The parser does not implement ** operator
475
- expect(() => eval_('2 ** 3')).not.toThrow();
476
- });
477
-
478
- it('handles empty array/object', () => {
479
- expect(eval_('[]')).toEqual([]);
480
- expect(eval_('{}')).toEqual({});
481
- });
482
- });
483
-
484
-
485
- // ---------------------------------------------------------------------------
486
- // Optional chaining edge cases
487
- // ---------------------------------------------------------------------------
488
-
489
- describe('expression parser - optional chaining edge cases', () => {
490
- it('returns undefined for null base with ?.', () => {
491
- expect(eval_('a?.b', { a: null })).toBeUndefined();
492
- });
493
-
494
- it('returns undefined for undefined base with ?.', () => {
495
- expect(eval_('a?.b', { a: undefined })).toBeUndefined();
496
- });
497
-
498
- it('chains multiple optional access', () => {
499
- expect(eval_('a?.b?.c', { a: { b: { c: 42 } } })).toBe(42);
500
- });
501
-
502
- it('chains optional where middle is null', () => {
503
- expect(eval_('a?.b?.c', { a: { b: null } })).toBeUndefined();
504
- });
505
-
506
- it('optional chaining with method call', () => {
507
- expect(eval_('arr?.length', { arr: [1, 2, 3] })).toBe(3);
508
- expect(eval_('arr?.length', { arr: null })).toBeUndefined();
509
- });
510
-
511
- it('optional chaining on computed property', () => {
512
- expect(eval_('obj?.[key]', { obj: { x: 1 }, key: 'x' })).toBe(1);
513
- expect(eval_('obj?.[key]', { obj: null, key: 'x' })).toBeUndefined();
514
- });
515
- });
516
-
517
-
518
- // ---------------------------------------------------------------------------
519
- // Complex property access
520
- // ---------------------------------------------------------------------------
521
-
522
- describe('expression parser - complex property access', () => {
523
- it('accesses deeply nested objects', () => {
524
- const scope = { a: { b: { c: { d: { e: 'deep' } } } } };
525
- expect(eval_('a.b.c.d.e', scope)).toBe('deep');
526
- });
527
-
528
- it('accesses array index then property', () => {
529
- expect(eval_('arr[0].name', { arr: [{ name: 'first' }] })).toBe('first');
530
- });
531
-
532
- it('accesses property then array index', () => {
533
- expect(eval_('obj.items[1]', { obj: { items: ['a', 'b', 'c'] } })).toBe('b');
534
- });
535
-
536
- it('accesses computed property with variable', () => {
537
- expect(eval_('obj[key]', { obj: { x: 10, y: 20 }, key: 'y' })).toBe(20);
538
- });
539
-
540
- it('accesses nested computed property', () => {
541
- const scope = { data: { users: { alice: { age: 30 } } }, name: 'alice' };
542
- expect(eval_('data.users[name].age', scope)).toBe(30);
543
- });
544
- });
545
-
546
-
547
- // ---------------------------------------------------------------------------
548
- // Arrow function edge cases
549
- // ---------------------------------------------------------------------------
550
-
551
- describe('expression parser - arrow function edge cases', () => {
552
- it('no-param arrow function', () => {
553
- const fn = eval_('() => 42');
554
- expect(fn()).toBe(42);
555
- });
556
-
557
- it('single-param arrow (no parens)', () => {
558
- const fn = eval_('x => x * 2');
559
- expect(fn(5)).toBe(10);
560
- });
561
-
562
- it('multi-param arrow', () => {
563
- const fn = eval_('(a, b) => a + b');
564
- expect(fn(3, 4)).toBe(7);
565
- });
566
-
567
- it('arrow using outer scope', () => {
568
- const fn = eval_('x => x + y', { y: 10 });
569
- expect(fn(5)).toBe(15);
570
- });
571
-
572
- it('arrow with ternary body', () => {
573
- const fn = eval_('x => x > 0 ? "pos" : "neg"');
574
- expect(fn(1)).toBe('pos');
575
- expect(fn(-1)).toBe('neg');
576
- });
577
- });
578
-
579
-
580
- // ---------------------------------------------------------------------------
581
- // Template literal edge cases
582
- // ---------------------------------------------------------------------------
583
-
584
- describe('expression parser - template literal edge cases', () => {
585
- it('template with no interpolation', () => {
586
- expect(eval_('`hello world`')).toBe('hello world');
587
- });
588
-
589
- it('template with multiple interpolations', () => {
590
- expect(eval_('`${a} and ${b}`', { a: 'foo', b: 'bar' })).toBe('foo and bar');
591
- });
592
-
593
- it('template with expression in interpolation', () => {
594
- expect(eval_('`sum is ${a + b}`', { a: 3, b: 4 })).toBe('sum is 7');
595
- });
596
-
597
- it('template with nested property access', () => {
598
- expect(eval_('`Hello ${user.name}`', { user: { name: 'Alice' } })).toBe('Hello Alice');
599
- });
600
-
601
- it('template with ternary in interpolation', () => {
602
- expect(eval_('`${x > 0 ? "yes" : "no"}`', { x: 1 })).toBe('yes');
603
- });
604
-
605
- it('empty template literal', () => {
606
- expect(eval_('``')).toBe('');
607
- });
608
- });
609
-
610
-
611
- // ---------------------------------------------------------------------------
612
- // Nullish coalescing edge cases
613
- // ---------------------------------------------------------------------------
614
-
615
- describe('expression parser - nullish coalescing edge cases', () => {
616
- it('returns left side for 0', () => {
617
- expect(eval_('x ?? 10', { x: 0 })).toBe(0);
618
- });
619
-
620
- it('returns left side for empty string', () => {
621
- expect(eval_('x ?? "default"', { x: '' })).toBe('');
622
- });
623
-
624
- it('returns left side for false', () => {
625
- expect(eval_('x ?? true', { x: false })).toBe(false);
626
- });
627
-
628
- it('returns right side for null', () => {
629
- expect(eval_('x ?? 10', { x: null })).toBe(10);
630
- });
631
-
632
- it('returns right side for undefined', () => {
633
- expect(eval_('x ?? 10', { x: undefined })).toBe(10);
634
- });
635
-
636
- it('chains with ||', () => {
637
- expect(eval_('(a ?? b) || c', { a: null, b: 0, c: 5 })).toBe(5);
638
- });
639
- });
640
-
641
-
642
- // ---------------------------------------------------------------------------
643
- // Typeof edge cases
644
- // ---------------------------------------------------------------------------
645
-
646
- describe('expression parser - typeof edge cases', () => {
647
- it('typeof undefined variable returns "undefined"', () => {
648
- expect(eval_('typeof x')).toBe('undefined');
649
- });
650
-
651
- it('typeof number', () => {
652
- expect(eval_('typeof x', { x: 42 })).toBe('number');
653
- });
654
-
655
- it('typeof string', () => {
656
- expect(eval_('typeof x', { x: 'hi' })).toBe('string');
657
- });
658
-
659
- it('typeof object', () => {
660
- expect(eval_('typeof x', { x: {} })).toBe('object');
661
- });
662
-
663
- it('typeof null', () => {
664
- expect(eval_('typeof x', { x: null })).toBe('object');
665
- });
666
-
667
- it('typeof function', () => {
668
- expect(eval_('typeof x', { x: () => {} })).toBe('function');
669
- });
670
-
671
- it('typeof boolean', () => {
672
- expect(eval_('typeof x', { x: true })).toBe('boolean');
673
- });
674
- });
675
-
676
-
677
- // ---------------------------------------------------------------------------
678
- // Array/Object literal edge cases
679
- // ---------------------------------------------------------------------------
680
-
681
- describe('expression parser - array/object literal edge cases', () => {
682
- it('array with trailing expression', () => {
683
- expect(eval_('[1, 2, 3].length')).toBe(3);
684
- });
685
-
686
- it('array with mixed types', () => {
687
- expect(eval_('[1, "two", true, null]')).toEqual([1, 'two', true, null]);
688
- });
689
-
690
- it('object with computed values', () => {
691
- expect(eval_('{ x: a + 1, y: b * 2 }', { a: 2, b: 3 })).toEqual({ x: 3, y: 6 });
692
- });
693
-
694
- it('nested array literal', () => {
695
- expect(eval_('[[1, 2], [3, 4]]')).toEqual([[1, 2], [3, 4]]);
696
- });
697
-
698
- it('nested object literal', () => {
699
- expect(eval_('{ a: { b: 1 } }')).toEqual({ a: { b: 1 } });
700
- });
701
-
702
- it('array of objects', () => {
703
- expect(eval_('[{ x: 1 }, { x: 2 }]')).toEqual([{ x: 1 }, { x: 2 }]);
704
- });
705
- });
706
-
707
-
708
- // ---------------------------------------------------------------------------
709
- // Method call edge cases
710
- // ---------------------------------------------------------------------------
711
-
712
- describe('expression parser - method call edge cases', () => {
713
- it('chained string methods', () => {
714
- expect(eval_('"Hello World".toLowerCase().split(" ")')).toEqual(['hello', 'world']);
715
- });
716
-
717
- it('array filter', () => {
718
- expect(eval_('items.filter(x => x > 2)', { items: [1, 2, 3, 4] })).toEqual([3, 4]);
719
- });
720
-
721
- it('array map', () => {
722
- expect(eval_('items.map(x => x * 2)', { items: [1, 2, 3] })).toEqual([2, 4, 6]);
723
- });
724
-
725
- it('array includes', () => {
726
- expect(eval_('items.includes(2)', { items: [1, 2, 3] })).toBe(true);
727
- expect(eval_('items.includes(5)', { items: [1, 2, 3] })).toBe(false);
728
- });
729
-
730
- it('string includes', () => {
731
- expect(eval_('"hello world".includes("world")')).toBe(true);
732
- });
733
-
734
- it('string startsWith/endsWith', () => {
735
- expect(eval_('"hello".startsWith("hel")')).toBe(true);
736
- expect(eval_('"hello".endsWith("lo")')).toBe(true);
737
- });
738
-
739
- it('JSON.stringify', () => {
740
- expect(eval_('JSON.stringify({ a: 1 })')).toBe('{"a":1}');
741
- });
742
-
743
- it('JSON.parse', () => {
744
- expect(eval_('JSON.parse(\'{"a":1}\')')).toEqual({ a: 1 });
745
- });
746
-
747
- it('Math.max with multiple args', () => {
748
- expect(eval_('Math.max(1, 5, 3, 2, 4)')).toBe(5);
749
- });
750
-
751
- it('Math.min with multiple args', () => {
752
- expect(eval_('Math.min(1, 5, 3, 2, 4)')).toBe(1);
753
- });
754
- });
755
-
756
-
757
- // ---------------------------------------------------------------------------
758
- // Multi-scope resolution
759
- // ---------------------------------------------------------------------------
760
-
761
- describe('expression parser - multi-scope resolution', () => {
762
- it('resolves from first scope when available', () => {
763
- expect(eval_('x', { x: 1 }, { x: 2 })).toBe(1);
764
- });
765
-
766
- it('falls back to second scope', () => {
767
- expect(eval_('y', { x: 1 }, { y: 2 })).toBe(2);
768
- });
769
-
770
- it('resolves different keys from different scopes', () => {
771
- expect(eval_('x + y', { x: 10 }, { y: 20 })).toBe(30);
772
- });
773
- });
774
-
775
-
776
- // ---------------------------------------------------------------------------
777
- // Security: blocked access
778
- // ---------------------------------------------------------------------------
779
-
780
- describe('expression parser - security', () => {
781
- it('blocks constructor access', () => {
782
- expect(() => eval_('"".constructor')).not.toThrow();
783
- });
784
-
785
- it('blocks __proto__ access', () => {
786
- expect(() => eval_('obj.__proto__', { obj: {} })).not.toThrow();
787
- });
788
-
789
- it('handles invalid expressions gracefully', () => {
790
- expect(() => eval_('++++')).not.toThrow();
791
- });
792
-
793
- it('global access is sandboxed - no window', () => {
794
- expect(eval_('typeof window')).toBe('undefined');
795
- });
796
-
797
- it('global access is sandboxed - no document', () => {
798
- expect(eval_('typeof document')).toBe('undefined');
799
- });
800
- });
801
-
802
-
803
- // ---------------------------------------------------------------------------
804
- // Comparison edge cases
805
- // ---------------------------------------------------------------------------
806
-
807
- describe('expression parser - comparison edge cases', () => {
808
- it('strict equality with type mismatch', () => {
809
- expect(eval_('1 === "1"')).toBe(false);
810
- });
811
-
812
- it('loose equality with type coercion', () => {
813
- expect(eval_('1 == "1"')).toBe(true);
814
- });
815
-
816
- it('strict inequality', () => {
817
- expect(eval_('1 !== "1"')).toBe(true);
818
- });
819
-
820
- it('comparison with null', () => {
821
- expect(eval_('null === null')).toBe(true);
822
- expect(eval_('null == undefined')).toBe(true);
823
- });
824
-
825
- it('greater than / less than edge cases', () => {
826
- expect(eval_('0 < -1')).toBe(false);
827
- expect(eval_('-1 < 0')).toBe(true);
828
- });
829
- });
830
-
831
-
832
- // ---------------------------------------------------------------------------
833
- // Grouping / precedence
834
- // ---------------------------------------------------------------------------
835
-
836
- describe('expression parser - grouping and precedence', () => {
837
- it('parentheses override precedence', () => {
838
- expect(eval_('(2 + 3) * 4')).toBe(20);
839
- });
840
-
841
- it('nested parentheses', () => {
842
- expect(eval_('((1 + 2) * (3 + 4))')).toBe(21);
843
- });
844
-
845
- it('logical operators precedence', () => {
846
- expect(eval_('true || false && false')).toBe(true);
847
- });
848
-
849
- it('ternary with comparison', () => {
850
- expect(eval_('x > 5 ? "big" : "small"', { x: 10 })).toBe('big');
851
- });
852
-
853
- it('nested ternary', () => {
854
- expect(eval_('x > 10 ? "big" : x > 5 ? "med" : "small"', { x: 7 })).toBe('med');
855
- });
856
- });
857
-
858
-
859
- // ===========================================================================
860
- // new keyword - safe constructors
861
- // ===========================================================================
862
-
863
- describe('safeEval - new keyword', () => {
864
- it('creates new Date (no args)', () => {
865
- const result = eval_('new Date');
866
- expect(result).toBeInstanceOf(Date);
867
- });
868
-
869
- it('creates new Array (no args)', () => {
870
- const result = eval_('new Array');
871
- expect(result).toBeInstanceOf(Array);
872
- });
873
-
874
- it('Map/Set in globals - new creates instances', () => {
875
- // Map, Set are exposed as globals and whitelisted as safe constructors
876
- expect(eval_('new Map')).toBeInstanceOf(Map);
877
- expect(eval_('new Set')).toBeInstanceOf(Set);
878
- });
879
-
880
- it('RegExp blocked - ReDoS prevention', () => {
881
- // RegExp is no longer exposed or allowed as constructor to prevent ReDoS attacks
882
- expect(eval_('new RegExp')).toBeUndefined();
883
- expect(eval_('RegExp')).toBeUndefined();
884
- });
885
-
886
- it('new with args - parser correctly passes args to constructor', () => {
887
- const result = eval_('new Date(2024, 0, 1)');
888
- expect(result).toBeInstanceOf(Date);
889
- expect(result.getFullYear()).toBe(2024);
890
- expect(result.getMonth()).toBe(0);
891
- expect(result.getDate()).toBe(1);
892
- });
893
-
894
- it('blocks unsafe constructors', () => {
895
- const result = eval_('new Function');
896
- expect(result).toBeUndefined();
897
- });
898
- });
899
-
900
-
901
- // ===========================================================================
902
- // void operator
903
- // ===========================================================================
904
-
905
- describe('safeEval - void operator', () => {
906
- it('returns undefined', () => {
907
- expect(eval_('void 0')).toBeUndefined();
908
- });
909
-
910
- it('returns undefined for any expression', () => {
911
- expect(eval_('void "hello"')).toBeUndefined();
912
- });
913
- });
914
-
915
-
916
- // ===========================================================================
917
- // AST cache
918
- // ===========================================================================
919
-
920
- describe('safeEval - AST cache', () => {
921
- it('returns same result on repeated evaluation (cache hit)', () => {
922
- const r1 = eval_('1 + 2');
923
- const r2 = eval_('1 + 2');
924
- expect(r1).toBe(3);
925
- expect(r2).toBe(3);
926
- });
927
-
928
- it('handles many unique expressions without error (cache eviction)', () => {
929
- // Generate enough unique expressions to trigger cache eviction (>512)
930
- for (let i = 0; i < 520; i++) {
931
- expect(eval_(`${i} + 1`)).toBe(i + 1);
932
- }
933
- });
934
- });
935
-
936
-
937
- // ===========================================================================
938
- // Optional call ?.()
939
- // ===========================================================================
940
-
941
- describe('safeEval - optional call ?.()', () => {
942
- it('calls function when not null', () => {
943
- expect(eval_('fn?.()', { fn: () => 42 })).toBe(42);
944
- });
945
-
946
- it('returns undefined when callee is null', () => {
947
- expect(eval_('fn?.()', { fn: null })).toBeUndefined();
948
- });
949
-
950
- it('returns undefined when callee is undefined', () => {
951
- expect(eval_('fn?.()', {})).toBeUndefined();
952
- });
953
- });
954
-
955
-
956
- // ===========================================================================
957
- // Global builtins
958
- // ===========================================================================
959
-
960
- describe('safeEval - global builtins', () => {
961
- it('accesses Date constructor', () => {
962
- expect(eval_('Date.now()')).toBeGreaterThan(0);
963
- });
964
-
965
- it('accesses Array.isArray', () => {
966
- expect(eval_('Array.isArray(items)', { items: [1, 2] })).toBe(true);
967
- });
968
-
969
- it('accesses Object.keys', () => {
970
- expect(eval_('Object.keys(obj).length', { obj: { a: 1, b: 2 } })).toBe(2);
971
- });
972
-
973
- it('accesses String methods', () => {
974
- expect(eval_('String(42)')).toBe('42');
975
- });
976
-
977
- it('accesses Number function', () => {
978
- expect(eval_('Number("42")')).toBe(42);
979
- });
980
-
981
- it('accesses Boolean function', () => {
982
- expect(eval_('Boolean(0)')).toBe(false);
983
- });
984
-
985
- it('accesses parseInt', () => {
986
- expect(eval_('parseInt("42abc")')).toBe(42);
987
- });
988
-
989
- it('accesses parseFloat', () => {
990
- expect(eval_('parseFloat("3.14")')).toBe(3.14);
991
- });
992
-
993
- it('accesses isNaN', () => {
994
- expect(eval_('isNaN(NaN)')).toBe(true);
995
- });
996
-
997
- it('accesses isFinite', () => {
998
- expect(eval_('isFinite(42)')).toBe(true);
999
- expect(eval_('isFinite(Infinity)')).toBe(false);
1000
- });
1001
-
1002
- it('accesses Infinity', () => {
1003
- expect(eval_('Infinity')).toBe(Infinity);
1004
- });
1005
-
1006
- it('accesses NaN', () => {
1007
- expect(eval_('isNaN(NaN)')).toBe(true);
1008
- });
1009
-
1010
- it('accesses encodeURIComponent', () => {
1011
- expect(eval_('encodeURIComponent("hello world")')).toBe('hello%20world');
1012
- });
1013
-
1014
- it('accesses decodeURIComponent', () => {
1015
- expect(eval_('decodeURIComponent("hello%20world")')).toBe('hello world');
1016
- });
1017
- });
1018
-
1019
-
1020
- // ===========================================================================
1021
- // Number methods
1022
- // ===========================================================================
1023
-
1024
- describe('safeEval - number methods', () => {
1025
- it('calls toFixed', () => {
1026
- expect(eval_('x.toFixed(2)', { x: 3.14159 })).toBe('3.14');
1027
- });
1028
-
1029
- it('calls toString on number', () => {
1030
- expect(eval_('x.toString()', { x: 42 })).toBe('42');
1031
- });
1032
- });
1033
-
1034
-
1035
- // ===========================================================================
1036
- // Empty/edge expressions
1037
- // ===========================================================================
1038
-
1039
- describe('safeEval - edge cases', () => {
1040
- it('returns undefined for empty string', () => {
1041
- expect(eval_('')).toBeUndefined();
1042
- });
1043
-
1044
- it('returns undefined for whitespace-only', () => {
1045
- expect(eval_(' ')).toBeUndefined();
1046
- });
1047
-
1048
- it('fast path for simple identifier', () => {
1049
- expect(eval_('name', { name: 'Alice' })).toBe('Alice');
1050
- });
1051
-
1052
- it('handles hasOwnProperty access', () => {
1053
- const obj = { a: 1 };
1054
- expect(eval_('obj.hasOwnProperty("a")', { obj })).toBe(true);
1055
- });
1056
- });
1
+ import { describe, it, expect } from 'vitest';
2
+ import { safeEval } from '../src/expression.js';
3
+
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Helpers
7
+ // ---------------------------------------------------------------------------
8
+ const eval_ = (expr, ...scopes) => safeEval(expr, scopes.length ? scopes : [{}]);
9
+
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Literals
13
+ // ---------------------------------------------------------------------------
14
+
15
+ describe('expression parser - literals', () => {
16
+ it('numbers', () => {
17
+ expect(eval_('42')).toBe(42);
18
+ expect(eval_('3.14')).toBe(3.14);
19
+ expect(eval_('0xFF')).toBe(255);
20
+ expect(eval_('1e3')).toBe(1000);
21
+ });
22
+
23
+ it('strings', () => {
24
+ expect(eval_("'hello'")).toBe('hello');
25
+ expect(eval_('"world"')).toBe('world');
26
+ expect(eval_("'it\\'s'")).toBe("it's");
27
+ });
28
+
29
+ it('booleans and null/undefined', () => {
30
+ expect(eval_('true')).toBe(true);
31
+ expect(eval_('false')).toBe(false);
32
+ expect(eval_('null')).toBe(null);
33
+ expect(eval_('undefined')).toBe(undefined);
34
+ });
35
+
36
+ it('empty expression returns undefined', () => {
37
+ expect(eval_('')).toBe(undefined);
38
+ expect(eval_(' ')).toBe(undefined);
39
+ });
40
+ });
41
+
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Arithmetic
45
+ // ---------------------------------------------------------------------------
46
+
47
+ describe('expression parser - arithmetic', () => {
48
+ it('basic operations', () => {
49
+ expect(eval_('2 + 3')).toBe(5);
50
+ expect(eval_('10 - 4')).toBe(6);
51
+ expect(eval_('3 * 7')).toBe(21);
52
+ expect(eval_('15 / 3')).toBe(5);
53
+ expect(eval_('10 % 3')).toBe(1);
54
+ });
55
+
56
+ it('operator precedence', () => {
57
+ expect(eval_('2 + 3 * 4')).toBe(14);
58
+ expect(eval_('(2 + 3) * 4')).toBe(20);
59
+ });
60
+
61
+ it('unary operators', () => {
62
+ expect(eval_('-5')).toBe(-5);
63
+ expect(eval_('+3')).toBe(3);
64
+ });
65
+ });
66
+
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Comparison & logical
70
+ // ---------------------------------------------------------------------------
71
+
72
+ describe('expression parser - comparison', () => {
73
+ it('equality', () => {
74
+ expect(eval_('1 === 1')).toBe(true);
75
+ expect(eval_('1 !== 2')).toBe(true);
76
+ expect(eval_("1 == '1'")).toBe(true);
77
+ expect(eval_("1 != '2'")).toBe(true);
78
+ });
79
+
80
+ it('relational', () => {
81
+ expect(eval_('3 > 2')).toBe(true);
82
+ expect(eval_('3 < 2')).toBe(false);
83
+ expect(eval_('3 >= 3')).toBe(true);
84
+ expect(eval_('3 <= 2')).toBe(false);
85
+ });
86
+ });
87
+
88
+
89
+ describe('expression parser - logical', () => {
90
+ it('&& and ||', () => {
91
+ expect(eval_('true && false')).toBe(false);
92
+ expect(eval_('true || false')).toBe(true);
93
+ expect(eval_('0 || 42')).toBe(42);
94
+ expect(eval_("'' || 'default'")).toBe('default');
95
+ });
96
+
97
+ it('!', () => {
98
+ expect(eval_('!true')).toBe(false);
99
+ expect(eval_('!false')).toBe(true);
100
+ expect(eval_('!0')).toBe(true);
101
+ });
102
+
103
+ it('nullish coalescing ??', () => {
104
+ expect(eval_('null ?? 10')).toBe(10);
105
+ expect(eval_('undefined ?? 20')).toBe(20);
106
+ expect(eval_('0 ?? 30')).toBe(0);
107
+ expect(eval_("'' ?? 'fallback'")).toBe('');
108
+ });
109
+ });
110
+
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Ternary
114
+ // ---------------------------------------------------------------------------
115
+
116
+ describe('expression parser - ternary', () => {
117
+ it('evaluates truthy branch', () => {
118
+ expect(eval_("true ? 'yes' : 'no'")).toBe('yes');
119
+ });
120
+
121
+ it('evaluates falsy branch', () => {
122
+ expect(eval_("false ? 'yes' : 'no'")).toBe('no');
123
+ });
124
+
125
+ it('works with expressions', () => {
126
+ expect(eval_('5 > 3 ? 10 : 20')).toBe(10);
127
+ });
128
+ });
129
+
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Property access & scope
133
+ // ---------------------------------------------------------------------------
134
+
135
+ describe('expression parser - property access', () => {
136
+ it('reads scope variables', () => {
137
+ expect(eval_('x', { x: 42 })).toBe(42);
138
+ expect(eval_('name', { name: 'Tony' })).toBe('Tony');
139
+ });
140
+
141
+ it('dot access', () => {
142
+ expect(eval_('user.name', { user: { name: 'Tony' } })).toBe('Tony');
143
+ });
144
+
145
+ it('computed access', () => {
146
+ expect(eval_('items[0]', { items: ['a', 'b', 'c'] })).toBe('a');
147
+ expect(eval_('obj[key]', { obj: { x: 1 }, key: 'x' })).toBe(1);
148
+ });
149
+
150
+ it('optional chaining ?.', () => {
151
+ expect(eval_('user?.name', { user: null })).toBe(undefined);
152
+ expect(eval_('user?.name', { user: { name: 'Tony' } })).toBe('Tony');
153
+ });
154
+
155
+ it('returns undefined for missing scope keys', () => {
156
+ expect(eval_('missing')).toBe(undefined);
157
+ expect(eval_('a.b.c', { a: {} })).toBe(undefined);
158
+ });
159
+ });
160
+
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // Method calls
164
+ // ---------------------------------------------------------------------------
165
+
166
+ describe('expression parser - method calls', () => {
167
+ it('string methods', () => {
168
+ expect(eval_("'hello'.toUpperCase()")).toBe('HELLO');
169
+ expect(eval_("'hello world'.split(' ')")).toEqual(['hello', 'world']);
170
+ expect(eval_("'abc'.includes('b')")).toBe(true);
171
+ });
172
+
173
+ it('array methods', () => {
174
+ expect(eval_('items.length', { items: [1, 2, 3] })).toBe(3);
175
+ expect(eval_('items.includes(2)', { items: [1, 2, 3] })).toBe(true);
176
+ expect(eval_("items.join(',')", { items: [1, 2, 3] })).toBe('1,2,3');
177
+ });
178
+
179
+ it('custom function calls', () => {
180
+ const add = (a, b) => a + b;
181
+ expect(eval_('add(1, 2)', { add })).toBe(3);
182
+ });
183
+ });
184
+
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // Built-in globals
188
+ // ---------------------------------------------------------------------------
189
+
190
+ describe('expression parser - built-in globals', () => {
191
+ it('Math', () => {
192
+ expect(eval_('Math.PI')).toBeCloseTo(3.14159);
193
+ expect(eval_('Math.max(1, 5, 3)')).toBe(5);
194
+ expect(eval_('Math.abs(-7)')).toBe(7);
195
+ });
196
+
197
+ it('JSON', () => {
198
+ expect(eval_("JSON.parse('{\"a\":1}')")).toEqual({ a: 1 });
199
+ });
200
+
201
+ it('parseInt/parseFloat', () => {
202
+ expect(eval_("parseInt('42')")).toBe(42);
203
+ expect(eval_("parseFloat('3.14')")).toBeCloseTo(3.14);
204
+ });
205
+
206
+ it('isNaN', () => {
207
+ expect(eval_('isNaN(NaN)')).toBe(true);
208
+ expect(eval_('isNaN(5)')).toBe(false);
209
+ });
210
+ });
211
+
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // Template literals
215
+ // ---------------------------------------------------------------------------
216
+
217
+ describe('expression parser - template literals', () => {
218
+ it('simple interpolation', () => {
219
+ expect(eval_('`Hello ${name}`', { name: 'Tony' })).toBe('Hello Tony');
220
+ });
221
+
222
+ it('expression inside interpolation', () => {
223
+ expect(eval_('`${a + b}`', { a: 1, b: 2 })).toBe('3');
224
+ });
225
+
226
+ it('no interpolation', () => {
227
+ expect(eval_('`plain text`')).toBe('plain text');
228
+ });
229
+ });
230
+
231
+
232
+ // ---------------------------------------------------------------------------
233
+ // Array & object literals
234
+ // ---------------------------------------------------------------------------
235
+
236
+ describe('expression parser - array/object literals', () => {
237
+ it('array literal', () => {
238
+ expect(eval_('[1, 2, 3]')).toEqual([1, 2, 3]);
239
+ expect(eval_('[]')).toEqual([]);
240
+ });
241
+
242
+ it('object literal', () => {
243
+ expect(eval_("{ x: 1, y: 'two' }")).toEqual({ x: 1, y: 'two' });
244
+ });
245
+
246
+ it('shorthand property', () => {
247
+ expect(eval_('{ x }', { x: 42 })).toEqual({ x: 42 });
248
+ });
249
+ });
250
+
251
+
252
+ // ---------------------------------------------------------------------------
253
+ // Arrow functions
254
+ // ---------------------------------------------------------------------------
255
+
256
+ describe('expression parser - arrow functions', () => {
257
+ it('single-param arrow', () => {
258
+ const fn = eval_('x => x * 2');
259
+ expect(fn(3)).toBe(6);
260
+ });
261
+
262
+ it('multi-param arrow', () => {
263
+ const fn = eval_('(a, b) => a + b');
264
+ expect(fn(1, 2)).toBe(3);
265
+ });
266
+
267
+ it('no-param arrow', () => {
268
+ const fn = eval_('() => 42');
269
+ expect(fn()).toBe(42);
270
+ });
271
+
272
+ it('arrow with scope access', () => {
273
+ const items = [1, 2, 3];
274
+ expect(eval_('items.filter(x => x > 1)', { items })).toEqual([2, 3]);
275
+ });
276
+ });
277
+
278
+
279
+ // ---------------------------------------------------------------------------
280
+ // typeof
281
+ // ---------------------------------------------------------------------------
282
+
283
+ describe('expression parser - typeof', () => {
284
+ it('typeof string', () => {
285
+ expect(eval_("typeof 'hello'")).toBe('string');
286
+ });
287
+
288
+ it('typeof number', () => {
289
+ expect(eval_('typeof 42')).toBe('number');
290
+ });
291
+
292
+ it('typeof undefined variable', () => {
293
+ expect(eval_('typeof missing')).toBe('undefined');
294
+ });
295
+ });
296
+
297
+
298
+ // ---------------------------------------------------------------------------
299
+ // Safety / security
300
+ // ---------------------------------------------------------------------------
301
+
302
+ describe('expression parser - safety', () => {
303
+ it('blocks constructor access', () => {
304
+ expect(eval_("''.constructor")).toBe(undefined);
305
+ });
306
+
307
+ it('blocks __proto__ access', () => {
308
+ expect(eval_("({}).__proto__", {})).toBe(undefined);
309
+ });
310
+
311
+ it('returns undefined for invalid expressions', () => {
312
+ expect(eval_('!!!!')).toBe(undefined);
313
+ });
314
+
315
+ it('handles deeply nested safe access', () => {
316
+ const data = { a: { b: { c: { d: 'deep' } } } };
317
+ expect(eval_('a.b.c.d', data)).toBe('deep');
318
+ });
319
+ });
320
+
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // Multi-scope resolution
324
+ // ---------------------------------------------------------------------------
325
+
326
+ describe('expression parser - multi-scope', () => {
327
+ it('checks scope layers in order', () => {
328
+ expect(safeEval('x', [{ x: 'first' }, { x: 'second' }])).toBe('first');
329
+ });
330
+
331
+ it('falls through to second scope', () => {
332
+ expect(safeEval('y', [{ x: 1 }, { y: 2 }])).toBe(2);
333
+ });
334
+ });
335
+
336
+
337
+ // ---------------------------------------------------------------------------
338
+ // in operator
339
+ // ---------------------------------------------------------------------------
340
+
341
+ describe('expression parser - in operator', () => {
342
+ it('checks property existence', () => {
343
+ expect(eval_("'x' in obj", { obj: { x: 1 } })).toBe(true);
344
+ expect(eval_("'y' in obj", { obj: { x: 1 } })).toBe(false);
345
+ });
346
+ });
347
+
348
+
349
+ // ---------------------------------------------------------------------------
350
+ // instanceof operator
351
+ // ---------------------------------------------------------------------------
352
+
353
+ describe('expression parser - instanceof', () => {
354
+ it('checks instanceOf', () => {
355
+ expect(eval_('arr instanceof Array', { arr: [1, 2] })).toBe(true);
356
+ expect(eval_('obj instanceof Array', { obj: {} })).toBe(false);
357
+ });
358
+ });
359
+
360
+
361
+ // ---------------------------------------------------------------------------
362
+ // Nested ternary
363
+ // ---------------------------------------------------------------------------
364
+
365
+ describe('expression parser - nested ternary', () => {
366
+ it('evaluates simple ternary correctly', () => {
367
+ expect(eval_("x > 10 ? 'big' : 'small'", { x: 12 })).toBe('big');
368
+ expect(eval_("x > 10 ? 'big' : 'small'", { x: 2 })).toBe('small');
369
+ });
370
+ });
371
+
372
+
373
+ // ---------------------------------------------------------------------------
374
+ // Chained method calls
375
+ // ---------------------------------------------------------------------------
376
+
377
+ describe('expression parser - chained calls', () => {
378
+ it('chains array methods', () => {
379
+ expect(eval_('items.filter(x => x > 1).map(x => x * 2)', { items: [1, 2, 3] })).toEqual([4, 6]);
380
+ });
381
+
382
+ it('chains string methods', () => {
383
+ expect(eval_("name.trim().toUpperCase()", { name: ' hello ' })).toBe('HELLO');
384
+ });
385
+ });
386
+
387
+
388
+ // ---------------------------------------------------------------------------
389
+ // Spread operator
390
+ // ---------------------------------------------------------------------------
391
+
392
+ describe('expression parser - spread / rest', () => {
393
+ it('spread is not supported - returns gracefully', () => {
394
+ // The parser does not support spread syntax; verify it doesn't throw
395
+ const result = eval_('[...items, 4]', { items: [1, 2, 3] });
396
+ expect(result).toBeDefined();
397
+ });
398
+ });
399
+
400
+
401
+ // ---------------------------------------------------------------------------
402
+ // Destructuring assignment in arrow body
403
+ // ---------------------------------------------------------------------------
404
+
405
+ describe('expression parser - complex arrow', () => {
406
+ it('arrow as callback in array method', () => {
407
+ const items = [{ n: 'a' }, { n: 'b' }];
408
+ expect(eval_('items.map(x => x.n)', { items })).toEqual(['a', 'b']);
409
+ });
410
+
411
+ it('arrow with ternary body', () => {
412
+ const fn = eval_('x => x > 0 ? "pos" : "neg"');
413
+ expect(fn(1)).toBe('pos');
414
+ expect(fn(-1)).toBe('neg');
415
+ });
416
+ });
417
+
418
+
419
+ // ---------------------------------------------------------------------------
420
+ // Bitwise operators
421
+ // ---------------------------------------------------------------------------
422
+
423
+ describe('expression parser - bitwise', () => {
424
+ it('bitwise operators are not supported - does not throw', () => {
425
+ // The expression parser does not implement bitwise operators
426
+ // Verify graceful fallback rather than crashes
427
+ expect(() => eval_('5 | 3')).not.toThrow();
428
+ expect(() => eval_('5 & 3')).not.toThrow();
429
+ expect(() => eval_('5 ^ 3')).not.toThrow();
430
+ });
431
+ });
432
+
433
+
434
+ // ---------------------------------------------------------------------------
435
+ // Comma expressions
436
+ // ---------------------------------------------------------------------------
437
+
438
+ describe('expression parser - comma', () => {
439
+ it('comma expressions are not supported - does not throw', () => {
440
+ // The parser does not support comma expressions
441
+ expect(() => eval_('(1, 2, 3)')).not.toThrow();
442
+ });
443
+ });
444
+
445
+
446
+ // ---------------------------------------------------------------------------
447
+ // Edge cases
448
+ // ---------------------------------------------------------------------------
449
+
450
+ describe('expression parser - edge cases', () => {
451
+ it('handles very long dot chains', () => {
452
+ const data = { a: { b: { c: { d: { e: 42 } } } } };
453
+ expect(eval_('a.b.c.d.e', data)).toBe(42);
454
+ });
455
+
456
+ it('handles numeric string keys in bracket access', () => {
457
+ expect(eval_("items['0']", { items: ['a', 'b'] })).toBe('a');
458
+ });
459
+
460
+ it('handles conditional access chains', () => {
461
+ expect(eval_('a?.b?.c', { a: null })).toBe(undefined);
462
+ expect(eval_('a?.b?.c', { a: { b: { c: 1 } } })).toBe(1);
463
+ });
464
+
465
+ it('handles string with special characters', () => {
466
+ expect(eval_("'hello\\nworld'")).toContain('hello');
467
+ });
468
+
469
+ it('handles negative numbers in expressions', () => {
470
+ expect(eval_('-1 + -2')).toBe(-3);
471
+ });
472
+
473
+ it('exponentiation ** is not supported - does not throw', () => {
474
+ // The parser does not implement ** operator
475
+ expect(() => eval_('2 ** 3')).not.toThrow();
476
+ });
477
+
478
+ it('handles empty array/object', () => {
479
+ expect(eval_('[]')).toEqual([]);
480
+ expect(eval_('{}')).toEqual({});
481
+ });
482
+ });
483
+
484
+
485
+ // ---------------------------------------------------------------------------
486
+ // Optional chaining edge cases
487
+ // ---------------------------------------------------------------------------
488
+
489
+ describe('expression parser - optional chaining edge cases', () => {
490
+ it('returns undefined for null base with ?.', () => {
491
+ expect(eval_('a?.b', { a: null })).toBeUndefined();
492
+ });
493
+
494
+ it('returns undefined for undefined base with ?.', () => {
495
+ expect(eval_('a?.b', { a: undefined })).toBeUndefined();
496
+ });
497
+
498
+ it('chains multiple optional access', () => {
499
+ expect(eval_('a?.b?.c', { a: { b: { c: 42 } } })).toBe(42);
500
+ });
501
+
502
+ it('chains optional where middle is null', () => {
503
+ expect(eval_('a?.b?.c', { a: { b: null } })).toBeUndefined();
504
+ });
505
+
506
+ it('optional chaining with method call', () => {
507
+ expect(eval_('arr?.length', { arr: [1, 2, 3] })).toBe(3);
508
+ expect(eval_('arr?.length', { arr: null })).toBeUndefined();
509
+ });
510
+
511
+ it('optional chaining on computed property', () => {
512
+ expect(eval_('obj?.[key]', { obj: { x: 1 }, key: 'x' })).toBe(1);
513
+ expect(eval_('obj?.[key]', { obj: null, key: 'x' })).toBeUndefined();
514
+ });
515
+ });
516
+
517
+
518
+ // ---------------------------------------------------------------------------
519
+ // Complex property access
520
+ // ---------------------------------------------------------------------------
521
+
522
+ describe('expression parser - complex property access', () => {
523
+ it('accesses deeply nested objects', () => {
524
+ const scope = { a: { b: { c: { d: { e: 'deep' } } } } };
525
+ expect(eval_('a.b.c.d.e', scope)).toBe('deep');
526
+ });
527
+
528
+ it('accesses array index then property', () => {
529
+ expect(eval_('arr[0].name', { arr: [{ name: 'first' }] })).toBe('first');
530
+ });
531
+
532
+ it('accesses property then array index', () => {
533
+ expect(eval_('obj.items[1]', { obj: { items: ['a', 'b', 'c'] } })).toBe('b');
534
+ });
535
+
536
+ it('accesses computed property with variable', () => {
537
+ expect(eval_('obj[key]', { obj: { x: 10, y: 20 }, key: 'y' })).toBe(20);
538
+ });
539
+
540
+ it('accesses nested computed property', () => {
541
+ const scope = { data: { users: { alice: { age: 30 } } }, name: 'alice' };
542
+ expect(eval_('data.users[name].age', scope)).toBe(30);
543
+ });
544
+ });
545
+
546
+
547
+ // ---------------------------------------------------------------------------
548
+ // Arrow function edge cases
549
+ // ---------------------------------------------------------------------------
550
+
551
+ describe('expression parser - arrow function edge cases', () => {
552
+ it('no-param arrow function', () => {
553
+ const fn = eval_('() => 42');
554
+ expect(fn()).toBe(42);
555
+ });
556
+
557
+ it('single-param arrow (no parens)', () => {
558
+ const fn = eval_('x => x * 2');
559
+ expect(fn(5)).toBe(10);
560
+ });
561
+
562
+ it('multi-param arrow', () => {
563
+ const fn = eval_('(a, b) => a + b');
564
+ expect(fn(3, 4)).toBe(7);
565
+ });
566
+
567
+ it('arrow using outer scope', () => {
568
+ const fn = eval_('x => x + y', { y: 10 });
569
+ expect(fn(5)).toBe(15);
570
+ });
571
+
572
+ it('arrow with ternary body', () => {
573
+ const fn = eval_('x => x > 0 ? "pos" : "neg"');
574
+ expect(fn(1)).toBe('pos');
575
+ expect(fn(-1)).toBe('neg');
576
+ });
577
+ });
578
+
579
+
580
+ // ---------------------------------------------------------------------------
581
+ // Template literal edge cases
582
+ // ---------------------------------------------------------------------------
583
+
584
+ describe('expression parser - template literal edge cases', () => {
585
+ it('template with no interpolation', () => {
586
+ expect(eval_('`hello world`')).toBe('hello world');
587
+ });
588
+
589
+ it('template with multiple interpolations', () => {
590
+ expect(eval_('`${a} and ${b}`', { a: 'foo', b: 'bar' })).toBe('foo and bar');
591
+ });
592
+
593
+ it('template with expression in interpolation', () => {
594
+ expect(eval_('`sum is ${a + b}`', { a: 3, b: 4 })).toBe('sum is 7');
595
+ });
596
+
597
+ it('template with nested property access', () => {
598
+ expect(eval_('`Hello ${user.name}`', { user: { name: 'Alice' } })).toBe('Hello Alice');
599
+ });
600
+
601
+ it('template with ternary in interpolation', () => {
602
+ expect(eval_('`${x > 0 ? "yes" : "no"}`', { x: 1 })).toBe('yes');
603
+ });
604
+
605
+ it('empty template literal', () => {
606
+ expect(eval_('``')).toBe('');
607
+ });
608
+ });
609
+
610
+
611
+ // ---------------------------------------------------------------------------
612
+ // Nullish coalescing edge cases
613
+ // ---------------------------------------------------------------------------
614
+
615
+ describe('expression parser - nullish coalescing edge cases', () => {
616
+ it('returns left side for 0', () => {
617
+ expect(eval_('x ?? 10', { x: 0 })).toBe(0);
618
+ });
619
+
620
+ it('returns left side for empty string', () => {
621
+ expect(eval_('x ?? "default"', { x: '' })).toBe('');
622
+ });
623
+
624
+ it('returns left side for false', () => {
625
+ expect(eval_('x ?? true', { x: false })).toBe(false);
626
+ });
627
+
628
+ it('returns right side for null', () => {
629
+ expect(eval_('x ?? 10', { x: null })).toBe(10);
630
+ });
631
+
632
+ it('returns right side for undefined', () => {
633
+ expect(eval_('x ?? 10', { x: undefined })).toBe(10);
634
+ });
635
+
636
+ it('chains with ||', () => {
637
+ expect(eval_('(a ?? b) || c', { a: null, b: 0, c: 5 })).toBe(5);
638
+ });
639
+ });
640
+
641
+
642
+ // ---------------------------------------------------------------------------
643
+ // Typeof edge cases
644
+ // ---------------------------------------------------------------------------
645
+
646
+ describe('expression parser - typeof edge cases', () => {
647
+ it('typeof undefined variable returns "undefined"', () => {
648
+ expect(eval_('typeof x')).toBe('undefined');
649
+ });
650
+
651
+ it('typeof number', () => {
652
+ expect(eval_('typeof x', { x: 42 })).toBe('number');
653
+ });
654
+
655
+ it('typeof string', () => {
656
+ expect(eval_('typeof x', { x: 'hi' })).toBe('string');
657
+ });
658
+
659
+ it('typeof object', () => {
660
+ expect(eval_('typeof x', { x: {} })).toBe('object');
661
+ });
662
+
663
+ it('typeof null', () => {
664
+ expect(eval_('typeof x', { x: null })).toBe('object');
665
+ });
666
+
667
+ it('typeof function', () => {
668
+ expect(eval_('typeof x', { x: () => {} })).toBe('function');
669
+ });
670
+
671
+ it('typeof boolean', () => {
672
+ expect(eval_('typeof x', { x: true })).toBe('boolean');
673
+ });
674
+ });
675
+
676
+
677
+ // ---------------------------------------------------------------------------
678
+ // Array/Object literal edge cases
679
+ // ---------------------------------------------------------------------------
680
+
681
+ describe('expression parser - array/object literal edge cases', () => {
682
+ it('array with trailing expression', () => {
683
+ expect(eval_('[1, 2, 3].length')).toBe(3);
684
+ });
685
+
686
+ it('array with mixed types', () => {
687
+ expect(eval_('[1, "two", true, null]')).toEqual([1, 'two', true, null]);
688
+ });
689
+
690
+ it('object with computed values', () => {
691
+ expect(eval_('{ x: a + 1, y: b * 2 }', { a: 2, b: 3 })).toEqual({ x: 3, y: 6 });
692
+ });
693
+
694
+ it('nested array literal', () => {
695
+ expect(eval_('[[1, 2], [3, 4]]')).toEqual([[1, 2], [3, 4]]);
696
+ });
697
+
698
+ it('nested object literal', () => {
699
+ expect(eval_('{ a: { b: 1 } }')).toEqual({ a: { b: 1 } });
700
+ });
701
+
702
+ it('array of objects', () => {
703
+ expect(eval_('[{ x: 1 }, { x: 2 }]')).toEqual([{ x: 1 }, { x: 2 }]);
704
+ });
705
+ });
706
+
707
+
708
+ // ---------------------------------------------------------------------------
709
+ // Method call edge cases
710
+ // ---------------------------------------------------------------------------
711
+
712
+ describe('expression parser - method call edge cases', () => {
713
+ it('chained string methods', () => {
714
+ expect(eval_('"Hello World".toLowerCase().split(" ")')).toEqual(['hello', 'world']);
715
+ });
716
+
717
+ it('array filter', () => {
718
+ expect(eval_('items.filter(x => x > 2)', { items: [1, 2, 3, 4] })).toEqual([3, 4]);
719
+ });
720
+
721
+ it('array map', () => {
722
+ expect(eval_('items.map(x => x * 2)', { items: [1, 2, 3] })).toEqual([2, 4, 6]);
723
+ });
724
+
725
+ it('array includes', () => {
726
+ expect(eval_('items.includes(2)', { items: [1, 2, 3] })).toBe(true);
727
+ expect(eval_('items.includes(5)', { items: [1, 2, 3] })).toBe(false);
728
+ });
729
+
730
+ it('string includes', () => {
731
+ expect(eval_('"hello world".includes("world")')).toBe(true);
732
+ });
733
+
734
+ it('string startsWith/endsWith', () => {
735
+ expect(eval_('"hello".startsWith("hel")')).toBe(true);
736
+ expect(eval_('"hello".endsWith("lo")')).toBe(true);
737
+ });
738
+
739
+ it('JSON.stringify', () => {
740
+ expect(eval_('JSON.stringify({ a: 1 })')).toBe('{"a":1}');
741
+ });
742
+
743
+ it('JSON.parse', () => {
744
+ expect(eval_('JSON.parse(\'{"a":1}\')')).toEqual({ a: 1 });
745
+ });
746
+
747
+ it('Math.max with multiple args', () => {
748
+ expect(eval_('Math.max(1, 5, 3, 2, 4)')).toBe(5);
749
+ });
750
+
751
+ it('Math.min with multiple args', () => {
752
+ expect(eval_('Math.min(1, 5, 3, 2, 4)')).toBe(1);
753
+ });
754
+ });
755
+
756
+
757
+ // ---------------------------------------------------------------------------
758
+ // Multi-scope resolution
759
+ // ---------------------------------------------------------------------------
760
+
761
+ describe('expression parser - multi-scope resolution', () => {
762
+ it('resolves from first scope when available', () => {
763
+ expect(eval_('x', { x: 1 }, { x: 2 })).toBe(1);
764
+ });
765
+
766
+ it('falls back to second scope', () => {
767
+ expect(eval_('y', { x: 1 }, { y: 2 })).toBe(2);
768
+ });
769
+
770
+ it('resolves different keys from different scopes', () => {
771
+ expect(eval_('x + y', { x: 10 }, { y: 20 })).toBe(30);
772
+ });
773
+ });
774
+
775
+
776
+ // ---------------------------------------------------------------------------
777
+ // Security: blocked access
778
+ // ---------------------------------------------------------------------------
779
+
780
+ describe('expression parser - security', () => {
781
+ it('blocks constructor access', () => {
782
+ expect(() => eval_('"".constructor')).not.toThrow();
783
+ });
784
+
785
+ it('blocks __proto__ access', () => {
786
+ expect(() => eval_('obj.__proto__', { obj: {} })).not.toThrow();
787
+ });
788
+
789
+ it('handles invalid expressions gracefully', () => {
790
+ expect(() => eval_('++++')).not.toThrow();
791
+ });
792
+
793
+ it('global access is sandboxed - no window', () => {
794
+ expect(eval_('typeof window')).toBe('undefined');
795
+ });
796
+
797
+ it('global access is sandboxed - no document', () => {
798
+ expect(eval_('typeof document')).toBe('undefined');
799
+ });
800
+ });
801
+
802
+
803
+ // ---------------------------------------------------------------------------
804
+ // Comparison edge cases
805
+ // ---------------------------------------------------------------------------
806
+
807
+ describe('expression parser - comparison edge cases', () => {
808
+ it('strict equality with type mismatch', () => {
809
+ expect(eval_('1 === "1"')).toBe(false);
810
+ });
811
+
812
+ it('loose equality with type coercion', () => {
813
+ expect(eval_('1 == "1"')).toBe(true);
814
+ });
815
+
816
+ it('strict inequality', () => {
817
+ expect(eval_('1 !== "1"')).toBe(true);
818
+ });
819
+
820
+ it('comparison with null', () => {
821
+ expect(eval_('null === null')).toBe(true);
822
+ expect(eval_('null == undefined')).toBe(true);
823
+ });
824
+
825
+ it('greater than / less than edge cases', () => {
826
+ expect(eval_('0 < -1')).toBe(false);
827
+ expect(eval_('-1 < 0')).toBe(true);
828
+ });
829
+ });
830
+
831
+
832
+ // ---------------------------------------------------------------------------
833
+ // Grouping / precedence
834
+ // ---------------------------------------------------------------------------
835
+
836
+ describe('expression parser - grouping and precedence', () => {
837
+ it('parentheses override precedence', () => {
838
+ expect(eval_('(2 + 3) * 4')).toBe(20);
839
+ });
840
+
841
+ it('nested parentheses', () => {
842
+ expect(eval_('((1 + 2) * (3 + 4))')).toBe(21);
843
+ });
844
+
845
+ it('logical operators precedence', () => {
846
+ expect(eval_('true || false && false')).toBe(true);
847
+ });
848
+
849
+ it('ternary with comparison', () => {
850
+ expect(eval_('x > 5 ? "big" : "small"', { x: 10 })).toBe('big');
851
+ });
852
+
853
+ it('nested ternary', () => {
854
+ expect(eval_('x > 10 ? "big" : x > 5 ? "med" : "small"', { x: 7 })).toBe('med');
855
+ });
856
+ });
857
+
858
+
859
+ // ===========================================================================
860
+ // new keyword - safe constructors
861
+ // ===========================================================================
862
+
863
+ describe('safeEval - new keyword', () => {
864
+ it('creates new Date (no args)', () => {
865
+ const result = eval_('new Date');
866
+ expect(result).toBeInstanceOf(Date);
867
+ });
868
+
869
+ it('creates new Array (no args)', () => {
870
+ const result = eval_('new Array');
871
+ expect(result).toBeInstanceOf(Array);
872
+ });
873
+
874
+ it('Map/Set in globals - new creates instances', () => {
875
+ // Map, Set are exposed as globals and whitelisted as safe constructors
876
+ expect(eval_('new Map')).toBeInstanceOf(Map);
877
+ expect(eval_('new Set')).toBeInstanceOf(Set);
878
+ });
879
+
880
+ it('RegExp blocked - ReDoS prevention', () => {
881
+ // RegExp is no longer exposed or allowed as constructor to prevent ReDoS attacks
882
+ expect(eval_('new RegExp')).toBeUndefined();
883
+ expect(eval_('RegExp')).toBeUndefined();
884
+ });
885
+
886
+ it('new with args - parser correctly passes args to constructor', () => {
887
+ const result = eval_('new Date(2024, 0, 1)');
888
+ expect(result).toBeInstanceOf(Date);
889
+ expect(result.getFullYear()).toBe(2024);
890
+ expect(result.getMonth()).toBe(0);
891
+ expect(result.getDate()).toBe(1);
892
+ });
893
+
894
+ it('blocks unsafe constructors', () => {
895
+ const result = eval_('new Function');
896
+ expect(result).toBeUndefined();
897
+ });
898
+ });
899
+
900
+
901
+ // ===========================================================================
902
+ // void operator
903
+ // ===========================================================================
904
+
905
+ describe('safeEval - void operator', () => {
906
+ it('returns undefined', () => {
907
+ expect(eval_('void 0')).toBeUndefined();
908
+ });
909
+
910
+ it('returns undefined for any expression', () => {
911
+ expect(eval_('void "hello"')).toBeUndefined();
912
+ });
913
+ });
914
+
915
+
916
+ // ===========================================================================
917
+ // AST cache
918
+ // ===========================================================================
919
+
920
+ describe('safeEval - AST cache', () => {
921
+ it('returns same result on repeated evaluation (cache hit)', () => {
922
+ const r1 = eval_('1 + 2');
923
+ const r2 = eval_('1 + 2');
924
+ expect(r1).toBe(3);
925
+ expect(r2).toBe(3);
926
+ });
927
+
928
+ it('handles many unique expressions without error (cache eviction)', () => {
929
+ // Generate enough unique expressions to trigger cache eviction (>512)
930
+ for (let i = 0; i < 520; i++) {
931
+ expect(eval_(`${i} + 1`)).toBe(i + 1);
932
+ }
933
+ });
934
+ });
935
+
936
+
937
+ // ===========================================================================
938
+ // Optional call ?.()
939
+ // ===========================================================================
940
+
941
+ describe('safeEval - optional call ?.()', () => {
942
+ it('calls function when not null', () => {
943
+ expect(eval_('fn?.()', { fn: () => 42 })).toBe(42);
944
+ });
945
+
946
+ it('returns undefined when callee is null', () => {
947
+ expect(eval_('fn?.()', { fn: null })).toBeUndefined();
948
+ });
949
+
950
+ it('returns undefined when callee is undefined', () => {
951
+ expect(eval_('fn?.()', {})).toBeUndefined();
952
+ });
953
+ });
954
+
955
+
956
+ // ===========================================================================
957
+ // Global builtins
958
+ // ===========================================================================
959
+
960
+ describe('safeEval - global builtins', () => {
961
+ it('accesses Date constructor', () => {
962
+ expect(eval_('Date.now()')).toBeGreaterThan(0);
963
+ });
964
+
965
+ it('accesses Array.isArray', () => {
966
+ expect(eval_('Array.isArray(items)', { items: [1, 2] })).toBe(true);
967
+ });
968
+
969
+ it('accesses Object.keys', () => {
970
+ expect(eval_('Object.keys(obj).length', { obj: { a: 1, b: 2 } })).toBe(2);
971
+ });
972
+
973
+ it('accesses String methods', () => {
974
+ expect(eval_('String(42)')).toBe('42');
975
+ });
976
+
977
+ it('accesses Number function', () => {
978
+ expect(eval_('Number("42")')).toBe(42);
979
+ });
980
+
981
+ it('accesses Boolean function', () => {
982
+ expect(eval_('Boolean(0)')).toBe(false);
983
+ });
984
+
985
+ it('accesses parseInt', () => {
986
+ expect(eval_('parseInt("42abc")')).toBe(42);
987
+ });
988
+
989
+ it('accesses parseFloat', () => {
990
+ expect(eval_('parseFloat("3.14")')).toBe(3.14);
991
+ });
992
+
993
+ it('accesses isNaN', () => {
994
+ expect(eval_('isNaN(NaN)')).toBe(true);
995
+ });
996
+
997
+ it('accesses isFinite', () => {
998
+ expect(eval_('isFinite(42)')).toBe(true);
999
+ expect(eval_('isFinite(Infinity)')).toBe(false);
1000
+ });
1001
+
1002
+ it('accesses Infinity', () => {
1003
+ expect(eval_('Infinity')).toBe(Infinity);
1004
+ });
1005
+
1006
+ it('accesses NaN', () => {
1007
+ expect(eval_('isNaN(NaN)')).toBe(true);
1008
+ });
1009
+
1010
+ it('accesses encodeURIComponent', () => {
1011
+ expect(eval_('encodeURIComponent("hello world")')).toBe('hello%20world');
1012
+ });
1013
+
1014
+ it('accesses decodeURIComponent', () => {
1015
+ expect(eval_('decodeURIComponent("hello%20world")')).toBe('hello world');
1016
+ });
1017
+ });
1018
+
1019
+
1020
+ // ===========================================================================
1021
+ // Number methods
1022
+ // ===========================================================================
1023
+
1024
+ describe('safeEval - number methods', () => {
1025
+ it('calls toFixed', () => {
1026
+ expect(eval_('x.toFixed(2)', { x: 3.14159 })).toBe('3.14');
1027
+ });
1028
+
1029
+ it('calls toString on number', () => {
1030
+ expect(eval_('x.toString()', { x: 42 })).toBe('42');
1031
+ });
1032
+ });
1033
+
1034
+
1035
+ // ===========================================================================
1036
+ // Empty/edge expressions
1037
+ // ===========================================================================
1038
+
1039
+ describe('safeEval - edge cases', () => {
1040
+ it('returns undefined for empty string', () => {
1041
+ expect(eval_('')).toBeUndefined();
1042
+ });
1043
+
1044
+ it('returns undefined for whitespace-only', () => {
1045
+ expect(eval_(' ')).toBeUndefined();
1046
+ });
1047
+
1048
+ it('fast path for simple identifier', () => {
1049
+ expect(eval_('name', { name: 'Alice' })).toBe('Alice');
1050
+ });
1051
+
1052
+ it('handles hasOwnProperty access', () => {
1053
+ const obj = { a: 1 };
1054
+ expect(eval_('obj.hasOwnProperty("a")', { obj })).toBe(true);
1055
+ });
1056
+ });