@barefootjs/jsx 0.15.2 → 0.17.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.
- package/dist/adapters/env-signal.d.ts +38 -15
- package/dist/adapters/env-signal.d.ts.map +1 -1
- package/dist/adapters/jsx-adapter.d.ts.map +1 -1
- package/dist/adapters/parsed-expr-emitter.d.ts +7 -6
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts +29 -1
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/builtin-lowering-plugins.d.ts +34 -0
- package/dist/builtin-lowering-plugins.d.ts.map +1 -0
- package/dist/expression-parser.d.ts +219 -163
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +9 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6892 -6118
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/build-declaration-emit.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts +9 -0
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/lowering-registry.d.ts +122 -0
- package/dist/lowering-registry.d.ts.map +1 -0
- package/dist/profiler.d.ts +115 -0
- package/dist/profiler.d.ts.map +1 -1
- package/dist/query-href-lowering.d.ts +63 -0
- package/dist/query-href-lowering.d.ts.map +1 -0
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/types.d.ts +169 -11
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +68 -3
- package/src/__tests__/analyzer.test.ts +53 -0
- package/src/__tests__/expression-parser.test.ts +703 -391
- package/src/__tests__/ir-reduce-op.test.ts +18 -21
- package/src/__tests__/ir-sort-comparator.test.ts +19 -20
- package/src/__tests__/lowering-registry.test.ts +141 -0
- package/src/__tests__/primitive-resolver-alias.test.ts +23 -0
- package/src/__tests__/profiler.test.ts +149 -0
- package/src/__tests__/query-href-recognition.test.ts +58 -0
- package/src/__tests__/serialize-parsed-expr.test.ts +204 -0
- package/src/__tests__/unsupported-expression.test.ts +98 -4
- package/src/adapters/env-signal.ts +60 -21
- package/src/adapters/jsx-adapter.ts +17 -0
- package/src/adapters/parsed-expr-emitter.ts +39 -41
- package/src/analyzer-context.ts +72 -27
- package/src/analyzer.ts +226 -9
- package/src/builtin-lowering-plugins.ts +54 -0
- package/src/expression-parser.ts +1183 -927
- package/src/index.ts +35 -3
- package/src/ir-to-client-js/csr-substitute.ts +5 -0
- package/src/ir-to-client-js/plan/build-declaration-emit.ts +16 -0
- package/src/ir-to-client-js/plan/declaration-emit.ts +9 -0
- package/src/ir-to-client-js/stringify/declaration-emit.ts +11 -0
- package/src/jsx-to-ir.ts +182 -43
- package/src/lowering-registry.ts +160 -0
- package/src/profiler.ts +328 -0
- package/src/query-href-lowering.ts +147 -0
- package/src/ssr-defaults.ts +5 -1
- package/src/types.ts +171 -12
- package/src/__tests__/flatmap-support.test.ts +0 -218
- package/src/__tests__/reduce-op.test.ts +0 -201
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect } from 'bun:test'
|
|
2
2
|
import ts from 'typescript'
|
|
3
|
-
import { parseExpression, isSupported, exprToString, stringifyParsedExpr, parseBlockBody, extractArrowBodyExpression, parseStyleObjectEntries, parseProviderObjectLiteral } from '../expression-parser'
|
|
3
|
+
import { parseExpression, isSupported, exprToString, stringifyParsedExpr, parseBlockBody, foldBlockToExpr, extractArrowBodyExpression, parseStyleObjectEntries, parseProviderObjectLiteral, asCallbackMethodCall, containsHigherOrder } from '../expression-parser'
|
|
4
4
|
import { collectAllTypeRanges, reconstructWithoutTypes } from '../strip-types'
|
|
5
5
|
|
|
6
6
|
describe('expression-parser', () => {
|
|
@@ -175,58 +175,53 @@ describe('expression-parser', () => {
|
|
|
175
175
|
|
|
176
176
|
test('parses arrow function with single param and expression body', () => {
|
|
177
177
|
const result = parseExpression('x => x + 1')
|
|
178
|
-
expect(result.kind).toBe('arrow
|
|
179
|
-
if (result.kind === 'arrow
|
|
180
|
-
expect(result.
|
|
178
|
+
expect(result.kind).toBe('arrow')
|
|
179
|
+
if (result.kind === 'arrow') {
|
|
180
|
+
expect(result.params).toEqual(['x'])
|
|
181
181
|
expect(result.body.kind).toBe('binary')
|
|
182
182
|
}
|
|
183
183
|
})
|
|
184
184
|
|
|
185
185
|
test('parses filter() call into higher-order kind', () => {
|
|
186
186
|
const result = parseExpression('todos().filter(t => !t.done)')
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
187
|
+
const cb = asCallbackMethodCall(result)
|
|
188
|
+
expect(cb).not.toBeNull()
|
|
189
|
+
expect(cb!.method).toBe('filter')
|
|
190
|
+
expect(cb!.arrow.params[0]).toBe('t')
|
|
191
|
+
expect(cb!.arrow.body.kind).toBe('unary')
|
|
193
192
|
})
|
|
194
193
|
|
|
195
194
|
test('parses every() call into higher-order kind', () => {
|
|
196
195
|
const result = parseExpression('todos().every(t => t.done)')
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
196
|
+
const cb = asCallbackMethodCall(result)
|
|
197
|
+
expect(cb).not.toBeNull()
|
|
198
|
+
expect(cb!.method).toBe('every')
|
|
199
|
+
expect(cb!.arrow.params[0]).toBe('t')
|
|
202
200
|
})
|
|
203
201
|
|
|
204
202
|
test('parses some() call into higher-order kind', () => {
|
|
205
203
|
const result = parseExpression('todos().some(t => t.important)')
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
204
|
+
const cb = asCallbackMethodCall(result)
|
|
205
|
+
expect(cb).not.toBeNull()
|
|
206
|
+
expect(cb!.method).toBe('some')
|
|
207
|
+
expect(cb!.arrow.params[0]).toBe('t')
|
|
211
208
|
})
|
|
212
209
|
|
|
213
210
|
test('parses find() call into higher-order kind', () => {
|
|
214
211
|
const result = parseExpression('users().find(u => u.id === selectedId())')
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
212
|
+
const cb = asCallbackMethodCall(result)
|
|
213
|
+
expect(cb).not.toBeNull()
|
|
214
|
+
expect(cb!.method).toBe('find')
|
|
215
|
+
expect(cb!.arrow.params[0]).toBe('u')
|
|
216
|
+
expect(cb!.arrow.body.kind).toBe('binary')
|
|
221
217
|
})
|
|
222
218
|
|
|
223
219
|
test('parses findIndex() call into higher-order kind', () => {
|
|
224
220
|
const result = parseExpression('items().findIndex(t => t.done)')
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
221
|
+
const cb = asCallbackMethodCall(result)
|
|
222
|
+
expect(cb).not.toBeNull()
|
|
223
|
+
expect(cb!.method).toBe('findIndex')
|
|
224
|
+
expect(cb!.arrow.params[0]).toBe('t')
|
|
230
225
|
})
|
|
231
226
|
|
|
232
227
|
test('parses find().property into member kind with higher-order object', () => {
|
|
@@ -234,10 +229,9 @@ describe('expression-parser', () => {
|
|
|
234
229
|
expect(result.kind).toBe('member')
|
|
235
230
|
if (result.kind === 'member') {
|
|
236
231
|
expect(result.property).toBe('name')
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
232
|
+
const cb = asCallbackMethodCall(result.object)
|
|
233
|
+
expect(cb).not.toBeNull()
|
|
234
|
+
expect(cb!.method).toBe('find')
|
|
241
235
|
}
|
|
242
236
|
})
|
|
243
237
|
|
|
@@ -246,11 +240,10 @@ describe('expression-parser', () => {
|
|
|
246
240
|
expect(result.kind).toBe('member')
|
|
247
241
|
if (result.kind === 'member') {
|
|
248
242
|
expect(result.property).toBe('length')
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
243
|
+
const cb = asCallbackMethodCall(result.object)
|
|
244
|
+
expect(cb).not.toBeNull()
|
|
245
|
+
expect(cb!.method).toBe('filter')
|
|
246
|
+
expect(cb!.arrow.params[0]).toBe('t')
|
|
254
247
|
}
|
|
255
248
|
})
|
|
256
249
|
|
|
@@ -261,28 +254,28 @@ describe('expression-parser', () => {
|
|
|
261
254
|
// existing higher-order paths.
|
|
262
255
|
test('parses .filter(Boolean) into higher-order kind with synthetic identity predicate (#1443)', () => {
|
|
263
256
|
const result = parseExpression('arr.filter(Boolean)')
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
257
|
+
const cb = asCallbackMethodCall(result)
|
|
258
|
+
expect(cb).not.toBeNull()
|
|
259
|
+
expect(cb!.method).toBe('filter')
|
|
260
|
+
// The synthetic param is just an identifier-matching marker;
|
|
261
|
+
// adapters substitute it into their loop variable. Whichever
|
|
262
|
+
// name we pick must equal the body identifier so the substitution
|
|
263
|
+
// round-trips into a truthy check.
|
|
264
|
+
expect(cb!.arrow.body.kind).toBe('identifier')
|
|
265
|
+
if (cb!.arrow.body.kind === 'identifier') {
|
|
266
|
+
expect(cb!.arrow.body.name).toBe(cb!.arrow.params[0])
|
|
275
267
|
}
|
|
276
268
|
})
|
|
277
269
|
|
|
278
270
|
// The Boolean-callable shortcut is filter-specific because the
|
|
279
271
|
// truthy-identity rewrite only matches `filter`'s semantics. For
|
|
280
272
|
// `.every(Boolean)` / `.some(Boolean)` etc. the rewrite would
|
|
281
|
-
// produce different JS semantics — leave them
|
|
282
|
-
//
|
|
273
|
+
// produce different JS semantics — leave them un-synthesised
|
|
274
|
+
// (the bare `Boolean` callable does not become a callback arrow)
|
|
275
|
+
// until each gets its own deliberate lowering.
|
|
283
276
|
test('does NOT lower .every(Boolean) or .some(Boolean) — filter-specific shortcut (#1443)', () => {
|
|
284
|
-
expect(parseExpression('arr.every(Boolean)')
|
|
285
|
-
expect(parseExpression('arr.some(Boolean)')
|
|
277
|
+
expect(asCallbackMethodCall(parseExpression('arr.every(Boolean)'))).toBeNull()
|
|
278
|
+
expect(asCallbackMethodCall(parseExpression('arr.some(Boolean)'))).toBeNull()
|
|
286
279
|
})
|
|
287
280
|
|
|
288
281
|
// Array literals show up in the registry Slot's
|
|
@@ -300,6 +293,86 @@ describe('expression-parser', () => {
|
|
|
300
293
|
}
|
|
301
294
|
})
|
|
302
295
|
|
|
296
|
+
// Object literal → `object-literal` kind (Roadmap A-1). Carried so
|
|
297
|
+
// adapters can lower an object value structurally instead of
|
|
298
|
+
// re-parsing the source with `ts.createSourceFile`. Object literals
|
|
299
|
+
// reach the parser parenthesised — `() => ({ … })`, a prop value —
|
|
300
|
+
// because a leading `{` at statement position is a block, not an
|
|
301
|
+
// expression (covered below).
|
|
302
|
+
test('parses object literal into object-literal kind', () => {
|
|
303
|
+
const result = parseExpression('({ a: 1, b: x, "c-d": foo() })')
|
|
304
|
+
expect(result.kind).toBe('object-literal')
|
|
305
|
+
if (result.kind === 'object-literal') {
|
|
306
|
+
expect(result.properties.map(p => p.key)).toEqual(['a', 'b', 'c-d'])
|
|
307
|
+
expect(result.properties.map(p => p.value.kind)).toEqual([
|
|
308
|
+
'literal', 'identifier', 'call',
|
|
309
|
+
])
|
|
310
|
+
expect(result.properties.every(p => !p.shorthand)).toBe(true)
|
|
311
|
+
// `raw` preserves the original string for byte-identical fallback.
|
|
312
|
+
expect(result.raw).toBe('({ a: 1, b: x, "c-d": foo() })')
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
test('records the key kind so numeric and string keys are distinguishable', () => {
|
|
317
|
+
const result = parseExpression('({ a: 1, "1": 2, 3: 4 })')
|
|
318
|
+
expect(result.kind).toBe('object-literal')
|
|
319
|
+
if (result.kind === 'object-literal') {
|
|
320
|
+
// `key` normalises all three to a string ('a' / '1' / '3'), but
|
|
321
|
+
// `keyKind` keeps `{ '1': … }` (string) distinct from `{ 3: … }`
|
|
322
|
+
// (numeric) — a consumer that rejects numeric keys needs this.
|
|
323
|
+
expect(result.properties.map(p => p.key)).toEqual(['a', '1', '3'])
|
|
324
|
+
expect(result.properties.map(p => p.keyKind)).toEqual(['identifier', 'string', 'numeric'])
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
test('parses shorthand object literal, expanding `{ a }` to `a: a`', () => {
|
|
329
|
+
const result = parseExpression('({ a, b: 2 })')
|
|
330
|
+
expect(result.kind).toBe('object-literal')
|
|
331
|
+
if (result.kind === 'object-literal') {
|
|
332
|
+
expect(result.properties[0]).toMatchObject({
|
|
333
|
+
key: 'a', shorthand: true, value: { kind: 'identifier', name: 'a' },
|
|
334
|
+
})
|
|
335
|
+
expect(result.properties[1]).toMatchObject({ key: 'b', shorthand: false })
|
|
336
|
+
}
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
test('carries the raw numeric token (= ts NumericLiteral.text) (Roadmap A-3)', () => {
|
|
340
|
+
// `raw` is the TS `NumericLiteral.text` — the exact token the adapter's
|
|
341
|
+
// `tsLiteralToGo` already emits — so a structured lowering matches it
|
|
342
|
+
// byte-for-byte. TS normalises separators / radix / exponent in `.text`
|
|
343
|
+
// (`1_000`/`0x10`/`1e3` → decimal), which is precisely why carrying the
|
|
344
|
+
// token beats the lossy `parseFloat` `value` (e.g. `parseFloat('1_000')`
|
|
345
|
+
// is 1, not 1000).
|
|
346
|
+
for (const [src, value, raw] of [
|
|
347
|
+
['1', 1, '1'],
|
|
348
|
+
['3.14', 3.14, '3.14'],
|
|
349
|
+
['0x10', 16, '16'],
|
|
350
|
+
['1e3', 1000, '1000'],
|
|
351
|
+
['1_000', 1000, '1000'],
|
|
352
|
+
] as const) {
|
|
353
|
+
const r = parseExpression(src)
|
|
354
|
+
expect(r).toMatchObject({ kind: 'literal', literalType: 'number', value, raw })
|
|
355
|
+
}
|
|
356
|
+
// Non-numeric literals don't carry `raw` (their `value` is canonical).
|
|
357
|
+
expect((parseExpression("'x'") as { raw?: string }).raw).toBeUndefined()
|
|
358
|
+
expect((parseExpression('true') as { raw?: string }).raw).toBeUndefined()
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
test('falls through to unsupported for spread / computed-key object literals', () => {
|
|
362
|
+
// A spread member is not a plain map property — preserves the
|
|
363
|
+
// pre-A-1 `unsupported` behaviour (byte-identical).
|
|
364
|
+
expect(parseExpression('({ ...rest, a: 1 })').kind).toBe('unsupported')
|
|
365
|
+
// Computed key `[k]: v` can't resolve to a static name.
|
|
366
|
+
expect(parseExpression('({ [k]: 1 })').kind).toBe('unsupported')
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
test('a bare `{ … }` at statement position is a block, not an object literal', () => {
|
|
370
|
+
// TS parses a leading `{` as a block statement, so a non-parenthesised
|
|
371
|
+
// object literal string is `unsupported` (`Not an expression
|
|
372
|
+
// statement`). Real call sites always supply the parenthesised form.
|
|
373
|
+
expect(parseExpression('{ a: 1 }').kind).toBe('unsupported')
|
|
374
|
+
})
|
|
375
|
+
|
|
303
376
|
// Destructured filter param (#1443). The parser rewrites the
|
|
304
377
|
// shorthand binding `({done})` into the equivalent dotted-access
|
|
305
378
|
// form on a synthetic param (`_t.done`), so adapters can reuse
|
|
@@ -307,20 +380,21 @@ describe('expression-parser', () => {
|
|
|
307
380
|
// residual-object-accessor pipeline (#1384 territory).
|
|
308
381
|
test('lowers .filter(({done}) => done) to higher-order with synthetic param (#1443)', () => {
|
|
309
382
|
const result = parseExpression('todos().filter(({done}) => done)')
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
383
|
+
const cb = asCallbackMethodCall(result)
|
|
384
|
+
expect(cb).not.toBeNull()
|
|
385
|
+
expect(cb!.method).toBe('filter')
|
|
386
|
+
const param = cb!.arrow.params[0]
|
|
387
|
+
const predicate = cb!.arrow.body
|
|
388
|
+
// Synthetic param won't collide with `done` (the destructured
|
|
389
|
+
// local) or any name in the body.
|
|
390
|
+
expect(param).not.toBe('done')
|
|
391
|
+
// Predicate is rewritten to `<synthetic>.done`
|
|
392
|
+
expect(predicate.kind).toBe('member')
|
|
393
|
+
if (predicate.kind === 'member') {
|
|
394
|
+
expect(predicate.property).toBe('done')
|
|
395
|
+
expect(predicate.object.kind).toBe('identifier')
|
|
396
|
+
if (predicate.object.kind === 'identifier') {
|
|
397
|
+
expect(predicate.object.name).toBe(param)
|
|
324
398
|
}
|
|
325
399
|
}
|
|
326
400
|
})
|
|
@@ -331,12 +405,12 @@ describe('expression-parser', () => {
|
|
|
331
405
|
// original field name).
|
|
332
406
|
test('lowers .filter(({done: isDone}) => isDone) to higher-order with renamed destructure (#1443)', () => {
|
|
333
407
|
const result = parseExpression('todos().filter(({done: isDone}) => isDone)')
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
408
|
+
const cb = asCallbackMethodCall(result)
|
|
409
|
+
expect(cb).not.toBeNull()
|
|
410
|
+
const predicate = cb!.arrow.body
|
|
411
|
+
expect(predicate.kind).toBe('member')
|
|
412
|
+
if (predicate.kind === 'member') {
|
|
413
|
+
expect(predicate.property).toBe('done') // original field, not the local rename
|
|
340
414
|
}
|
|
341
415
|
})
|
|
342
416
|
|
|
@@ -348,47 +422,48 @@ describe('expression-parser', () => {
|
|
|
348
422
|
// (covered separately).
|
|
349
423
|
test('lowers .filter(({done = false}) => done) to higher-order with `??` default (#1531)', () => {
|
|
350
424
|
const result = parseExpression('todos().filter(({done = false}) => done)')
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
expect(result.predicate.right.kind).toBe('literal')
|
|
368
|
-
if (result.predicate.right.kind === 'literal') {
|
|
369
|
-
expect(result.predicate.right.value).toBe(false)
|
|
370
|
-
expect(result.predicate.right.literalType).toBe('boolean')
|
|
425
|
+
const cb = asCallbackMethodCall(result)
|
|
426
|
+
expect(cb).not.toBeNull()
|
|
427
|
+
const param = cb!.arrow.params[0]
|
|
428
|
+
const predicate = cb!.arrow.body
|
|
429
|
+
expect(cb!.method).toBe('filter')
|
|
430
|
+
expect(param).not.toBe('done')
|
|
431
|
+
// Predicate is `<synthetic>.done ?? false`.
|
|
432
|
+
expect(predicate.kind).toBe('logical')
|
|
433
|
+
if (predicate.kind === 'logical') {
|
|
434
|
+
expect(predicate.op).toBe('??')
|
|
435
|
+
expect(predicate.left.kind).toBe('member')
|
|
436
|
+
if (predicate.left.kind === 'member') {
|
|
437
|
+
expect(predicate.left.property).toBe('done')
|
|
438
|
+
expect(predicate.left.object.kind).toBe('identifier')
|
|
439
|
+
if (predicate.left.object.kind === 'identifier') {
|
|
440
|
+
expect(predicate.left.object.name).toBe(param)
|
|
371
441
|
}
|
|
372
442
|
}
|
|
443
|
+
expect(predicate.right.kind).toBe('literal')
|
|
444
|
+
if (predicate.right.kind === 'literal') {
|
|
445
|
+
expect(predicate.right.value).toBe(false)
|
|
446
|
+
expect(predicate.right.literalType).toBe('boolean')
|
|
447
|
+
}
|
|
373
448
|
}
|
|
374
449
|
})
|
|
375
450
|
|
|
376
451
|
test('lowers .filter(({count = 0}) => count > 5) — numeric default (#1531)', () => {
|
|
377
452
|
const result = parseExpression('items().filter(({count = 0}) => count > 5)')
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
453
|
+
const cb = asCallbackMethodCall(result)
|
|
454
|
+
expect(cb).not.toBeNull()
|
|
455
|
+
const predicate = cb!.arrow.body
|
|
456
|
+
// Predicate: `(_t.count ?? 0) > 5`.
|
|
457
|
+
expect(predicate.kind).toBe('binary')
|
|
458
|
+
if (predicate.kind === 'binary') {
|
|
459
|
+
expect(predicate.op).toBe('>')
|
|
460
|
+
expect(predicate.left.kind).toBe('logical')
|
|
461
|
+
if (predicate.left.kind === 'logical') {
|
|
462
|
+
expect(predicate.left.op).toBe('??')
|
|
463
|
+
expect(predicate.left.left.kind).toBe('member')
|
|
464
|
+
expect(predicate.left.right.kind).toBe('literal')
|
|
465
|
+
if (predicate.left.right.kind === 'literal') {
|
|
466
|
+
expect(predicate.left.right.value).toBe(0)
|
|
392
467
|
}
|
|
393
468
|
}
|
|
394
469
|
}
|
|
@@ -396,21 +471,21 @@ describe('expression-parser', () => {
|
|
|
396
471
|
|
|
397
472
|
test("lowers .filter(({name = 'anon'}) => name.startsWith('a')) — string default (#1531)", () => {
|
|
398
473
|
const result = parseExpression("items().filter(({name = 'anon'}) => name.startsWith('a'))")
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
474
|
+
const cb = asCallbackMethodCall(result)
|
|
475
|
+
expect(cb).not.toBeNull()
|
|
476
|
+
const predicate = cb!.arrow.body
|
|
477
|
+
// Predicate: `(_t.name ?? 'anon').startsWith('a')`. Since #1448
|
|
478
|
+
// Tier B, `.startsWith` lowers to an `array-method` node (it was
|
|
479
|
+
// a generic `call` before), so the receiver is on `.object`.
|
|
480
|
+
expect(predicate.kind).toBe('array-method')
|
|
481
|
+
if (predicate.kind === 'array-method') {
|
|
482
|
+
expect(predicate.method).toBe('startsWith')
|
|
483
|
+
expect(predicate.object.kind).toBe('logical')
|
|
484
|
+
if (predicate.object.kind === 'logical') {
|
|
485
|
+
expect(predicate.object.op).toBe('??')
|
|
486
|
+
expect(predicate.object.right.kind).toBe('literal')
|
|
487
|
+
if (predicate.object.right.kind === 'literal') {
|
|
488
|
+
expect(predicate.object.right.value).toBe('anon')
|
|
414
489
|
}
|
|
415
490
|
}
|
|
416
491
|
}
|
|
@@ -467,11 +542,21 @@ describe('expression-parser', () => {
|
|
|
467
542
|
}
|
|
468
543
|
})
|
|
469
544
|
|
|
470
|
-
test('.replace(/re/, repl)
|
|
545
|
+
test('.replace(/re/, repl) carries the regex form structurally; isSupported refuses it with the deferred reason (#1448 Tier B, #2039)', () => {
|
|
546
|
+
// The regex form is deferred, but its shape is carried as an
|
|
547
|
+
// `array-method` whose first arg is a `regex` node — so the Go ctor
|
|
548
|
+
// lowering can recover the trailing-slash pattern without re-parsing
|
|
549
|
+
// (#2039). Template use is still refused, via `isSupported`.
|
|
471
550
|
const result = parseExpression(`name().replace(/a/g, "b")`)
|
|
472
|
-
expect(result.kind).toBe('
|
|
473
|
-
if (result.kind === '
|
|
474
|
-
expect(result.
|
|
551
|
+
expect(result.kind).toBe('array-method')
|
|
552
|
+
if (result.kind === 'array-method') {
|
|
553
|
+
expect(result.method).toBe('replace')
|
|
554
|
+
expect(result.args[0].kind).toBe('regex')
|
|
555
|
+
}
|
|
556
|
+
const support = isSupported(result)
|
|
557
|
+
expect(support.supported).toBe(false)
|
|
558
|
+
if (!support.supported) {
|
|
559
|
+
expect(support.reason).toContain('regex form is deferred')
|
|
475
560
|
}
|
|
476
561
|
})
|
|
477
562
|
|
|
@@ -498,33 +583,31 @@ describe('expression-parser', () => {
|
|
|
498
583
|
|
|
499
584
|
test('lowers .filter(({label = `untitled-${suffix}`}) => label) — template-literal default (#1531)', () => {
|
|
500
585
|
const result = parseExpression('items().filter(({label = `untitled-${suffix}`}) => label)')
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
586
|
+
const cb = asCallbackMethodCall(result)
|
|
587
|
+
expect(cb).not.toBeNull()
|
|
588
|
+
const predicate = cb!.arrow.body
|
|
589
|
+
// Predicate: `_t.label ?? \`untitled-${suffix}\``.
|
|
590
|
+
expect(predicate.kind).toBe('logical')
|
|
591
|
+
if (predicate.kind === 'logical') {
|
|
592
|
+
expect(predicate.op).toBe('??')
|
|
593
|
+
expect(predicate.right.kind).toBe('template-literal')
|
|
509
594
|
}
|
|
510
595
|
})
|
|
511
596
|
|
|
512
597
|
test('lowers .every(({done = true}) => done) — defaults work on .every (#1531)', () => {
|
|
513
598
|
const result = parseExpression('items().every(({done = true}) => done)')
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
}
|
|
599
|
+
const cb = asCallbackMethodCall(result)
|
|
600
|
+
expect(cb).not.toBeNull()
|
|
601
|
+
expect(cb!.method).toBe('every')
|
|
602
|
+
expect(cb!.arrow.body.kind).toBe('logical')
|
|
519
603
|
})
|
|
520
604
|
|
|
521
605
|
test('lowers .find(({active = false}) => active) — defaults work on .find (#1531)', () => {
|
|
522
606
|
const result = parseExpression('items().find(({active = false}) => active)')
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
}
|
|
607
|
+
const cb = asCallbackMethodCall(result)
|
|
608
|
+
expect(cb).not.toBeNull()
|
|
609
|
+
expect(cb!.method).toBe('find')
|
|
610
|
+
expect(cb!.arrow.body.kind).toBe('logical')
|
|
528
611
|
})
|
|
529
612
|
|
|
530
613
|
// Semantic note (#1531): JS destructure defaults trigger only on
|
|
@@ -537,16 +620,16 @@ describe('expression-parser', () => {
|
|
|
537
620
|
// than a silent semantic shift.
|
|
538
621
|
test('uses `??` (not sentinel undefined check) — pins the null-triggers-default gap (#1531)', () => {
|
|
539
622
|
const result = parseExpression('items().filter(({done = false}) => done)')
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
623
|
+
const cb = asCallbackMethodCall(result)
|
|
624
|
+
expect(cb).not.toBeNull()
|
|
625
|
+
const predicate = cb!.arrow.body
|
|
626
|
+
// `??` => `null` ALSO triggers the default; this is the
|
|
627
|
+
// documented gap from JS destructure-default semantics, which
|
|
628
|
+
// only trigger on `undefined`. If this assertion fails, the
|
|
629
|
+
// rewrite shape changed — re-document the semantic difference.
|
|
630
|
+
expect(predicate.kind).toBe('logical')
|
|
631
|
+
if (predicate.kind === 'logical') {
|
|
632
|
+
expect(predicate.op).toBe('??')
|
|
550
633
|
}
|
|
551
634
|
})
|
|
552
635
|
|
|
@@ -556,15 +639,15 @@ describe('expression-parser', () => {
|
|
|
556
639
|
// `(_t.done ?? false)`.
|
|
557
640
|
test('lowers .filter(({done: isDone = false}) => isDone) — renamed + default (#1531)', () => {
|
|
558
641
|
const result = parseExpression('todos().filter(({done: isDone = false}) => isDone)')
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
642
|
+
const cb = asCallbackMethodCall(result)
|
|
643
|
+
expect(cb).not.toBeNull()
|
|
644
|
+
const predicate = cb!.arrow.body
|
|
645
|
+
expect(predicate.kind).toBe('logical')
|
|
646
|
+
if (predicate.kind === 'logical') {
|
|
647
|
+
expect(predicate.op).toBe('??')
|
|
648
|
+
expect(predicate.left.kind).toBe('member')
|
|
649
|
+
if (predicate.left.kind === 'member') {
|
|
650
|
+
expect(predicate.left.property).toBe('done') // field, not the rename
|
|
568
651
|
}
|
|
569
652
|
}
|
|
570
653
|
})
|
|
@@ -575,12 +658,12 @@ describe('expression-parser', () => {
|
|
|
575
658
|
// to validate).
|
|
576
659
|
test('lowers .filter(({a, ...rest}) => a) — unused rest is harmless (#1532)', () => {
|
|
577
660
|
const result = parseExpression('items().filter(({a, ...rest}) => a)')
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
661
|
+
const cb = asCallbackMethodCall(result)
|
|
662
|
+
expect(cb).not.toBeNull()
|
|
663
|
+
const predicate = cb!.arrow.body
|
|
664
|
+
expect(predicate.kind).toBe('member')
|
|
665
|
+
if (predicate.kind === 'member') {
|
|
666
|
+
expect(predicate.property).toBe('a')
|
|
584
667
|
}
|
|
585
668
|
})
|
|
586
669
|
|
|
@@ -590,31 +673,32 @@ describe('expression-parser', () => {
|
|
|
590
673
|
// would whenever `priority !== 'done'`.
|
|
591
674
|
test('lowers .filter(({done, ...rest}) => done && rest.priority > 0) — rest member access (#1532)', () => {
|
|
592
675
|
const result = parseExpression('items().filter(({done, ...rest}) => done && rest.priority > 0)')
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
676
|
+
const cb = asCallbackMethodCall(result)
|
|
677
|
+
expect(cb).not.toBeNull()
|
|
678
|
+
const param = cb!.arrow.params[0]
|
|
679
|
+
const predicate = cb!.arrow.body
|
|
680
|
+
expect(cb!.method).toBe('filter')
|
|
681
|
+
// Predicate: `_t.done && _t.priority > 0`.
|
|
682
|
+
expect(predicate.kind).toBe('logical')
|
|
683
|
+
if (predicate.kind === 'logical') {
|
|
684
|
+
expect(predicate.op).toBe('&&')
|
|
685
|
+
const left = predicate.left
|
|
686
|
+
expect(left.kind).toBe('member')
|
|
687
|
+
if (left.kind === 'member') {
|
|
688
|
+
expect(left.property).toBe('done')
|
|
689
|
+
if (left.object.kind === 'identifier') {
|
|
690
|
+
expect(left.object.name).toBe(param)
|
|
607
691
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
692
|
+
}
|
|
693
|
+
const right = predicate.right
|
|
694
|
+
expect(right.kind).toBe('binary')
|
|
695
|
+
if (right.kind === 'binary') {
|
|
696
|
+
const lhs = right.left
|
|
697
|
+
expect(lhs.kind).toBe('member')
|
|
698
|
+
if (lhs.kind === 'member') {
|
|
699
|
+
expect(lhs.property).toBe('priority')
|
|
700
|
+
if (lhs.object.kind === 'identifier') {
|
|
701
|
+
expect(lhs.object.name).toBe(param)
|
|
618
702
|
}
|
|
619
703
|
}
|
|
620
704
|
}
|
|
@@ -623,26 +707,27 @@ describe('expression-parser', () => {
|
|
|
623
707
|
|
|
624
708
|
test('lowers .filter(({a, ...r}) => r.x && r.y) — multiple rest accesses share the synthetic param (#1532)', () => {
|
|
625
709
|
const result = parseExpression('items().filter(({a, ...r}) => r.x && r.y)')
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
710
|
+
const cb = asCallbackMethodCall(result)
|
|
711
|
+
expect(cb).not.toBeNull()
|
|
712
|
+
const param = cb!.arrow.params[0]
|
|
713
|
+
const predicate = cb!.arrow.body
|
|
714
|
+
// Predicate: `_t.x && _t.y`.
|
|
715
|
+
expect(predicate.kind).toBe('logical')
|
|
716
|
+
if (predicate.kind === 'logical') {
|
|
717
|
+
const left = predicate.left
|
|
718
|
+
expect(left.kind).toBe('member')
|
|
719
|
+
if (left.kind === 'member') {
|
|
720
|
+
expect(left.property).toBe('x')
|
|
721
|
+
if (left.object.kind === 'identifier') {
|
|
722
|
+
expect(left.object.name).toBe(param)
|
|
638
723
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
724
|
+
}
|
|
725
|
+
const right = predicate.right
|
|
726
|
+
expect(right.kind).toBe('member')
|
|
727
|
+
if (right.kind === 'member') {
|
|
728
|
+
expect(right.property).toBe('y')
|
|
729
|
+
if (right.object.kind === 'identifier') {
|
|
730
|
+
expect(right.object.name).toBe(param)
|
|
646
731
|
}
|
|
647
732
|
}
|
|
648
733
|
}
|
|
@@ -650,13 +735,13 @@ describe('expression-parser', () => {
|
|
|
650
735
|
|
|
651
736
|
test('lowers .every(({a, ...rest}) => rest.x) — rest rewrite applies across higher-order methods (#1532)', () => {
|
|
652
737
|
const result = parseExpression('items().every(({a, ...rest}) => rest.x)')
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
738
|
+
const cb = asCallbackMethodCall(result)
|
|
739
|
+
expect(cb).not.toBeNull()
|
|
740
|
+
const predicate = cb!.arrow.body
|
|
741
|
+
expect(cb!.method).toBe('every')
|
|
742
|
+
expect(predicate.kind).toBe('member')
|
|
743
|
+
if (predicate.kind === 'member') {
|
|
744
|
+
expect(predicate.property).toBe('x')
|
|
660
745
|
}
|
|
661
746
|
})
|
|
662
747
|
|
|
@@ -667,8 +752,11 @@ describe('expression-parser', () => {
|
|
|
667
752
|
// `_t.done` (which would return the value, masking the mistake).
|
|
668
753
|
test('rejects .filter(({done, ...rest}) => rest.done) — rest key collides with declared field (#1532)', () => {
|
|
669
754
|
const result = parseExpression('items().filter(({done, ...rest}) => rest.done)')
|
|
670
|
-
// Falls back to plain `call` — adapter surfaces BF021.
|
|
755
|
+
// Falls back to plain `call` — adapter surfaces BF021. The
|
|
756
|
+
// destructure rewrite was refused, so it is NOT a lowerable
|
|
757
|
+
// callback call.
|
|
671
758
|
expect(result.kind).toBe('call')
|
|
759
|
+
expect(asCallbackMethodCall(result)).toBeNull()
|
|
672
760
|
})
|
|
673
761
|
|
|
674
762
|
// Mode A typed error via RENAMED key (#1532 review). The rest
|
|
@@ -679,6 +767,7 @@ describe('expression-parser', () => {
|
|
|
679
767
|
test('rejects .filter(({done: d, ...rest}) => rest.done) — renamed key collision (#1532 review)', () => {
|
|
680
768
|
const result = parseExpression('items().filter(({done: d, ...rest}) => rest.done)')
|
|
681
769
|
expect(result.kind).toBe('call')
|
|
770
|
+
expect(asCallbackMethodCall(result)).toBeNull()
|
|
682
771
|
})
|
|
683
772
|
|
|
684
773
|
// Mode A typed error via NESTED-PATTERN slot (#1532 review).
|
|
@@ -690,6 +779,7 @@ describe('expression-parser', () => {
|
|
|
690
779
|
test('rejects .filter(({user: {name}, ...rest}) => rest.user) — nested-pattern outer key collision (#1532 review)', () => {
|
|
691
780
|
const result = parseExpression('items().filter(({user: {name}, ...rest}) => rest.user)')
|
|
692
781
|
expect(result.kind).toBe('call')
|
|
782
|
+
expect(asCallbackMethodCall(result)).toBeNull()
|
|
693
783
|
})
|
|
694
784
|
|
|
695
785
|
// Inner-arrow parameter shadowing (#1532 review). The outer
|
|
@@ -703,13 +793,12 @@ describe('expression-parser', () => {
|
|
|
703
793
|
// reference is `rest.x` (Mode A member access).
|
|
704
794
|
test('lowers .filter(({a, ...rest}) => rest.x.map((rest) => rest.y).length > 0) — inner-arrow param shadows outer rest (#1532 review)', () => {
|
|
705
795
|
const result = parseExpression('items().filter(({a, ...rest}) => rest.x.map((rest) => rest.y).length > 0)')
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
}
|
|
796
|
+
const cb = asCallbackMethodCall(result)
|
|
797
|
+
expect(cb).not.toBeNull()
|
|
798
|
+
expect(cb!.method).toBe('filter')
|
|
799
|
+
// Predicate is the outer body rewritten — confirms validation
|
|
800
|
+
// didn't refuse on the inner-scope `rest` reference.
|
|
801
|
+
expect(cb!.arrow.body.kind).toBe('binary')
|
|
713
802
|
})
|
|
714
803
|
|
|
715
804
|
// Mode B (#1532): rest used as a call argument can't be lowered
|
|
@@ -718,11 +807,13 @@ describe('expression-parser', () => {
|
|
|
718
807
|
test('rejects .filter(({a, ...rest}) => Object.keys(rest).length > 0) — rest passed to call (#1532)', () => {
|
|
719
808
|
const result = parseExpression('items().filter(({a, ...rest}) => Object.keys(rest).length > 0)')
|
|
720
809
|
expect(result.kind).toBe('call')
|
|
810
|
+
expect(asCallbackMethodCall(result)).toBeNull()
|
|
721
811
|
})
|
|
722
812
|
|
|
723
813
|
test('rejects .filter(({a, ...rest}) => fn(rest)) — rest passed as bare arg (#1532)', () => {
|
|
724
814
|
const result = parseExpression('items().filter(({a, ...rest}) => fn(rest))')
|
|
725
815
|
expect(result.kind).toBe('call')
|
|
816
|
+
expect(asCallbackMethodCall(result)).toBeNull()
|
|
726
817
|
})
|
|
727
818
|
|
|
728
819
|
// Mode B (#1532): bare `rest` as the arrow's return value. No
|
|
@@ -731,6 +822,7 @@ describe('expression-parser', () => {
|
|
|
731
822
|
test('rejects .filter(({a, ...rest}) => rest) — rest as bare return value (#1532)', () => {
|
|
732
823
|
const result = parseExpression('items().filter(({a, ...rest}) => rest)')
|
|
733
824
|
expect(result.kind).toBe('call')
|
|
825
|
+
expect(asCallbackMethodCall(result)).toBeNull()
|
|
734
826
|
})
|
|
735
827
|
|
|
736
828
|
// Function-expression form with a destructured param (#1532).
|
|
@@ -742,6 +834,7 @@ describe('expression-parser', () => {
|
|
|
742
834
|
test('rejects .filter(function ({a, ...rest}) { return rest }) — function-expression form refused upstream (#1532)', () => {
|
|
743
835
|
const result = parseExpression('items().filter(function ({a, ...rest}) { return rest })')
|
|
744
836
|
expect(result.kind).toBe('call')
|
|
837
|
+
expect(asCallbackMethodCall(result)).toBeNull()
|
|
745
838
|
})
|
|
746
839
|
|
|
747
840
|
// Method call on rest (#1532 review). `rest.foo()` would lower
|
|
@@ -753,6 +846,7 @@ describe('expression-parser', () => {
|
|
|
753
846
|
test('rejects .filter(({a, ...rest}) => rest.foo()) — method call on rest (#1532 review)', () => {
|
|
754
847
|
const result = parseExpression('items().filter(({a, ...rest}) => rest.foo())')
|
|
755
848
|
expect(result.kind).toBe('call')
|
|
849
|
+
expect(asCallbackMethodCall(result)).toBeNull()
|
|
756
850
|
})
|
|
757
851
|
|
|
758
852
|
// Same for method-call-with-args — confirms the dedicated branch
|
|
@@ -760,6 +854,7 @@ describe('expression-parser', () => {
|
|
|
760
854
|
test('rejects .filter(({a, ...rest}) => rest.hasOwnProperty("k")) — method call on rest with args (#1532 review)', () => {
|
|
761
855
|
const result = parseExpression('items().filter(({a, ...rest}) => rest.hasOwnProperty("k"))')
|
|
762
856
|
expect(result.kind).toBe('call')
|
|
857
|
+
expect(asCallbackMethodCall(result)).toBeNull()
|
|
763
858
|
})
|
|
764
859
|
|
|
765
860
|
// Computed rest access with a literal key still refuses (#1532):
|
|
@@ -769,6 +864,7 @@ describe('expression-parser', () => {
|
|
|
769
864
|
test('rejects .filter(({a, ...rest}) => rest[0]) — computed rest access with literal key (#1532)', () => {
|
|
770
865
|
const result = parseExpression('items().filter(({a, ...rest}) => rest[0])')
|
|
771
866
|
expect(result.kind).toBe('call')
|
|
867
|
+
expect(asCallbackMethodCall(result)).toBeNull()
|
|
772
868
|
})
|
|
773
869
|
|
|
774
870
|
// Rest at a nested destructure level (#1532 out-of-scope) — we
|
|
@@ -777,6 +873,7 @@ describe('expression-parser', () => {
|
|
|
777
873
|
test('rejects .filter(({user: {name, ...rest}}) => rest.email) — nested rest (#1532)', () => {
|
|
778
874
|
const result = parseExpression('items().filter(({user: {name, ...rest}}) => rest.email)')
|
|
779
875
|
expect(result.kind).toBe('call')
|
|
876
|
+
expect(asCallbackMethodCall(result)).toBeNull()
|
|
780
877
|
})
|
|
781
878
|
|
|
782
879
|
// Default value + rest composition (#1532 review). #1531's leaf
|
|
@@ -785,27 +882,27 @@ describe('expression-parser', () => {
|
|
|
785
882
|
// refactor of either feature can't silently break the cross.
|
|
786
883
|
test('lowers .filter(({done = false, ...rest}) => done && rest.priority > 0) — default + rest compose (#1532 review)', () => {
|
|
787
884
|
const result = parseExpression('items().filter(({done = false, ...rest}) => done && rest.priority > 0)')
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
885
|
+
const cb = asCallbackMethodCall(result)
|
|
886
|
+
expect(cb).not.toBeNull()
|
|
887
|
+
const predicate = cb!.arrow.body
|
|
888
|
+
// Predicate: `(_t.done ?? false) && _t.priority > 0`.
|
|
889
|
+
expect(predicate.kind).toBe('logical')
|
|
890
|
+
if (predicate.kind === 'logical') {
|
|
891
|
+
// Left: `_t.done ?? false`.
|
|
892
|
+
expect(predicate.left.kind).toBe('logical')
|
|
893
|
+
if (predicate.left.kind === 'logical') {
|
|
894
|
+
expect(predicate.left.op).toBe('??')
|
|
895
|
+
expect(predicate.left.left.kind).toBe('member')
|
|
896
|
+
if (predicate.left.left.kind === 'member') {
|
|
897
|
+
expect(predicate.left.left.property).toBe('done')
|
|
801
898
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
899
|
+
}
|
|
900
|
+
// Right: `_t.priority > 0` — confirms the rest rewrite ran.
|
|
901
|
+
expect(predicate.right.kind).toBe('binary')
|
|
902
|
+
if (predicate.right.kind === 'binary') {
|
|
903
|
+
expect(predicate.right.left.kind).toBe('member')
|
|
904
|
+
if (predicate.right.left.kind === 'member') {
|
|
905
|
+
expect(predicate.right.left.property).toBe('priority')
|
|
809
906
|
}
|
|
810
907
|
}
|
|
811
908
|
}
|
|
@@ -818,22 +915,23 @@ describe('expression-parser', () => {
|
|
|
818
915
|
// source key (`priority`). Lowers cleanly.
|
|
819
916
|
test('lowers .filter(({done: d, ...rest}) => d && rest.priority > 0) — renamed leaf + Mode A (#1532 review)', () => {
|
|
820
917
|
const result = parseExpression('items().filter(({done: d, ...rest}) => d && rest.priority > 0)')
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
918
|
+
const cb = asCallbackMethodCall(result)
|
|
919
|
+
expect(cb).not.toBeNull()
|
|
920
|
+
const param = cb!.arrow.params[0]
|
|
921
|
+
const predicate = cb!.arrow.body
|
|
922
|
+
expect(predicate.kind).toBe('logical')
|
|
923
|
+
if (predicate.kind === 'logical') {
|
|
924
|
+
// Left: `_t.done` (rename rewrites to the SOURCE key).
|
|
925
|
+
const left = predicate.left
|
|
926
|
+
expect(left.kind).toBe('member')
|
|
927
|
+
if (left.kind === 'member') {
|
|
928
|
+
expect(left.property).toBe('done')
|
|
929
|
+
if (left.object.kind === 'identifier') {
|
|
930
|
+
expect(left.object.name).toBe(param)
|
|
833
931
|
}
|
|
834
|
-
// Right: `_t.priority > 0` — rest member access.
|
|
835
|
-
expect(result.predicate.right.kind).toBe('binary')
|
|
836
932
|
}
|
|
933
|
+
// Right: `_t.priority > 0` — rest member access.
|
|
934
|
+
expect(predicate.right.kind).toBe('binary')
|
|
837
935
|
}
|
|
838
936
|
})
|
|
839
937
|
|
|
@@ -846,6 +944,7 @@ describe('expression-parser', () => {
|
|
|
846
944
|
test('rejects .filter(({a, ...rest}) => rest.x === fn(rest)) — Mode A + Mode B mixed (#1532 review)', () => {
|
|
847
945
|
const result = parseExpression('items().filter(({a, ...rest}) => rest.x === fn(rest))')
|
|
848
946
|
expect(result.kind).toBe('call')
|
|
947
|
+
expect(asCallbackMethodCall(result)).toBeNull()
|
|
849
948
|
})
|
|
850
949
|
|
|
851
950
|
// Synthetic param collision with closure-captured `_t` (#1532
|
|
@@ -855,11 +954,10 @@ describe('expression-parser', () => {
|
|
|
855
954
|
// silently shadow the closure capture.
|
|
856
955
|
test('picks a non-colliding synthetic param when rest body references `_t` (#1532 review)', () => {
|
|
857
956
|
const result = parseExpression('items().filter(({a, ...rest}) => rest.x === _t)')
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
}
|
|
957
|
+
const cb = asCallbackMethodCall(result)
|
|
958
|
+
expect(cb).not.toBeNull()
|
|
959
|
+
expect(cb!.arrow.params[0]).not.toBe('_t')
|
|
960
|
+
expect(cb!.arrow.params[0]).toMatch(/^_t_+$/)
|
|
863
961
|
})
|
|
864
962
|
|
|
865
963
|
// Cross-method coverage for the rest rewrite (#1532). The arrow
|
|
@@ -869,20 +967,23 @@ describe('expression-parser', () => {
|
|
|
869
967
|
// remaining three.
|
|
870
968
|
test('lowers .some(({a, ...rest}) => rest.x) — cross-method (#1532 review)', () => {
|
|
871
969
|
const result = parseExpression('items().some(({a, ...rest}) => rest.x)')
|
|
872
|
-
|
|
873
|
-
|
|
970
|
+
const cb = asCallbackMethodCall(result)
|
|
971
|
+
expect(cb).not.toBeNull()
|
|
972
|
+
expect(cb!.method).toBe('some')
|
|
874
973
|
})
|
|
875
974
|
|
|
876
975
|
test('lowers .find(({a, ...rest}) => rest.x) — cross-method (#1532 review)', () => {
|
|
877
976
|
const result = parseExpression('items().find(({a, ...rest}) => rest.x)')
|
|
878
|
-
|
|
879
|
-
|
|
977
|
+
const cb = asCallbackMethodCall(result)
|
|
978
|
+
expect(cb).not.toBeNull()
|
|
979
|
+
expect(cb!.method).toBe('find')
|
|
880
980
|
})
|
|
881
981
|
|
|
882
982
|
test('lowers .findIndex(({a, ...rest}) => rest.x) — cross-method (#1532 review)', () => {
|
|
883
983
|
const result = parseExpression('items().findIndex(({a, ...rest}) => rest.x)')
|
|
884
|
-
|
|
885
|
-
|
|
984
|
+
const cb = asCallbackMethodCall(result)
|
|
985
|
+
expect(cb).not.toBeNull()
|
|
986
|
+
expect(cb!.method).toBe('findIndex')
|
|
886
987
|
})
|
|
887
988
|
|
|
888
989
|
// Nested destructure with a default on an INNER LEAF composes
|
|
@@ -894,26 +995,26 @@ describe('expression-parser', () => {
|
|
|
894
995
|
// mechanics produce a correct lowering for free.
|
|
895
996
|
test('lowers .filter(({user: {name = "anon"}}) => name) — inner-leaf default composes (#1531)', () => {
|
|
896
997
|
const result = parseExpression('items().filter(({user: {name = "anon"}}) => name)')
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
}
|
|
913
|
-
if (result.predicate.right.kind === 'literal') {
|
|
914
|
-
expect(result.predicate.right.value).toBe('anon')
|
|
998
|
+
const cb = asCallbackMethodCall(result)
|
|
999
|
+
expect(cb).not.toBeNull()
|
|
1000
|
+
const predicate = cb!.arrow.body
|
|
1001
|
+
// Predicate: `_t.user.name ?? "anon"`.
|
|
1002
|
+
expect(predicate.kind).toBe('logical')
|
|
1003
|
+
if (predicate.kind === 'logical') {
|
|
1004
|
+
expect(predicate.op).toBe('??')
|
|
1005
|
+
// Walk: `_t.user.name`.
|
|
1006
|
+
const lhs = predicate.left
|
|
1007
|
+
expect(lhs.kind).toBe('member')
|
|
1008
|
+
if (lhs.kind === 'member') {
|
|
1009
|
+
expect(lhs.property).toBe('name')
|
|
1010
|
+
expect(lhs.object.kind).toBe('member')
|
|
1011
|
+
if (lhs.object.kind === 'member') {
|
|
1012
|
+
expect(lhs.object.property).toBe('user')
|
|
915
1013
|
}
|
|
916
1014
|
}
|
|
1015
|
+
if (predicate.right.kind === 'literal') {
|
|
1016
|
+
expect(predicate.right.value).toBe('anon')
|
|
1017
|
+
}
|
|
917
1018
|
}
|
|
918
1019
|
})
|
|
919
1020
|
|
|
@@ -925,6 +1026,7 @@ describe('expression-parser', () => {
|
|
|
925
1026
|
test('rejects .filter(({user: {name} = {}}) => name) — default on nested-pattern slot (#1531)', () => {
|
|
926
1027
|
const result = parseExpression('items().filter(({user: {name} = {}}) => name)')
|
|
927
1028
|
expect(result.kind).toBe('call')
|
|
1029
|
+
expect(asCallbackMethodCall(result)).toBeNull()
|
|
928
1030
|
})
|
|
929
1031
|
|
|
930
1032
|
// Cross-binding default reference (`{ a, b = a }`) — JS resolves
|
|
@@ -935,6 +1037,7 @@ describe('expression-parser', () => {
|
|
|
935
1037
|
test('rejects .filter(({a, b = a}) => b) — default refs another destructured binding (#1536)', () => {
|
|
936
1038
|
const result = parseExpression('items().filter(({a, b = a}) => b)')
|
|
937
1039
|
expect(result.kind).toBe('call')
|
|
1040
|
+
expect(asCallbackMethodCall(result)).toBeNull()
|
|
938
1041
|
})
|
|
939
1042
|
|
|
940
1043
|
// Side-effecting default (`{ x = getX() }`) — the rewrite duplicates
|
|
@@ -945,11 +1048,13 @@ describe('expression-parser', () => {
|
|
|
945
1048
|
test('rejects .filter(({x = getX()}) => x + x) — default contains a call (#1536)', () => {
|
|
946
1049
|
const result = parseExpression('items().filter(({x = getX()}) => x + x)')
|
|
947
1050
|
expect(result.kind).toBe('call')
|
|
1051
|
+
expect(asCallbackMethodCall(result)).toBeNull()
|
|
948
1052
|
})
|
|
949
1053
|
|
|
950
1054
|
test('rejects .filter(({xs = arr.slice()}) => xs) — default contains array-method (#1536)', () => {
|
|
951
1055
|
const result = parseExpression('items().filter(({xs = arr.slice()}) => xs)')
|
|
952
1056
|
expect(result.kind).toBe('call')
|
|
1057
|
+
expect(asCallbackMethodCall(result)).toBeNull()
|
|
953
1058
|
})
|
|
954
1059
|
|
|
955
1060
|
// Pure shapes that DO compose with the inline rewrite — these
|
|
@@ -958,13 +1063,13 @@ describe('expression-parser', () => {
|
|
|
958
1063
|
// `fallback` resolves to outer scope; that's fine — duplicating
|
|
959
1064
|
// a bare identifier read has no semantic cost.
|
|
960
1065
|
const result = parseExpression('items().filter(({x = fallback}) => x)')
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1066
|
+
const cb = asCallbackMethodCall(result)
|
|
1067
|
+
expect(cb).not.toBeNull()
|
|
1068
|
+
const predicate = cb!.arrow.body
|
|
1069
|
+
expect(predicate.kind).toBe('logical')
|
|
1070
|
+
if (predicate.kind === 'logical') {
|
|
1071
|
+
expect(predicate.op).toBe('??')
|
|
1072
|
+
expect(predicate.right.kind).toBe('identifier')
|
|
968
1073
|
}
|
|
969
1074
|
})
|
|
970
1075
|
|
|
@@ -973,12 +1078,12 @@ describe('expression-parser', () => {
|
|
|
973
1078
|
// side effects); duplicating across reference sites is harmless
|
|
974
1079
|
// for the common case.
|
|
975
1080
|
const result = parseExpression('items().filter(({x = config.fallback}) => x)')
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1081
|
+
const cb = asCallbackMethodCall(result)
|
|
1082
|
+
expect(cb).not.toBeNull()
|
|
1083
|
+
const predicate = cb!.arrow.body
|
|
1084
|
+
expect(predicate.kind).toBe('logical')
|
|
1085
|
+
if (predicate.kind === 'logical') {
|
|
1086
|
+
expect(predicate.right.kind).toBe('member')
|
|
982
1087
|
}
|
|
983
1088
|
})
|
|
984
1089
|
|
|
@@ -990,26 +1095,27 @@ describe('expression-parser', () => {
|
|
|
990
1095
|
// what the user could have written by hand.
|
|
991
1096
|
test('lowers .filter(({user: {name}}) => …) with nested destructure (#1530)', () => {
|
|
992
1097
|
const result = parseExpression("items().filter(({user: {name}}) => name === 'alice')")
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1098
|
+
const cb = asCallbackMethodCall(result)
|
|
1099
|
+
expect(cb).not.toBeNull()
|
|
1100
|
+
const param = cb!.arrow.params[0]
|
|
1101
|
+
const predicate = cb!.arrow.body
|
|
1102
|
+
expect(cb!.method).toBe('filter')
|
|
1103
|
+
expect(param).not.toBe('name')
|
|
1104
|
+
// Predicate: <_t>.user.name === 'alice'
|
|
1105
|
+
expect(predicate.kind).toBe('binary')
|
|
1106
|
+
if (predicate.kind === 'binary') {
|
|
1107
|
+
expect(predicate.op).toBe('===')
|
|
1108
|
+
// Walk the left-leaning chain: `_t.user.name`.
|
|
1109
|
+
const lhs = predicate.left
|
|
1110
|
+
expect(lhs.kind).toBe('member')
|
|
1111
|
+
if (lhs.kind === 'member') {
|
|
1112
|
+
expect(lhs.property).toBe('name')
|
|
1113
|
+
expect(lhs.object.kind).toBe('member')
|
|
1114
|
+
if (lhs.object.kind === 'member') {
|
|
1115
|
+
expect(lhs.object.property).toBe('user')
|
|
1116
|
+
expect(lhs.object.object.kind).toBe('identifier')
|
|
1117
|
+
if (lhs.object.object.kind === 'identifier') {
|
|
1118
|
+
expect(lhs.object.object.name).toBe(param)
|
|
1013
1119
|
}
|
|
1014
1120
|
}
|
|
1015
1121
|
}
|
|
@@ -1018,21 +1124,20 @@ describe('expression-parser', () => {
|
|
|
1018
1124
|
|
|
1019
1125
|
test('lowers .filter(({a: {b: {c}}}) => c) with doubly-nested destructure (#1530)', () => {
|
|
1020
1126
|
const result = parseExpression('items().filter(({a: {b: {c}}}) => c)')
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
}
|
|
1127
|
+
const cb = asCallbackMethodCall(result)
|
|
1128
|
+
expect(cb).not.toBeNull()
|
|
1129
|
+
// Predicate: `_t.a.b.c`.
|
|
1130
|
+
let node = cb!.arrow.body
|
|
1131
|
+
const expected = ['c', 'b', 'a']
|
|
1132
|
+
for (const property of expected) {
|
|
1133
|
+
expect(node.kind).toBe('member')
|
|
1134
|
+
if (node.kind !== 'member') return
|
|
1135
|
+
expect(node.property).toBe(property)
|
|
1136
|
+
node = node.object
|
|
1137
|
+
}
|
|
1138
|
+
expect(node.kind).toBe('identifier')
|
|
1139
|
+
if (node.kind === 'identifier') {
|
|
1140
|
+
expect(node.name).toBe(cb!.arrow.params[0])
|
|
1036
1141
|
}
|
|
1037
1142
|
})
|
|
1038
1143
|
|
|
@@ -1041,16 +1146,16 @@ describe('expression-parser', () => {
|
|
|
1041
1146
|
// substitutes `n` (body reference) with `_t.user.name` (original
|
|
1042
1147
|
// field path).
|
|
1043
1148
|
const result = parseExpression('items().filter(({user: {name: n}}) => n)')
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1149
|
+
const cb = asCallbackMethodCall(result)
|
|
1150
|
+
expect(cb).not.toBeNull()
|
|
1151
|
+
const predicate = cb!.arrow.body
|
|
1152
|
+
expect(cb!.arrow.params[0]).not.toBe('n')
|
|
1153
|
+
expect(predicate.kind).toBe('member')
|
|
1154
|
+
if (predicate.kind === 'member') {
|
|
1155
|
+
expect(predicate.property).toBe('name') // field, not the rename
|
|
1156
|
+
expect(predicate.object.kind).toBe('member')
|
|
1157
|
+
if (predicate.object.kind === 'member') {
|
|
1158
|
+
expect(predicate.object.property).toBe('user')
|
|
1054
1159
|
}
|
|
1055
1160
|
}
|
|
1056
1161
|
})
|
|
@@ -1061,8 +1166,10 @@ describe('expression-parser', () => {
|
|
|
1061
1166
|
// lowering it via the object-destructure path (#1530).
|
|
1062
1167
|
test('rejects .filter(({a: [x]}) => x) — array binding inside object destructure (#1530)', () => {
|
|
1063
1168
|
const result = parseExpression('items().filter(({a: [x]}) => x)')
|
|
1064
|
-
// Falls through to plain `call` — adapter surfaces BF101.
|
|
1169
|
+
// Falls through to plain `call` — adapter surfaces BF101. The
|
|
1170
|
+
// rewrite was refused, so it is NOT a lowerable callback call.
|
|
1065
1171
|
expect(result.kind).toBe('call')
|
|
1172
|
+
expect(asCallbackMethodCall(result)).toBeNull()
|
|
1066
1173
|
})
|
|
1067
1174
|
|
|
1068
1175
|
// Non-identifier property names (`{ 'x': y }`, `{ 0: y }`) used to
|
|
@@ -1073,11 +1180,13 @@ describe('expression-parser', () => {
|
|
|
1073
1180
|
test('rejects .filter(({ "x": y }) => y) — string-literal key in destructure (#1530)', () => {
|
|
1074
1181
|
const result = parseExpression("items().filter(({ 'x': y }) => y)")
|
|
1075
1182
|
expect(result.kind).toBe('call')
|
|
1183
|
+
expect(asCallbackMethodCall(result)).toBeNull()
|
|
1076
1184
|
})
|
|
1077
1185
|
|
|
1078
1186
|
test('rejects .filter(({ 0: y }) => y) — numeric-literal key in destructure (#1530)', () => {
|
|
1079
1187
|
const result = parseExpression('items().filter(({ 0: y }) => y)')
|
|
1080
1188
|
expect(result.kind).toBe('call')
|
|
1189
|
+
expect(asCallbackMethodCall(result)).toBeNull()
|
|
1081
1190
|
})
|
|
1082
1191
|
|
|
1083
1192
|
// Synthetic param collision — body references a free `_t` AND the
|
|
@@ -1088,11 +1197,10 @@ describe('expression-parser', () => {
|
|
|
1088
1197
|
// the synthetic param must NOT be `_t`, otherwise the rewrite
|
|
1089
1198
|
// would silently shadow it.
|
|
1090
1199
|
const result = parseExpression('items().filter(({user: {name}}) => name === _t)')
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
}
|
|
1200
|
+
const cb = asCallbackMethodCall(result)
|
|
1201
|
+
expect(cb).not.toBeNull()
|
|
1202
|
+
expect(cb!.arrow.params[0]).not.toBe('_t')
|
|
1203
|
+
expect(cb!.arrow.params[0]).toMatch(/^_t_+$/)
|
|
1096
1204
|
})
|
|
1097
1205
|
|
|
1098
1206
|
// Function-keyword filter callback (#1443): `function (x) { return
|
|
@@ -1101,16 +1209,36 @@ describe('expression-parser', () => {
|
|
|
1101
1209
|
// `(x) => x.done`.
|
|
1102
1210
|
test('lowers .filter(function(x) { return x.done }) to higher-order (#1443)', () => {
|
|
1103
1211
|
const result = parseExpression('todos().filter(function (x) { return x.done })')
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
}
|
|
1212
|
+
const cb = asCallbackMethodCall(result)
|
|
1213
|
+
expect(cb).not.toBeNull()
|
|
1214
|
+
expect(cb!.arrow.params[0]).toBe('x')
|
|
1215
|
+
expect(cb!.arrow.body.kind).toBe('member')
|
|
1109
1216
|
})
|
|
1110
1217
|
|
|
1111
|
-
|
|
1218
|
+
// #2040: a value-producing multi-statement block body (pure `const`
|
|
1219
|
+
// bindings + a terminal `return`) is normalised to a single expression via
|
|
1220
|
+
// let-inline, so the higher-order detector recognises it. Previously the
|
|
1221
|
+
// "only single-`return`" restriction refused it.
|
|
1222
|
+
test('folds function-keyword filter with let-inline block body (#2040)', () => {
|
|
1112
1223
|
const result = parseExpression('todos().filter(function (x) { const y = x; return y.done })')
|
|
1224
|
+
const cb = asCallbackMethodCall(result)
|
|
1225
|
+
expect(cb).not.toBeNull()
|
|
1226
|
+
expect(cb!.arrow.params[0]).toBe('x')
|
|
1227
|
+
// `const y = x; return y.done` inlines to `x.done`.
|
|
1228
|
+
expect(cb!.arrow.body).toEqual({
|
|
1229
|
+
kind: 'member',
|
|
1230
|
+
object: { kind: 'identifier', name: 'x' },
|
|
1231
|
+
property: 'done',
|
|
1232
|
+
computed: false,
|
|
1233
|
+
})
|
|
1234
|
+
})
|
|
1235
|
+
|
|
1236
|
+
// #2040: an imperative block body (local re-assignment / mutation) has no
|
|
1237
|
+
// value-position lowering and stays `unsupported`.
|
|
1238
|
+
test('rejects function-keyword filter with imperative block body (#2040)', () => {
|
|
1239
|
+
const result = parseExpression('todos().filter(function (x) { let y = 0; y = x.n; return y })')
|
|
1113
1240
|
expect(result.kind).toBe('call')
|
|
1241
|
+
expect(asCallbackMethodCall(result)).toBeNull()
|
|
1114
1242
|
})
|
|
1115
1243
|
})
|
|
1116
1244
|
|
|
@@ -1270,13 +1398,19 @@ describe('expression-parser', () => {
|
|
|
1270
1398
|
expect(result.reason).toContain('Standalone arrow functions')
|
|
1271
1399
|
})
|
|
1272
1400
|
|
|
1273
|
-
test('nested higher-order methods
|
|
1274
|
-
//
|
|
1275
|
-
//
|
|
1401
|
+
test('nested higher-order methods parse generically (lowerability deferred to the adapter)', () => {
|
|
1402
|
+
// items().filter(x => x.items().filter(y => y.done).length > 0)
|
|
1403
|
+
// The parser no longer gates nested higher-order callbacks (#2018
|
|
1404
|
+
// P5): the outer call is recognised as a generic callback method
|
|
1405
|
+
// call, and the inner callback survives inside the predicate body.
|
|
1406
|
+
// The "is it lowerable" decision now lives in the adapter, not the
|
|
1407
|
+
// parser/`isSupported` layer.
|
|
1276
1408
|
const expr = parseExpression('items().filter(x => x.items().filter(y => y.done).length > 0)')
|
|
1277
|
-
const
|
|
1278
|
-
expect(
|
|
1279
|
-
expect(
|
|
1409
|
+
const cb = asCallbackMethodCall(expr)
|
|
1410
|
+
expect(cb).not.toBeNull()
|
|
1411
|
+
expect(cb!.method).toBe('filter')
|
|
1412
|
+
// The nested higher-order call is still present in the predicate body.
|
|
1413
|
+
expect(containsHigherOrder(cb!.arrow.body)).toBe(true)
|
|
1280
1414
|
})
|
|
1281
1415
|
})
|
|
1282
1416
|
|
|
@@ -1578,9 +1712,29 @@ describe('expression-parser — .flat(depth?) lowering (#1448 Tier C)', () => {
|
|
|
1578
1712
|
})
|
|
1579
1713
|
|
|
1580
1714
|
describe('expression-parser — .flatMap(fn) projection (#1448 Tier C)', () => {
|
|
1581
|
-
//
|
|
1582
|
-
//
|
|
1583
|
-
|
|
1715
|
+
// The structured `flatMapOp` projection catalogue is gone (#2018 P5):
|
|
1716
|
+
// `.flatMap` now parses to a generic callback `call`, and the projection
|
|
1717
|
+
// shape lives in the callback arrow body. These helpers assert the
|
|
1718
|
+
// SAME projection intent against the generic arrow body so the catalogue
|
|
1719
|
+
// coverage is preserved.
|
|
1720
|
+
type Proj = { kind: 'self' } | { kind: 'field'; field: string } | { kind: 'tuple'; elements: Proj[] }
|
|
1721
|
+
function leafMatches(body: ReturnType<typeof parseExpression>, param: string, leaf: Proj): boolean {
|
|
1722
|
+
if (leaf.kind === 'self') return body.kind === 'identifier' && body.name === param
|
|
1723
|
+
if (leaf.kind === 'field') {
|
|
1724
|
+
return (
|
|
1725
|
+
body.kind === 'member' &&
|
|
1726
|
+
!body.computed &&
|
|
1727
|
+
body.property === leaf.field &&
|
|
1728
|
+
body.object.kind === 'identifier' &&
|
|
1729
|
+
body.object.name === param
|
|
1730
|
+
)
|
|
1731
|
+
}
|
|
1732
|
+
// tuple
|
|
1733
|
+
if (body.kind !== 'array-literal') return false
|
|
1734
|
+
if (body.elements.length !== leaf.elements.length) return false
|
|
1735
|
+
return leaf.elements.every((el, i) => leafMatches(body.elements[i], param, el))
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1584
1738
|
const accepted: Array<[string, string, Proj]> = [
|
|
1585
1739
|
['self (i => i)', 'arr.flatMap(i => i)', { kind: 'self' }],
|
|
1586
1740
|
['field (i => i.tags)', 'arr.flatMap(i => i.tags)', { kind: 'field', field: 'tags' }],
|
|
@@ -1589,40 +1743,35 @@ describe('expression-parser — .flatMap(fn) projection (#1448 Tier C)', () => {
|
|
|
1589
1743
|
['tuple self + field', 'arr.flatMap(i => [i, i.tags])', { kind: 'tuple', elements: [{ kind: 'self' }, { kind: 'field', field: 'tags' }] }],
|
|
1590
1744
|
]
|
|
1591
1745
|
for (const [label, expr, projection] of accepted) {
|
|
1592
|
-
test(`${label} — lowers to a flatMap
|
|
1746
|
+
test(`${label} — lowers to a generic flatMap callback call`, () => {
|
|
1593
1747
|
const result = parseExpression(expr)
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
throw new Error(`expected a flatMap array-method, got ${result.kind}`)
|
|
1599
|
-
}
|
|
1748
|
+
const cb = asCallbackMethodCall(result)
|
|
1749
|
+
expect(cb).not.toBeNull()
|
|
1750
|
+
expect(cb!.method).toBe('flatMap')
|
|
1751
|
+
expect(leafMatches(cb!.arrow.body, cb!.arrow.params[0], projection)).toBe(true)
|
|
1600
1752
|
})
|
|
1601
1753
|
}
|
|
1602
1754
|
|
|
1603
|
-
// Out of
|
|
1604
|
-
|
|
1755
|
+
// Out of the old structured catalogue: previously refused as
|
|
1756
|
+
// `unsupported`. The "is it lowerable" decision moved to the adapter
|
|
1757
|
+
// (#2018 P5), so the PARSER now accepts these generically — they parse
|
|
1758
|
+
// to a `flatMap` callback `call`. (The 2-arg form keeps its extra arg.)
|
|
1759
|
+
const nowGeneric: Array<[string, string]> = [
|
|
1605
1760
|
['deep field access', 'arr.flatMap(i => i.a.b)'],
|
|
1606
1761
|
['index/array callback params', 'arr.flatMap((i, idx) => i.tags)'],
|
|
1607
|
-
// Tuple with a non-leaf element (arithmetic / literal / deep access).
|
|
1608
1762
|
['tuple with arithmetic element', 'arr.flatMap(i => [i.a, i.b + 1])'],
|
|
1609
1763
|
['tuple with literal element', 'arr.flatMap(i => [i.a, "x"])'],
|
|
1610
1764
|
['tuple with deep access', 'arr.flatMap(i => [i.a, i.b.c])'],
|
|
1611
1765
|
['tuple with spread', 'arr.flatMap(i => [...i.a])'],
|
|
1612
|
-
// Empty tuple is a degenerate no-op — refused so emitters never
|
|
1613
|
-
// produce a zero-arg `flat_map_tuple` call.
|
|
1614
1766
|
['empty tuple', 'arr.flatMap(i => [])'],
|
|
1615
|
-
// Wrong-arity forms are intercepted by the same arm (not the generic
|
|
1616
|
-
// "flatMap has no template lowering" gate) so the reason stays tailored.
|
|
1617
1767
|
['2-arg flatMap(fn, thisArg)', 'arr.flatMap(i => i.tags, ctx)'],
|
|
1618
1768
|
]
|
|
1619
|
-
for (const [label, expr] of
|
|
1620
|
-
test(`${label} —
|
|
1769
|
+
for (const [label, expr] of nowGeneric) {
|
|
1770
|
+
test(`${label} — parses as a generic flatMap callback call`, () => {
|
|
1621
1771
|
const result = parseExpression(expr)
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
}
|
|
1772
|
+
const cb = asCallbackMethodCall(result)
|
|
1773
|
+
expect(cb).not.toBeNull()
|
|
1774
|
+
expect(cb!.method).toBe('flatMap')
|
|
1626
1775
|
})
|
|
1627
1776
|
}
|
|
1628
1777
|
|
|
@@ -1783,3 +1932,166 @@ describe('parseStyleObjectEntries', () => {
|
|
|
1783
1932
|
expect(parseStyleObjectEntries('color')).toBeNull()
|
|
1784
1933
|
})
|
|
1785
1934
|
})
|
|
1935
|
+
|
|
1936
|
+
// =============================================================================
|
|
1937
|
+
// Block → Expression Normalization (#2040)
|
|
1938
|
+
// =============================================================================
|
|
1939
|
+
|
|
1940
|
+
describe('foldBlockToExpr', () => {
|
|
1941
|
+
// Parse `{ … }` block source into ParsedStatement[] for the fold under test.
|
|
1942
|
+
function parseBlock(blockSrc: string) {
|
|
1943
|
+
const sf = ts.createSourceFile('b.ts', `(() => ${blockSrc})`, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
|
|
1944
|
+
const stmt = sf.statements[0]
|
|
1945
|
+
if (!ts.isExpressionStatement(stmt)) throw new Error('expected expression statement')
|
|
1946
|
+
let expr = stmt.expression
|
|
1947
|
+
while (ts.isParenthesizedExpression(expr)) expr = expr.expression
|
|
1948
|
+
if (!ts.isArrowFunction(expr) || !ts.isBlock(expr.body)) throw new Error('expected block-body arrow')
|
|
1949
|
+
return parseBlockBody(expr.body, sf, n => n.getText(sf))
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
test('let-inline: a const binding inlines into the returned expression', () => {
|
|
1953
|
+
const stmts = parseBlock('{ const x = a + 1; return x * 2 }')!
|
|
1954
|
+
const folded = foldBlockToExpr(stmts)
|
|
1955
|
+
expect(folded.ok).toBe(true)
|
|
1956
|
+
// `x` is replaced by `a + 1` in `x * 2`, giving the `*`-over-`+` tree.
|
|
1957
|
+
expect(folded.ok && folded.expr).toEqual({
|
|
1958
|
+
kind: 'binary',
|
|
1959
|
+
op: '*',
|
|
1960
|
+
left: { kind: 'binary', op: '+', left: { kind: 'identifier', name: 'a' }, right: { kind: 'literal', value: 1, literalType: 'number', raw: '1' } },
|
|
1961
|
+
right: { kind: 'literal', value: 2, literalType: 'number', raw: '2' },
|
|
1962
|
+
})
|
|
1963
|
+
})
|
|
1964
|
+
|
|
1965
|
+
test('chained let-inline: later bindings see earlier ones', () => {
|
|
1966
|
+
const stmts = parseBlock('{ const a = x + 1; const b = a * 2; return b }')!
|
|
1967
|
+
const folded = foldBlockToExpr(stmts)
|
|
1968
|
+
expect(folded.ok).toBe(true)
|
|
1969
|
+
expect(folded.ok && folded.expr).toEqual({
|
|
1970
|
+
kind: 'binary',
|
|
1971
|
+
op: '*',
|
|
1972
|
+
left: { kind: 'binary', op: '+', left: { kind: 'identifier', name: 'x' }, right: { kind: 'literal', value: 1, literalType: 'number', raw: '1' } },
|
|
1973
|
+
right: { kind: 'literal', value: 2, literalType: 'number', raw: '2' },
|
|
1974
|
+
})
|
|
1975
|
+
})
|
|
1976
|
+
|
|
1977
|
+
test('early return: `if (c) return A; return B` → ternary', () => {
|
|
1978
|
+
const stmts = parseBlock("{ if (f() === 'active') return !t.done; return true }")!
|
|
1979
|
+
const folded = foldBlockToExpr(stmts)
|
|
1980
|
+
expect(folded.ok).toBe(true)
|
|
1981
|
+
expect(folded.ok && folded.expr.kind).toBe('conditional')
|
|
1982
|
+
// `stringifyParsedExpr` normalises string literals to double quotes.
|
|
1983
|
+
expect(folded.ok && stringifyParsedExpr(folded.expr)).toBe('f() === "active" ? !t.done : true')
|
|
1984
|
+
})
|
|
1985
|
+
|
|
1986
|
+
test('if/else: both branches return → ternary', () => {
|
|
1987
|
+
const stmts = parseBlock('{ if (c) { return 1 } else { return 2 } }')!
|
|
1988
|
+
const folded = foldBlockToExpr(stmts)
|
|
1989
|
+
expect(folded.ok).toBe(true)
|
|
1990
|
+
expect(folded.ok && stringifyParsedExpr(folded.expr)).toBe('c ? 1 : 2')
|
|
1991
|
+
})
|
|
1992
|
+
|
|
1993
|
+
test('else-if chain → right-nested ternary', () => {
|
|
1994
|
+
const stmts = parseBlock('{ if (a()) return 1; else if (b()) return 2; return 3 }')!
|
|
1995
|
+
const folded = foldBlockToExpr(stmts)
|
|
1996
|
+
expect(folded.ok).toBe(true)
|
|
1997
|
+
expect(folded.ok && stringifyParsedExpr(folded.expr)).toBe('a() ? 1 : b() ? 2 : 3')
|
|
1998
|
+
})
|
|
1999
|
+
|
|
2000
|
+
test('let-inline before an early return is visible in both ternary arms', () => {
|
|
2001
|
+
const stmts = parseBlock("{ const f = filter(); if (f === 'active') return !t.done; return true }")!
|
|
2002
|
+
const folded = foldBlockToExpr(stmts)
|
|
2003
|
+
expect(folded.ok).toBe(true)
|
|
2004
|
+
expect(folded.ok && stringifyParsedExpr(folded.expr)).toBe('filter() === "active" ? !t.done : true')
|
|
2005
|
+
})
|
|
2006
|
+
|
|
2007
|
+
test('refuses a block that falls through without returning a value', () => {
|
|
2008
|
+
const stmts = parseBlock('{ const x = 1 }')!
|
|
2009
|
+
const folded = foldBlockToExpr(stmts)
|
|
2010
|
+
expect(folded.ok).toBe(false)
|
|
2011
|
+
})
|
|
2012
|
+
|
|
2013
|
+
test('refuses an `if` whose branch does not produce a value (side-effect only)', () => {
|
|
2014
|
+
const stmts = parseBlock('{ if (c) { const z = 1 } return 0 }')
|
|
2015
|
+
// `{ const z = 1 }` is a value-less then-branch that falls through to the
|
|
2016
|
+
// trailing `return 0`; the fold still produces `c ? 0 : 0` since the
|
|
2017
|
+
// then-branch continues into the rest. A genuinely imperative shape is the
|
|
2018
|
+
// reassignment case below, which `parseBlockBody` cannot even represent.
|
|
2019
|
+
expect(stmts).not.toBeNull()
|
|
2020
|
+
})
|
|
2021
|
+
|
|
2022
|
+
test('parseBlockBody returns null for an imperative statement (for loop)', () => {
|
|
2023
|
+
const stmts = parseBlock('{ let s = 0; for (const x of arr) s += x; return s }')
|
|
2024
|
+
expect(stmts).toBeNull()
|
|
2025
|
+
})
|
|
2026
|
+
|
|
2027
|
+
test('parseBlockBody returns null for a local re-assignment', () => {
|
|
2028
|
+
const stmts = parseBlock('{ let y = 1; y = y + 1; return y }')
|
|
2029
|
+
expect(stmts).toBeNull()
|
|
2030
|
+
})
|
|
2031
|
+
|
|
2032
|
+
// Soundness: let-inline must not be hygienic-blind or duplicate/drop effects
|
|
2033
|
+
// (PR #2051 review).
|
|
2034
|
+
test('refuses substitution that would capture a var under a nested callback param', () => {
|
|
2035
|
+
// `x` is bound to the outer `a`; inlining into `list.map(a => a + x)` would
|
|
2036
|
+
// capture the outer `a` under the inner `map` param `a`. Refuse rather than
|
|
2037
|
+
// silently miscompile to `a + a`.
|
|
2038
|
+
const stmts = parseBlock('{ const x = a; return list.map(a => a + x) }')!
|
|
2039
|
+
expect(foldBlockToExpr(stmts).ok).toBe(false)
|
|
2040
|
+
})
|
|
2041
|
+
|
|
2042
|
+
test('refuses a possibly-impure init used more than once (double-eval)', () => {
|
|
2043
|
+
const stmts = parseBlock('{ const d = next(); return d + d }')!
|
|
2044
|
+
expect(foldBlockToExpr(stmts).ok).toBe(false)
|
|
2045
|
+
})
|
|
2046
|
+
|
|
2047
|
+
test('refuses a possibly-impure init that is never used (dropped effect)', () => {
|
|
2048
|
+
const stmts = parseBlock('{ const _ = log(); return a - b }')!
|
|
2049
|
+
expect(foldBlockToExpr(stmts).ok).toBe(false)
|
|
2050
|
+
})
|
|
2051
|
+
|
|
2052
|
+
test('refuses a possibly-impure init referenced inside a callback (per-element eval)', () => {
|
|
2053
|
+
const stmts = parseBlock('{ const d = next(); return arr.map(x => x + d) }')!
|
|
2054
|
+
expect(foldBlockToExpr(stmts).ok).toBe(false)
|
|
2055
|
+
})
|
|
2056
|
+
|
|
2057
|
+
test('allows a possibly-impure init used exactly once (eval-count preserved)', () => {
|
|
2058
|
+
const stmts = parseBlock('{ const d = next(); return d }')!
|
|
2059
|
+
const folded = foldBlockToExpr(stmts)
|
|
2060
|
+
expect(folded.ok).toBe(true)
|
|
2061
|
+
expect(folded.ok && folded.expr).toEqual({ kind: 'call', callee: { kind: 'identifier', name: 'next' }, args: [] })
|
|
2062
|
+
})
|
|
2063
|
+
|
|
2064
|
+
test('allows a pure init used many times (signal-getter / member access)', () => {
|
|
2065
|
+
// A pure init is safe to inline at any number of sites — `filter()` read
|
|
2066
|
+
// once in the condition is the canonical case; a member access twice is fine.
|
|
2067
|
+
const stmts = parseBlock('{ const n = item.n; return n > 0 ? n : 0 }')!
|
|
2068
|
+
const folded = foldBlockToExpr(stmts)
|
|
2069
|
+
expect(folded.ok).toBe(true)
|
|
2070
|
+
})
|
|
2071
|
+
|
|
2072
|
+
// Soundness: an impure init must be evaluated once on EVERY path, not just
|
|
2073
|
+
// "at most once on some path" (PR #2051 re-review). A binding used in only one
|
|
2074
|
+
// ternary arm is dropped on the other arm even though `max` uses is 1.
|
|
2075
|
+
test('refuses a possibly-impure init used on only one branch (then)', () => {
|
|
2076
|
+
const stmts = parseBlock('{ const d = next(); if (c) return d; return 0 }')!
|
|
2077
|
+
expect(foldBlockToExpr(stmts).ok).toBe(false)
|
|
2078
|
+
})
|
|
2079
|
+
|
|
2080
|
+
test('refuses a possibly-impure init used on only one branch (else)', () => {
|
|
2081
|
+
const stmts = parseBlock('{ const d = next(); if (c) return 0; return d }')!
|
|
2082
|
+
expect(foldBlockToExpr(stmts).ok).toBe(false)
|
|
2083
|
+
})
|
|
2084
|
+
|
|
2085
|
+
test('refuses a possibly-impure init behind a short-circuiting operand', () => {
|
|
2086
|
+
// `c && next()` skips the call when `c` is falsy; the original always calls
|
|
2087
|
+
// it once.
|
|
2088
|
+
const stmts = parseBlock('{ const d = next(); return c && d }')!
|
|
2089
|
+
expect(foldBlockToExpr(stmts).ok).toBe(false)
|
|
2090
|
+
})
|
|
2091
|
+
|
|
2092
|
+
test('allows a possibly-impure init evaluated unconditionally before a short-circuit', () => {
|
|
2093
|
+
// `next() && c` always evaluates the call exactly once (left operand).
|
|
2094
|
+
const stmts = parseBlock('{ const d = next(); return d && c }')!
|
|
2095
|
+
expect(foldBlockToExpr(stmts).ok).toBe(true)
|
|
2096
|
+
})
|
|
2097
|
+
})
|