@barefootjs/jsx 0.17.0 → 0.18.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 (88) hide show
  1. package/dist/adapters/env-signal.d.ts +42 -7
  2. package/dist/adapters/env-signal.d.ts.map +1 -1
  3. package/dist/adapters/interface.d.ts +20 -4
  4. package/dist/adapters/interface.d.ts.map +1 -1
  5. package/dist/adapters/parsed-expr-emitter.d.ts +3 -1
  6. package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
  7. package/dist/analyzer-context.d.ts.map +1 -1
  8. package/dist/analyzer.d.ts.map +1 -1
  9. package/dist/augment-inherited-props.d.ts +19 -0
  10. package/dist/augment-inherited-props.d.ts.map +1 -1
  11. package/dist/compiler.d.ts.map +1 -1
  12. package/dist/expression-parser.d.ts +48 -2
  13. package/dist/expression-parser.d.ts.map +1 -1
  14. package/dist/index.d.ts +24 -5
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1728 -969
  17. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/compute-inlinability.d.ts.map +1 -1
  19. package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +9 -0
  21. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
  23. package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +19 -0
  24. package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
  25. package/dist/ir-to-client-js/emit-registration.d.ts +4 -2
  26. package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
  27. package/dist/ir-to-client-js/html-template.d.ts +41 -0
  28. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  29. package/dist/ir-to-client-js/types.d.ts +22 -1
  30. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  31. package/dist/jsx-to-ir.d.ts.map +1 -1
  32. package/dist/loop-destructure.d.ts +55 -18
  33. package/dist/loop-destructure.d.ts.map +1 -1
  34. package/dist/lowering-registry.d.ts +13 -0
  35. package/dist/lowering-registry.d.ts.map +1 -1
  36. package/dist/relocate.d.ts +28 -0
  37. package/dist/relocate.d.ts.map +1 -1
  38. package/dist/ssr-defaults.d.ts.map +1 -1
  39. package/dist/ssr-seed-plan.d.ts +84 -0
  40. package/dist/ssr-seed-plan.d.ts.map +1 -0
  41. package/dist/types.d.ts +79 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/package.json +2 -2
  44. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +284 -12
  45. package/src/__tests__/augment-inherited-props.test.ts +96 -0
  46. package/src/__tests__/compiler-runtime-contract.test.ts +11 -1
  47. package/src/__tests__/compiler-stress-1244.test.ts +13 -4
  48. package/src/__tests__/csr-substitution-safety-divergence.test.ts +137 -0
  49. package/src/__tests__/destructured-map-params.test.ts +11 -1
  50. package/src/__tests__/expression-parser.test.ts +74 -3
  51. package/src/__tests__/free-identifiers.test.ts +55 -0
  52. package/src/__tests__/ir-sort-comparator.test.ts +261 -0
  53. package/src/__tests__/loop-destructure.test.ts +313 -0
  54. package/src/__tests__/loop-hoisted-template.test.ts +235 -0
  55. package/src/__tests__/materialize-getter-calls.test.ts +58 -0
  56. package/src/__tests__/props-destructuring.test.ts +110 -0
  57. package/src/__tests__/serialize-parsed-expr.test.ts +88 -2
  58. package/src/__tests__/ssr-defaults.test.ts +20 -0
  59. package/src/__tests__/ssr-seed-plan.test.ts +212 -0
  60. package/src/__tests__/staged-ir/11-template-primitive-registry.test.ts +231 -1
  61. package/src/__tests__/tagged-template-interleave.test.ts +268 -0
  62. package/src/__tests__/unsupported-expression.test.ts +194 -7
  63. package/src/adapters/env-signal.ts +57 -9
  64. package/src/adapters/interface.ts +20 -4
  65. package/src/adapters/parsed-expr-emitter.ts +19 -2
  66. package/src/analyzer-context.ts +20 -0
  67. package/src/analyzer.ts +74 -1
  68. package/src/augment-inherited-props.ts +139 -9
  69. package/src/compiler.ts +10 -1
  70. package/src/expression-parser.ts +421 -50
  71. package/src/index.ts +30 -3
  72. package/src/ir-to-client-js/collect-elements.ts +15 -1
  73. package/src/ir-to-client-js/compute-inlinability.ts +6 -1
  74. package/src/ir-to-client-js/control-flow/plan/build-loop.ts +1 -0
  75. package/src/ir-to-client-js/control-flow/plan/loop.ts +9 -0
  76. package/src/ir-to-client-js/control-flow/stringify/loop.ts +30 -8
  77. package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +30 -0
  78. package/src/ir-to-client-js/emit-registration.ts +4 -2
  79. package/src/ir-to-client-js/html-template.ts +198 -1
  80. package/src/ir-to-client-js/index.ts +1 -0
  81. package/src/ir-to-client-js/types.ts +22 -0
  82. package/src/jsx-to-ir.ts +409 -24
  83. package/src/loop-destructure.ts +89 -36
  84. package/src/lowering-registry.ts +16 -0
  85. package/src/relocate.ts +201 -14
  86. package/src/ssr-defaults.ts +34 -32
  87. package/src/ssr-seed-plan.ts +146 -0
  88. package/src/types.ts +76 -0
@@ -382,3 +382,264 @@ describe('sort().map() / toSorted().map()', () => {
382
382
  }
383
383
  })
384
384
  })
385
+
386
+ // #2090: `.sort(cb)` / `.toSorted(cb)` where `cb` is a bare identifier
387
+ // reference to a same-file arrow/function-declaration comparator, resolved
388
+ // through the analyzer's scope machinery (`findLocalConst` /
389
+ // `findLocalFunction` in jsx-to-ir.ts) one hop, then fed through the SAME
390
+ // `sortComparatorFromArrow` catalogue as an inline comparator — no new
391
+ // comparator shapes, no IR schema change.
392
+ describe('function-reference sort comparator (#2090)', () => {
393
+ test('const arrow reference, ascending', () => {
394
+ const source = `
395
+ 'use client'
396
+ import { createSignal } from '@barefootjs/client'
397
+
398
+ export function ProductList() {
399
+ const [products, setProducts] = createSignal<any[]>([])
400
+ const byPrice = (a, b) => a.price - b.price
401
+ return (
402
+ <ul>
403
+ {products().sort(byPrice).map(p => (
404
+ <li key={p.name}>{p.name}</li>
405
+ ))}
406
+ </ul>
407
+ )
408
+ }
409
+ `
410
+
411
+ const ctx = analyzeComponent(source, 'ProductList.tsx')
412
+ const ir = jsxToIR(ctx)
413
+
414
+ expect(ir).not.toBeNull()
415
+ if (ir!.type === 'element') {
416
+ const loop = ir!.children.find(c => c.type === 'loop')
417
+ expect(loop).toBeDefined()
418
+ if (loop?.type === 'loop') {
419
+ expect(loop.sortComparator).toBeDefined()
420
+ expect(sortComparatorFromArrow(loop.sortComparator!.arrow)!.keys).toEqual([
421
+ { key: { kind: 'field', field: 'price' }, type: 'numeric', direction: 'asc' },
422
+ ])
423
+ expect(loop.sortComparator!.paramA).toBe('a')
424
+ expect(loop.sortComparator!.paramB).toBe('b')
425
+ expect(loop.sortComparator!.raw).toBe('a.price - b.price')
426
+ expect(loop.array).toBe('products()')
427
+ }
428
+ }
429
+ })
430
+
431
+ test('const arrow reference, descending multi-key (||-chain)', () => {
432
+ const source = `
433
+ 'use client'
434
+ import { createSignal } from '@barefootjs/client'
435
+
436
+ export function ProductList() {
437
+ const [products, setProducts] = createSignal<any[]>([])
438
+ const byPriceThenName = (a, b) => b.price - a.price || a.name.localeCompare(b.name)
439
+ return (
440
+ <ul>
441
+ {products().sort(byPriceThenName).map(p => (
442
+ <li key={p.name}>{p.name}</li>
443
+ ))}
444
+ </ul>
445
+ )
446
+ }
447
+ `
448
+
449
+ const ctx = analyzeComponent(source, 'ProductList.tsx')
450
+ const ir = jsxToIR(ctx)
451
+
452
+ expect(ir).not.toBeNull()
453
+ if (ir!.type === 'element') {
454
+ const loop = ir!.children.find(c => c.type === 'loop')
455
+ if (loop?.type === 'loop') {
456
+ expect(loop.sortComparator).toBeDefined()
457
+ expect(sortComparatorFromArrow(loop.sortComparator!.arrow)!.keys).toEqual([
458
+ { key: { kind: 'field', field: 'price' }, type: 'numeric', direction: 'desc' },
459
+ { key: { kind: 'field', field: 'name' }, type: 'string', direction: 'asc' },
460
+ ])
461
+ expect(loop.sortComparator!.raw).toBe('b.price - a.price || a.name.localeCompare(b.name)')
462
+ }
463
+ }
464
+ })
465
+
466
+ test('function-declaration reference with return-block body', () => {
467
+ const source = `
468
+ 'use client'
469
+ import { createSignal } from '@barefootjs/client'
470
+
471
+ function byPrice(a, b) {
472
+ return a.price - b.price
473
+ }
474
+
475
+ export function ProductList() {
476
+ const [products, setProducts] = createSignal<any[]>([])
477
+ return (
478
+ <ul>
479
+ {products().sort(byPrice).map(p => (
480
+ <li key={p.name}>{p.name}</li>
481
+ ))}
482
+ </ul>
483
+ )
484
+ }
485
+ `
486
+
487
+ const ctx = analyzeComponent(source, 'ProductList.tsx')
488
+ const ir = jsxToIR(ctx)
489
+
490
+ expect(ir).not.toBeNull()
491
+ if (ir!.type === 'element') {
492
+ const loop = ir!.children.find(c => c.type === 'loop')
493
+ if (loop?.type === 'loop') {
494
+ expect(loop.sortComparator).toBeDefined()
495
+ expect(sortComparatorFromArrow(loop.sortComparator!.arrow)!.keys).toEqual([
496
+ { key: { kind: 'field', field: 'price' }, type: 'numeric', direction: 'asc' },
497
+ ])
498
+ expect(loop.sortComparator!.paramA).toBe('a')
499
+ expect(loop.sortComparator!.paramB).toBe('b')
500
+ expect(loop.sortComparator!.raw).toBe('a.price - b.price')
501
+ }
502
+ }
503
+ })
504
+
505
+ test('.toSorted(ref) resolves the same way as .sort(ref)', () => {
506
+ const source = `
507
+ 'use client'
508
+ import { createSignal } from '@barefootjs/client'
509
+
510
+ export function ProductList() {
511
+ const [products, setProducts] = createSignal<any[]>([])
512
+ const byPrice = (a, b) => a.price - b.price
513
+ return (
514
+ <ul>
515
+ {products().toSorted(byPrice).map(p => (
516
+ <li key={p.name}>{p.name}</li>
517
+ ))}
518
+ </ul>
519
+ )
520
+ }
521
+ `
522
+
523
+ const ctx = analyzeComponent(source, 'ProductList.tsx')
524
+ const ir = jsxToIR(ctx)
525
+
526
+ expect(ir).not.toBeNull()
527
+ if (ir!.type === 'element') {
528
+ const loop = ir!.children.find(c => c.type === 'loop')
529
+ if (loop?.type === 'loop') {
530
+ expect(loop.sortComparator).toBeDefined()
531
+ expect(sortComparatorFromArrow(loop.sortComparator!.arrow)!.keys).toEqual([
532
+ { key: { kind: 'field', field: 'price' }, type: 'numeric', direction: 'asc' },
533
+ ])
534
+ }
535
+ }
536
+ })
537
+
538
+ test('component-scope const shadows a module-scope const of the same name', () => {
539
+ const source = `
540
+ 'use client'
541
+ import { createSignal } from '@barefootjs/client'
542
+
543
+ const byPrice = (a, b) => a.price - b.price
544
+
545
+ export function ProductList() {
546
+ const [products, setProducts] = createSignal<any[]>([])
547
+ const byPrice = (a, b) => b.price - a.price
548
+ return (
549
+ <ul>
550
+ {products().sort(byPrice).map(p => (
551
+ <li key={p.name}>{p.name}</li>
552
+ ))}
553
+ </ul>
554
+ )
555
+ }
556
+ `
557
+
558
+ const ctx = analyzeComponent(source, 'ProductList.tsx')
559
+ const ir = jsxToIR(ctx)
560
+
561
+ expect(ir).not.toBeNull()
562
+ if (ir!.type === 'element') {
563
+ const loop = ir!.children.find(c => c.type === 'loop')
564
+ if (loop?.type === 'loop') {
565
+ expect(loop.sortComparator).toBeDefined()
566
+ // The component-scope binding (descending) wins over the
567
+ // module-scope one (ascending) — distinguishable via `direction`.
568
+ expect(sortComparatorFromArrow(loop.sortComparator!.arrow)!.keys).toEqual([
569
+ { key: { kind: 'field', field: 'price' }, type: 'numeric', direction: 'desc' },
570
+ ])
571
+ expect(loop.sortComparator!.raw).toBe('b.price - a.price')
572
+ }
573
+ }
574
+ })
575
+
576
+ test('filter().sort(ref).map() resolves the reference in a chain', () => {
577
+ const source = `
578
+ 'use client'
579
+ import { createSignal } from '@barefootjs/client'
580
+
581
+ export function TodoList() {
582
+ const [todos, setTodos] = createSignal<any[]>([])
583
+ const byPriority = (a, b) => a.priority - b.priority
584
+ return (
585
+ <ul>
586
+ {todos().filter(t => !t.done).sort(byPriority).map(t => (
587
+ <li key={t.text}>{t.text}</li>
588
+ ))}
589
+ </ul>
590
+ )
591
+ }
592
+ `
593
+
594
+ const ctx = analyzeComponent(source, 'TodoList.tsx')
595
+ const ir = jsxToIR(ctx)
596
+
597
+ expect(ir).not.toBeNull()
598
+ if (ir!.type === 'element') {
599
+ const loop = ir!.children.find(c => c.type === 'loop')
600
+ if (loop?.type === 'loop') {
601
+ expect(loop.filterPredicate).toBeDefined()
602
+ expect(loop.sortComparator).toBeDefined()
603
+ expect(loop.chainOrder).toBe('filter-sort')
604
+ expect(sortComparatorFromArrow(loop.sortComparator!.arrow)!.keys).toEqual([
605
+ { key: { kind: 'field', field: 'priority' }, type: 'numeric', direction: 'asc' },
606
+ ])
607
+ }
608
+ }
609
+ })
610
+
611
+ test('sort(ref).filter().map() resolves the reference in a chain', () => {
612
+ const source = `
613
+ 'use client'
614
+ import { createSignal } from '@barefootjs/client'
615
+
616
+ export function TodoList() {
617
+ const [todos, setTodos] = createSignal<any[]>([])
618
+ const byPriority = (a, b) => a.priority - b.priority
619
+ return (
620
+ <ul>
621
+ {todos().sort(byPriority).filter(t => !t.done).map(t => (
622
+ <li key={t.text}>{t.text}</li>
623
+ ))}
624
+ </ul>
625
+ )
626
+ }
627
+ `
628
+
629
+ const ctx = analyzeComponent(source, 'TodoList.tsx')
630
+ const ir = jsxToIR(ctx)
631
+
632
+ expect(ir).not.toBeNull()
633
+ if (ir!.type === 'element') {
634
+ const loop = ir!.children.find(c => c.type === 'loop')
635
+ if (loop?.type === 'loop') {
636
+ expect(loop.filterPredicate).toBeDefined()
637
+ expect(loop.sortComparator).toBeDefined()
638
+ expect(loop.chainOrder).toBe('sort-filter')
639
+ expect(sortComparatorFromArrow(loop.sortComparator!.arrow)!.keys).toEqual([
640
+ { key: { kind: 'field', field: 'priority' }, type: 'numeric', direction: 'asc' },
641
+ ])
642
+ }
643
+ }
644
+ })
645
+ })
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Tests for #2087 Phase A: `isLowerableLoopDestructure` (née
3
+ * `isLowerableObjectRestDestructure`) — the gate template adapters use to
4
+ * decide whether a `.map()` callback's destructure param can be lowered
5
+ * natively, versus falling back to the BF104 diagnostic.
6
+ *
7
+ * Compiles a small component to IR (`analyzeComponent` + `jsxToIR`, the same
8
+ * helper pattern as `ir-sort-comparator.test.ts`), locates the `IRLoop`
9
+ * node, and asserts the gate's verdict plus (where relevant) the structured
10
+ * `segments` path each binding carries.
11
+ */
12
+
13
+ import { describe, test, expect } from 'bun:test'
14
+ import { analyzeComponent } from '../analyzer'
15
+ import { jsxToIR } from '../jsx-to-ir'
16
+ import { isLowerableLoopDestructure, isLowerableObjectRestDestructure } from '../loop-destructure'
17
+ import { ErrorCodes } from '../errors'
18
+ import type { IRLoop, IRNode } from '../types'
19
+
20
+ function findFirstLoop(node: IRNode): IRLoop {
21
+ if (node.type === 'loop') return node
22
+ if (node.type === 'element' || node.type === 'fragment') {
23
+ for (const child of node.children) {
24
+ if (child.type === 'loop') return child
25
+ if (child.type === 'element' || child.type === 'fragment') {
26
+ try {
27
+ return findFirstLoop(child)
28
+ } catch {
29
+ // keep scanning siblings
30
+ }
31
+ }
32
+ }
33
+ }
34
+ throw new Error('no loop node found in IR')
35
+ }
36
+
37
+ function compileLoop(
38
+ source: string,
39
+ targetComponentName?: string,
40
+ filename = 'Test.tsx',
41
+ ): { loop: IRLoop; errors: ReturnType<typeof analyzeComponent>['errors'] } {
42
+ const ctx = analyzeComponent(source, filename, targetComponentName)
43
+ const ir = jsxToIR(ctx)
44
+ expect(ir).not.toBeNull()
45
+ return { loop: findFirstLoop(ir!), errors: ctx.errors }
46
+ }
47
+
48
+ describe('isLowerableLoopDestructure (#2087)', () => {
49
+ test('simple .field bindings + object-rest read via member access → true (existing behavior)', () => {
50
+ const { loop } = compileLoop(`
51
+ 'use client'
52
+ import { createSignal } from '@barefootjs/client'
53
+
54
+ type Task = { id: string; title: string; flag: string }
55
+ export function RestObject() {
56
+ const [tasks, setTasks] = createSignal<Task[]>([])
57
+ return (
58
+ <ul onClick={() => setTasks(t => t)}>
59
+ {tasks().map(({ id, title, ...rest }) => (
60
+ <li key={id}>{title}:{rest.flag}</li>
61
+ ))}
62
+ </ul>
63
+ )
64
+ }
65
+ `)
66
+ expect(isLowerableLoopDestructure(loop)).toBe(true)
67
+ })
68
+
69
+ test('array-index tuple destructure ([k, v]) → true (NEW)', () => {
70
+ const { loop } = compileLoop(`
71
+ 'use client'
72
+ import { createSignal } from '@barefootjs/client'
73
+
74
+ type Row = readonly [string, string]
75
+ export function IndexPairs() {
76
+ const [rows, setRows] = createSignal<Row[]>([])
77
+ return (
78
+ <ul onClick={() => setRows(r => r)}>
79
+ {rows().map(([k, v]) => (
80
+ <li key={k}>{k}:{v}</li>
81
+ ))}
82
+ </ul>
83
+ )
84
+ }
85
+ `)
86
+ expect(isLowerableLoopDestructure(loop)).toBe(true)
87
+ })
88
+
89
+ test('nested { id, cells: [head] } → true (NEW)', () => {
90
+ const { loop } = compileLoop(`
91
+ 'use client'
92
+ import { createSignal } from '@barefootjs/client'
93
+
94
+ type Row = { id: string; cells: readonly string[] }
95
+ export function NestedHead() {
96
+ const [rows, setRows] = createSignal<Row[]>([])
97
+ return (
98
+ <ul onClick={() => setRows(r => r)}>
99
+ {rows().map(({ id, cells: [head] }) => (
100
+ <li key={id}>{head}</li>
101
+ ))}
102
+ </ul>
103
+ )
104
+ }
105
+ `)
106
+ expect(isLowerableLoopDestructure(loop)).toBe(true)
107
+ })
108
+
109
+ test('array-rest [first, ...tail] with tail.length read → true (NEW)', () => {
110
+ const { loop } = compileLoop(`
111
+ 'use client'
112
+ import { createSignal } from '@barefootjs/client'
113
+
114
+ export function RestTuple() {
115
+ const [rows, setRows] = createSignal<string[][]>([])
116
+ return (
117
+ <ul onClick={() => setRows(r => r)}>
118
+ {rows().map(([first, ...tail]) => (
119
+ <li key={first}>{first} (+{tail.length})</li>
120
+ ))}
121
+ </ul>
122
+ )
123
+ }
124
+ `)
125
+ expect(isLowerableLoopDestructure(loop)).toBe(true)
126
+ })
127
+
128
+ test('object-rest {...rest} spread onto the loop-item root <li> → true (NEW)', () => {
129
+ const { loop } = compileLoop(`
130
+ 'use client'
131
+ import { createSignal } from '@barefootjs/client'
132
+
133
+ type Task = { id: string; title: string; flag: string }
134
+ export function RestSpread() {
135
+ const [tasks, setTasks] = createSignal<Task[]>([])
136
+ return (
137
+ <ul onClick={() => setTasks(t => t)}>
138
+ {tasks().map(({ id, title, ...rest }) => (
139
+ <li key={id} {...rest}>{title}</li>
140
+ ))}
141
+ </ul>
142
+ )
143
+ }
144
+ `)
145
+ expect(isLowerableLoopDestructure(loop)).toBe(true)
146
+ })
147
+
148
+ test('object-rest spread on a COMPONENT (<Child {...rest} />) → false', () => {
149
+ const { loop } = compileLoop(`
150
+ 'use client'
151
+ import { createSignal } from '@barefootjs/client'
152
+
153
+ type Task = { id: string; title: string; flag: string }
154
+ function Child(props: { title: string; flag: string }) {
155
+ return <span>{props.title}</span>
156
+ }
157
+ export function RestOntoComponent() {
158
+ const [tasks, setTasks] = createSignal<Task[]>([])
159
+ return (
160
+ <ul onClick={() => setTasks(t => t)}>
161
+ {tasks().map(({ id, ...rest }) => (
162
+ <li key={id}><Child {...rest} /></li>
163
+ ))}
164
+ </ul>
165
+ )
166
+ }
167
+ `, 'RestOntoComponent')
168
+ expect(isLowerableLoopDestructure(loop)).toBe(false)
169
+ })
170
+
171
+ test('object-rest bare value use in a text expression ({String(rest)}) → false', () => {
172
+ const { loop } = compileLoop(`
173
+ 'use client'
174
+ import { createSignal } from '@barefootjs/client'
175
+
176
+ type Task = { id: string; title: string; flag: string }
177
+ export function RestBareText() {
178
+ const [tasks, setTasks] = createSignal<Task[]>([])
179
+ return (
180
+ <ul onClick={() => setTasks(t => t)}>
181
+ {tasks().map(({ id, ...rest }) => (
182
+ <li key={id}>{String(rest)}</li>
183
+ ))}
184
+ </ul>
185
+ )
186
+ }
187
+ `)
188
+ expect(isLowerableLoopDestructure(loop)).toBe(false)
189
+ })
190
+
191
+ test('object-rest bare value use in an event handler (onClick={() => fn(rest)}) → false', () => {
192
+ const { loop } = compileLoop(`
193
+ 'use client'
194
+ import { createSignal } from '@barefootjs/client'
195
+
196
+ type Task = { id: string; title: string; flag: string }
197
+ export function RestBareHandler() {
198
+ const [tasks, setTasks] = createSignal<Task[]>([])
199
+ const fn = (t: unknown) => {}
200
+ return (
201
+ <ul>
202
+ {tasks().map(({ id, ...rest }) => (
203
+ <li key={id} onClick={() => fn(rest)}>{id}</li>
204
+ ))}
205
+ </ul>
206
+ )
207
+ }
208
+ `)
209
+ expect(isLowerableLoopDestructure(loop)).toBe(false)
210
+ })
211
+
212
+ test('.filter(...).map(({a}) => ...) chain → false', () => {
213
+ const { loop } = compileLoop(`
214
+ 'use client'
215
+ import { createSignal } from '@barefootjs/client'
216
+
217
+ type Task = { id: string; done: boolean }
218
+ export function FilterChain() {
219
+ const [tasks, setTasks] = createSignal<Task[]>([])
220
+ return (
221
+ <ul onClick={() => setTasks(t => t)}>
222
+ {tasks().filter(t => !t.done).map(({ id }) => (
223
+ <li key={id}>{id}</li>
224
+ ))}
225
+ </ul>
226
+ )
227
+ }
228
+ `)
229
+ expect(isLowerableLoopDestructure(loop)).toBe(false)
230
+ })
231
+
232
+ test('binding named __bf_item → false', () => {
233
+ const { loop } = compileLoop(`
234
+ 'use client'
235
+ import { createSignal } from '@barefootjs/client'
236
+
237
+ export function ReservedName() {
238
+ const [rows, setRows] = createSignal<{ __bf_item: string }[]>([])
239
+ return (
240
+ <ul onClick={() => setRows(r => r)}>
241
+ {rows().map(({ __bf_item }) => (
242
+ <li key={__bf_item}>{__bf_item}</li>
243
+ ))}
244
+ </ul>
245
+ )
246
+ }
247
+ `)
248
+ expect(isLowerableLoopDestructure(loop)).toBe(false)
249
+ })
250
+
251
+ test('computed property key still raises BF025 and produces no paramBindings (unchanged)', () => {
252
+ const { loop, errors } = compileLoop(`
253
+ 'use client'
254
+ import { createSignal } from '@barefootjs/client'
255
+
256
+ const KEY = 'a' as const
257
+ export function Computed() {
258
+ const [items, setItems] = createSignal<Record<string, number>[]>([])
259
+ return (
260
+ <ul onClick={() => setItems(i => i)}>
261
+ {items().map(({ [KEY]: v }) => <li key={v}>{v}</li>)}
262
+ </ul>
263
+ )
264
+ }
265
+ `)
266
+ const bf025 = errors.filter(e => e.code === ErrorCodes.UNSUPPORTED_DESTRUCTURE_REST)
267
+ expect(bf025.length).toBe(1)
268
+ expect(bf025[0].severity).toBe('error')
269
+ expect(loop.paramBindings).toBeUndefined()
270
+ expect(isLowerableLoopDestructure(loop)).toBe(false)
271
+ })
272
+
273
+ test('deprecated alias isLowerableObjectRestDestructure is the same function', () => {
274
+ expect(isLowerableObjectRestDestructure).toBe(isLowerableLoopDestructure)
275
+ })
276
+
277
+ test('segments: nested { id, cells: [head, ...rest] } pins field/index/non-ident classification', () => {
278
+ const { loop } = compileLoop(`
279
+ 'use client'
280
+ import { createSignal } from '@barefootjs/client'
281
+
282
+ type Row = { id: string; cells: readonly string[]; 'data-priority': string }
283
+ export function NestedSegments() {
284
+ const [rows, setRows] = createSignal<Row[]>([])
285
+ return (
286
+ <ul onClick={() => setRows(r => r)}>
287
+ {rows().map(({ id, cells: [head, ...rest], 'data-priority': prio }) => (
288
+ <li key={id}>{head}:{String(rest.length)}:{prio}</li>
289
+ ))}
290
+ </ul>
291
+ )
292
+ }
293
+ `)
294
+ const bindings = loop.paramBindings
295
+ expect(bindings).toBeDefined()
296
+
297
+ const idBinding = bindings!.find(b => b.name === 'id')
298
+ expect(idBinding?.segments).toEqual([{ kind: 'field', key: 'id', isIdent: true }])
299
+
300
+ const headBinding = bindings!.find(b => b.name === 'head')
301
+ expect(headBinding?.segments).toEqual([
302
+ { kind: 'field', key: 'cells', isIdent: true },
303
+ { kind: 'index', index: 0 },
304
+ ])
305
+
306
+ const restBinding = bindings!.find(b => b.name === 'rest')
307
+ expect(restBinding?.rest?.kind).toBe('array')
308
+ expect(restBinding?.segments).toEqual([{ kind: 'field', key: 'cells', isIdent: true }])
309
+
310
+ const prioBinding = bindings!.find(b => b.name === 'prio')
311
+ expect(prioBinding?.segments).toEqual([{ kind: 'field', key: 'data-priority', isIdent: false }])
312
+ })
313
+ })