@barefootjs/xslate 0.31.4 → 0.31.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/xslate",
3
- "version": "0.31.4",
3
+ "version": "0.31.6",
4
4
  "description": "Text::Xslate (Kolon) adapter for BarefootJS — compiles IR to .tx templates and ships the Xslate rendering backend; runs under any PSGI/Plack app",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -55,7 +55,7 @@
55
55
  "directory": "packages/adapter-xslate"
56
56
  },
57
57
  "dependencies": {
58
- "@barefootjs/shared": "0.31.4"
58
+ "@barefootjs/shared": "0.31.6"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@barefootjs/jsx": ">=0.2.0",
@@ -72,9 +72,9 @@
72
72
  },
73
73
  "devDependencies": {
74
74
  "@barefootjs/adapter-tests": "0.1.0",
75
- "@barefootjs/jsx": "0.31.4",
76
- "@barefootjs/vite": "0.31.4",
77
- "@barefootjs/client": "0.31.4",
75
+ "@barefootjs/jsx": "0.31.6",
76
+ "@barefootjs/vite": "0.31.6",
77
+ "@barefootjs/client": "0.31.6",
78
78
  "typescript": "^5.0.0",
79
79
  "vite": "^6.0.0"
80
80
  }
@@ -458,10 +458,11 @@ export function Parent() {
458
458
  // `ir.metadata.localConstants` with no notion of AST scope — it used to
459
459
  // substitute an outer const's literal value even at an occurrence that is
460
460
  // actually an enclosing loop callback's own (shadowing) parameter, so every
461
- // iteration rendered the same hard-coded literal. Guarded with the same
462
- // coarse `collectLoopBoundNames` exclusion as #2212: any name a loop binds
463
- // anywhere in the component never inlines, falling back to the bare
464
- // identifier.
461
+ // iteration rendered the same hard-coded literal. Guarded via the threaded,
462
+ // position-accurate `BindingScope` (#2482 Stage 2 previously a coarse
463
+ // whole-component exclusion, same as #2212's, that also suppressed a
464
+ // genuinely non-shadowed occurrence outside the loop; the threaded scope
465
+ // only shadows AT the enclosing loop's own body).
465
466
  describe('XslateAdapter - const inlining vs loop-param shadowing (#2221)', () => {
466
467
  test('a loop param shadowing an outer literal const emits the identifier, not the const value', () => {
467
468
  const { template } = compileAndGenerate(`
@@ -500,11 +501,11 @@ function Widget({ values }: { values: number[] }) {
500
501
  expect(template).toContain('1 + 5')
501
502
  })
502
503
 
503
- // The accepted coarse-exclusion trade-off (same as #2212): a name that is
504
- // loop-bound ANYWHERE in the component never inlines, even at a genuinely
505
- // non-shadowed occurrence outside the loop the bare identifier is
506
- // emitted instead of the value.
507
- test('a const referenced outside the loop whose name is loop-bound elsewhere falls back to the identifier (accepted trade-off)', () => {
504
+ // Position-accurate scope (#2482 Stage 2): a name that is loop-bound
505
+ // elsewhere in the component but NOT at THIS occurrence still inlines
506
+ // here the coarse whole-component exclusion this used to hit (and
507
+ // accept as a trade-off) is gone; only the loop's own body shadows.
508
+ test('a const referenced outside the loop whose name is loop-bound elsewhere still inlines outside, stays the identifier inside', () => {
508
509
  const { template } = compileAndGenerate(`
509
510
  function Widget({ values }: { values: number[] }) {
510
511
  const label: string = 'x'
@@ -514,7 +515,7 @@ function Widget({ values }: { values: number[] }) {
514
515
  </div>
515
516
  }
516
517
  `)
517
- expect(template).not.toContain("1 + 'x'")
518
+ expect(template).toContain("1 + 'x'")
518
519
  expect(template).toContain('2 + $label')
519
520
  })
520
521
  })
@@ -526,9 +527,8 @@ function Widget({ values }: { values: number[] }) {
526
527
  // substitute the outer const's member value even at an occurrence that is
527
528
  // actually an enclosing loop callback's own (shadowing) parameter, so every
528
529
  // iteration rendered the same hard-coded literal instead of the per-item
529
- // value. Guarded with the same coarse `staticLoopSourceBoundNames`
530
- // exclusion as #2221: any name a loop binds anywhere in the component
531
- // never inlines, falling back to the bare `$cfg.x` member expression.
530
+ // value. Guarded via the threaded, position-accurate `BindingScope` (#2482
531
+ // Stage 2), same as #2221's fix above.
532
532
  describe('XslateAdapter - record-literal member lookup vs loop-param shadowing (#2237)', () => {
533
533
  test('a loop param shadowing an outer module object const emits the member access, not the outer literal', () => {
534
534
  const { template } = compileAndGenerate(`
@@ -553,12 +553,12 @@ function Widget({ variant }: { variant: 'solid' | 'ghost' }) {
553
553
  expect(template).toContain("<: 'bg-ghost' :>")
554
554
  })
555
555
 
556
- // The accepted coarse-exclusion trade-off (same as #2221/#2212): an
557
- // object name that is loop-bound ANYWHERE in the component never
558
- // inlines its member lookups, even at a genuinely non-shadowed
559
- // occurrence outside the loop the bare member expression is emitted
560
- // instead of the value.
561
- test('a record member referenced outside the loop whose object name is loop-bound elsewhere falls back to the member expression (accepted trade-off)', () => {
556
+ // Position-accurate scope (#2482 Stage 2): an object name that is
557
+ // loop-bound elsewhere in the component but NOT at THIS occurrence still
558
+ // inlines its member lookup here the coarse whole-component exclusion
559
+ // this used to hit (and accept as a trade-off) is gone; only the loop's
560
+ // own body shadows.
561
+ test('a record member referenced outside the loop whose object name is loop-bound elsewhere still inlines outside, stays the member expression inside', () => {
562
562
  const { template } = compileAndGenerate(`
563
563
  const cfg = { x: 'outer-lit' }
564
564
  function Widget({ rows }: { rows: { x: string }[] }) {
@@ -568,11 +568,49 @@ function Widget({ rows }: { rows: { x: string }[] }) {
568
568
  </div>
569
569
  }
570
570
  `)
571
- expect(template).not.toContain("<: 'outer-lit' :>")
571
+ expect(template).toContain("<: 'outer-lit' :>")
572
572
  expect(template).toContain('<: $cfg.x :>')
573
573
  })
574
574
  })
575
575
 
576
+ // Copilot review on #2600 (#2482 Stage 2 follow-up): the position-accurate
577
+ // `this.scope` (replacing the old coarse `staticLoopSourceBoundNames` set)
578
+ // was only threaded around `renderChildren(loop.children)` in `renderLoop`
579
+ // — narrower than the coarse set's always-on coverage. Two row-context
580
+ // conversions sit OUTSIDE that window: a `.map()` preamble local's own
581
+ // initializer, and the whole-item-conditional `loop-i:` key anchor
582
+ // (`loop.key`). Both genuinely evaluate PER ROW and must see the row's own
583
+ // bindings — a loop param shadowing a same-named outer const must resolve
584
+ // to the row value there too, exactly as it already does inside the loop
585
+ // body proper. `renderLoop` now enters the row scope before converting
586
+ // either and pops it after, closing the gap.
587
+ describe('XslateAdapter - row scope covers preamble/key conversions outside renderChildren (#2482 Stage 2 Copilot follow-up)', () => {
588
+ test('a loop param shadowing an outer const resolves to the row value inside a whole-item-conditional loop-i: key anchor', () => {
589
+ const { template } = compileAndGenerate(`
590
+ function Widget({ values }: { values: number[] }) {
591
+ const label: string = 'x'
592
+ return <ul>{values.map((label) => (label > 0 ? <li key={label}>{label}</li> : null))}</ul>
593
+ }
594
+ `)
595
+ expect(template).toContain('$bf.comment("loop-i:" ~ $label)')
596
+ expect(template).not.toContain('$bf.comment("loop-i:" ~ \'x\')')
597
+ })
598
+
599
+ test('a loop param shadowing an outer const resolves to the row value inside a .map() preamble local initializer', () => {
600
+ const { template } = compileAndGenerate(`
601
+ function Widget({ values }: { values: number[] }) {
602
+ const label: string = 'x'
603
+ return <ul>{values.map((label) => {
604
+ const cls = label
605
+ return <li key={label} class={cls}>{label}</li>
606
+ })}</ul>
607
+ }
608
+ `)
609
+ expect(template).toContain(': my $cls = $label;')
610
+ expect(template).not.toContain(": my $cls = 'x';")
611
+ })
612
+ })
613
+
576
614
  describe('XslateAdapter - scriptAssets (Vite late-binding, PR1)', () => {
577
615
  const CLIENT_COMPONENT = `
578
616
  'use client'
@@ -78,8 +78,8 @@ import {
78
78
  dangerousInnerHtmlMetacharViolation,
79
79
  dangerousInnerHtmlDiagnostic,
80
80
  resolveStaticLoopSource,
81
- collectLoopBoundNames,
82
81
  derivesScopeFromSlot,
82
+ BindingScope,
83
83
  } from '@barefootjs/jsx'
84
84
  import { isAriaBooleanAttr, isBooleanResultExpr } from './boolean-result.ts'
85
85
  import ts from 'typescript'
@@ -246,30 +246,24 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
246
246
  private localConstants: IRMetadata['localConstants'] = []
247
247
 
248
248
  /**
249
- * Every name a `.map()`/`.filter()` loop callback binds as its item/index
250
- * parameter anywhere in the component (#2208 fable review). A static
251
- * loop-SOURCE name (e.g. a function-scope `const items = [...]`) must
252
- * never resolve through `resolveStaticLoopSource` at a use site where a
253
- * DIFFERENT, enclosing loop's own callback param shadows it — same
254
- * shadowing hazard, and same coarse-but-safe mitigation, as #2212's
255
- * `collectLoopBoundNames` use in `collectStringValueNames`.
249
+ * The one canonical, position-accurate "names bound by an enclosing loop
250
+ * callback" service (#2482 Stage 2) replaces the two ad-hoc devices
251
+ * this adapter used to carry side by side: a coarse whole-component
252
+ * shadow-name Set (built once at `generate()` entry, used only to guard
253
+ * static-const inlining) and a ref-counted, position-accurate live map
254
+ * (pushed/popped around `renderLoop`'s body, used for the boolean-prop/
255
+ * nullable-optional classification sites (#2488) and `emitSpread`'s
256
+ * local-const fallback (#2489)). Both consulted the "is this name
257
+ * loop-bound" question with DIFFERENT meanings — the coarse/live drift
258
+ * #2482 Stage 2 ends. Every shadow-guard site now reads this ONE
259
+ * threaded, immutable scope: `enterLoopRow`/pop-by-reference around
260
+ * `renderChildren(loop.children)` in `renderLoop`, mirroring the Stage
261
+ * 1a/1b `ctx.scope` precedent in `jsx-to-ir.ts`. `IRLoop` already
262
+ * structurally satisfies `LoopBindingSource`
263
+ * (`param`/`index`/`paramBindings`/`preamble`), so `renderLoop` passes
264
+ * the loop node straight to `enterLoopRow`.
256
265
  */
257
- private staticLoopSourceBoundNames: Set<string> = new Set()
258
-
259
- /**
260
- * Names currently bound by an enclosing loop body — the block-param
261
- * locals `renderLoop` introduces (item, index, per-binding destructure
262
- * fields, `.map()` preamble declarations) — ref-counted so nested loops
263
- * compose. This is the POSITION-ACCURATE twin of the coarse
264
- * `staticLoopSourceBoundNames` above: that set is a whole-component
265
- * union used only to guard static-const inlining (safe to over-suppress
266
- * there — the fallback is just "don't inline"), whereas the boolean-prop
267
- * and nullable-optional classification sites (#2488) and `emitSpread`'s
268
- * local-const fallback (#2489) need to know whether a name is loop-bound
269
- * AT THIS POSITION, because for those the coarse fallback would silently
270
- * mis-render a genuine occurrence of the same name outside the loop too.
271
- */
272
- private loopBoundNames: Map<string, number> = new Map()
266
+ private scope: BindingScope = BindingScope.EMPTY
273
267
 
274
268
  /**
275
269
  * Optional, no-default props that are `undef` when the caller omits them.
@@ -303,8 +297,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
303
297
  // Per-compile prop classifications (see `props/prop-classes.ts`).
304
298
  this.booleanTypedProps = collectBooleanTypedProps(ir)
305
299
  this.localConstants = ir.metadata.localConstants ?? []
306
- this.staticLoopSourceBoundNames = collectLoopBoundNames(ir)
307
- this.loopBoundNames.clear()
300
+ this.scope = BindingScope.EMPTY
308
301
  this.nullableOptionalProps = collectNullableOptionalProps(ir)
309
302
  this.stringValueNames = collectStringValueNames(ir)
310
303
  this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants)
@@ -808,9 +801,11 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
808
801
  // bare identifier expression below, same as before #2208 — which
809
802
  // still trips the pre-existing BF101 gate for an unresolvable local
810
803
  // const reference (a loud, conservative refusal, not a silent wrong
811
- // value).
804
+ // value). Canonical, position-accurate predicate (#2482 Stage 2) — the
805
+ // enclosing loop scope's own membership, not a coarse whole-component
806
+ // union.
812
807
  const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
813
- isNameShadowed: name => this.staticLoopSourceBoundNames.has(name),
808
+ isNameShadowed: this.scope.asShadowPredicate(),
814
809
  })
815
810
  const staticArray = staticItems !== null ? staticValueToKolon(staticItems) : null
816
811
 
@@ -826,6 +821,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
826
821
  suggestion: {
827
822
  message:
828
823
  'Pre-compute the array server-side and pass it as a prop, or mark the loop position as @client-only so it runs in JS on the client.',
824
+ escape: [{ kind: 'prop-precompute' }, { kind: 'client-directive' }],
829
825
  },
830
826
  })
831
827
  }
@@ -925,6 +921,34 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
925
921
  // loop-child naming convention). renderedChildren above was computed with
926
922
  // the previous flag; recompute under the loop flag.
927
923
 
924
+ // This loop's row scope, for the position-accurate shadow guard
925
+ // (#2488/#2489, canonicalized on `BindingScope` in #2482 Stage 2).
926
+ // `IRLoop` already structurally satisfies `LoopBindingSource`
927
+ // (`param`/`index`/`paramBindings`/`preamble`) — `enterLoopRow(loop)`
928
+ // binds exactly the for-header target(s), each destructure binding,
929
+ // the index (when present), and the `.map()` preamble's declared
930
+ // locals, mirroring what THIS renderLoop's for-header + `indexLocalLines`
931
+ // actually introduce. Restored by reference (immutable — no ref-count
932
+ // bookkeeping) so nested loops compose for free. Entered BEFORE
933
+ // `preambleLines`/`bodyChildren` are converted and popped AFTER, not
934
+ // just around `renderChildren` — Copilot review on #2600 caught that
935
+ // the narrower window silently regressed the module-const shadow guard
936
+ // (`_resolveLiteralConst`/`_resolveStaticRecordLiteral`, now
937
+ // scope-driven instead of the old coarse whole-component set) for two
938
+ // row-context conversions that sit OUTSIDE `renderChildren`: a
939
+ // `.map()` preamble local's own initializer (`d.raw`, below) and the
940
+ // whole-item-conditional `loop-i:` key anchor (`loop.key`, in
941
+ // `bodyChildren` below) — both genuinely evaluate PER ROW and must see
942
+ // the row's own bindings. `loop.filterPredicate` deliberately stays
943
+ // OUTSIDE this window (further down, after the pop): per the Stage 0
944
+ // design a filter/sort callback's own param is a separate `callback`
945
+ // frame, never folded into the `.map()` row — and empirically
946
+ // `XslateFilterEmitter` (the filter predicate's dedicated,
947
+ // self-contained emitter) never consults `this.scope` at all, so its
948
+ // position relative to the pop is inert either way.
949
+ const prevScope = this.scope
950
+ this.scope = prevScope.enterLoopRow(loop)
951
+
928
952
  // Per-row locals for a `.map()` callback preamble (#2447), in source
929
953
  // order so a later initializer sees an earlier local — same as the
930
954
  // source block. Phase 1 refuses the loop outright when the preamble
@@ -933,40 +957,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
933
957
  const preambleLines = (loop.preamble?.declarations ?? []).map(
934
958
  d => `: my $${d.name} = ${this.convertExpressionToKolon(d.raw, d.valueParsed)};`,
935
959
  )
936
- // Names this loop binds in body scope, for the position-accurate
937
- // `loopBoundNames` guard (#2488) — mirrors the ERB adapter's `loopBound`
938
- // derivation, adapted to what THIS renderLoop actually binds: the
939
- // for-header target(s) (`loopVar` / `objectIteration` key+value /
940
- // `iterationShape === 'keys'`'s `param`), each `indexLocalLines` name
941
- // (explicit index, or a destructure binding), and the `.map()`
942
- // preamble's declared locals. Ref-counted so nested loops compose.
943
- const loopBound: string[] = []
944
- if (loop.objectIteration === 'entries') {
945
- // `.kv()` binds the pair var the key/value locals are derived from (#2488).
946
- loopBound.push('__bf_pair', loop.index ?? param, param)
947
- } else if (loop.objectIteration === 'keys' || loop.objectIteration === 'values') {
948
- loopBound.push(param)
949
- } else if (loop.iterationShape === 'keys') {
950
- // The header still binds the throwaway loop var; `param` is only the
951
- // index alias derived from it beneath (#2488).
952
- loopBound.push('__bf_item', param)
953
- } else if (supportableDestructure) {
954
- loopBound.push('__bf_item', ...(loop.paramBindings ?? []).map(b => b.name))
955
- if (loop.index) loopBound.push(loop.index)
956
- } else {
957
- loopBound.push(param)
958
- if (loop.index) loopBound.push(loop.index)
959
- }
960
- for (const d of loop.preamble?.declarations ?? []) loopBound.push(d.name)
961
- for (const n of loopBound) {
962
- this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1)
963
- }
964
960
  const childrenUnderLoop = this.renderChildren(loop.children)
965
- for (const n of loopBound) {
966
- const c = (this.loopBoundNames.get(n) ?? 1) - 1
967
- if (c <= 0) this.loopBoundNames.delete(n)
968
- else this.loopBoundNames.set(n, c)
969
- }
970
961
  this.currentLoopKeyDepth = prevLoopKeyDepth
971
962
  this.inLoop = prevInLoop
972
963
  void renderedChildren
@@ -974,11 +965,13 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
974
965
  // Whole-item conditional: prepend an always-present `<!--bf-loop-i:KEY-->`
975
966
  // anchor before each item's (possibly empty) conditional content so the
976
967
  // client's `mapArrayAnchored` can hydrate every SSR-rendered item by its
977
- // anchor.
968
+ // anchor. Still under the row scope (see above) — `loop.key` is a
969
+ // per-row expression.
978
970
  const bodyChildren =
979
971
  loop.bodyIsItemConditional && loop.key
980
972
  ? `<: $bf.comment("loop-i:" ~ ${this.convertExpressionToKolon(loop.key)}) | mark_raw :>\n${childrenUnderLoop}`
981
973
  : childrenUnderLoop
974
+ this.scope = prevScope
982
975
 
983
976
  const lines: string[] = []
984
977
  // Scoped per-call-site marker so sibling `.map()`s under the same parent
@@ -1441,13 +1434,13 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
1441
1434
  // function-scope (`!isModule`) consts whose value is NOT itself a bare
1442
1435
  // identifier (loop guard) are considered.
1443
1436
  //
1444
- // `loopBoundNames` guard (#2489): an enclosing `.map()` callback's own
1445
- // param can shadow this outer const's name (`.map((attrs) => <p
1437
+ // `this.scope` shadow guard (#2489): an enclosing `.map()` callback's
1438
+ // own param can shadow this outer const's name (`.map((attrs) => <p
1446
1439
  // {...attrs} />)`) — without the guard this forwarded the OUTER
1447
1440
  // const's value at every iteration instead of the per-item value.
1448
- // Must be the LIVE map, not `staticLoopSourceBoundNames` — the same
1449
- // name spread at ROOT must still resolve the const.
1450
- if (/^[A-Za-z_$][\w$]*$/.test(trimmed) && !this.loopBoundNames.has(trimmed)) {
1441
+ // Must be the threaded, position-accurate scope — the same name
1442
+ // spread at ROOT (no enclosing loop) must still resolve the const.
1443
+ if (/^[A-Za-z_$][\w$]*$/.test(trimmed) && !this.scope.isBound(trimmed)) {
1451
1444
  const localConst = (this.localConstants ?? []).find(
1452
1445
  c => c.name === trimmed && !c.isModule,
1453
1446
  )
@@ -1845,9 +1838,9 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
1845
1838
  return this.booleanTypedProps.has(bare)
1846
1839
  }
1847
1840
 
1848
- /** Position-accurate loop-bound-name check — see `loopBoundNames`'s docstring. */
1841
+ /** Position-accurate loop-bound-name check — see `this.scope`'s docstring. */
1849
1842
  private isLoopBoundName(name: string): boolean {
1850
- return this.loopBoundNames.has(name)
1843
+ return this.scope.isBound(name)
1851
1844
  }
1852
1845
 
1853
1846
  /**
@@ -1857,17 +1850,16 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
1857
1850
  * stash, so a bare `$totalPages` renders empty.
1858
1851
  *
1859
1852
  * The lookup is a flat name match with no notion of AST scope, so a
1860
- * name that any loop callback binds as its item/index param never
1861
- * inlines (#2221) — the occurrence may be the loop's own (shadowing)
1862
- * binding, and substituting the outer const's value there renders every
1863
- * iteration with the same hard-coded literal. Coarse (a genuinely
1864
- * non-shadowed same-named const elsewhere in the component also stops
1865
- * inlining, falling back to the bare identifier) but safe — the same
1866
- * trade-off as #2212's `collectLoopBoundNames` use in
1867
- * `collectStringValueNames`.
1853
+ * name bound by the CURRENTLY ENCLOSING loop's item/index/destructure/
1854
+ * preamble param never inlines (#2221) — the occurrence may be the
1855
+ * loop's own (shadowing) binding, and substituting the outer const's
1856
+ * value there renders every iteration with the same hard-coded literal.
1857
+ * Position-accurate via the threaded `this.scope` (#2482 Stage 2) — a
1858
+ * same-named const elsewhere in the component, outside any loop that
1859
+ * shadows it, still inlines.
1868
1860
  */
1869
1861
  private _resolveLiteralConst(name: string): string | null {
1870
- if (this.staticLoopSourceBoundNames.has(name)) return null
1862
+ if (this.scope.isBound(name)) return null
1871
1863
  const c = (this.localConstants ?? []).find(lc => lc.name === name)
1872
1864
  if (c?.value === undefined) return null
1873
1865
  const v = c.value.trim()
@@ -1885,15 +1877,16 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
1885
1877
  * scope, so an enclosing loop callback's own param of the same name
1886
1878
  * (`.map((cfg) => <li>{cfg.x}</li>)` shadowing a module `const cfg = {…}`)
1887
1879
  * still resolved to the OUTER const's member value at every iteration
1888
- * (#2237) — the sibling hazard to #2221's `_resolveLiteralConst`. Same
1889
- * coarse-but-safe `staticLoopSourceBoundNames` guard: any name a loop
1890
- * binds anywhere in the component never inlines, falling back to the bare
1891
- * `$cfg.x` member expression (which an Xslate `: for` loop binds
1892
- * correctly at the shadowed occurrences).
1880
+ * (#2237) — the sibling hazard to #2221's `_resolveLiteralConst`.
1881
+ * Position-accurate via the threaded `this.scope` (#2482 Stage 2), passed
1882
+ * as `lookupStaticRecordLiteral`'s required guard: a name the CURRENTLY
1883
+ * ENCLOSING loop binds never inlines, falling back to the bare `$cfg.x`
1884
+ * member expression (which an Xslate `: for` loop binds correctly at the
1885
+ * shadowed occurrence); a same-named const elsewhere, outside any loop
1886
+ * that shadows it, still inlines.
1893
1887
  */
1894
1888
  private _resolveStaticRecordLiteral(objectName: string, key: string): string | null {
1895
- if (this.staticLoopSourceBoundNames.has(objectName)) return null
1896
- const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants)
1889
+ const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants, name => this.scope.isBound(name))
1897
1890
  if (!hit) return null
1898
1891
  return hit.kind === 'number'
1899
1892
  ? hit.text
@@ -13,6 +13,9 @@ export const conformancePins: ConformancePins = {
13
13
  // JS-runtime target runs it, a DSL adapter surfaces BF021 + `/* @client */`.
14
14
  // See spec/callback-fidelity.md.
15
15
  'filter-typeof-predicate': [{ code: 'BF021', severity: 'error' }],
16
+ // Array-builder `.map()` body (imperative `push`-into-array preamble):
17
+ // BF021, with a verified `/* @client */` escape — `map-array-builder-
18
+ // body-client` (#2613). See that fixture's docstring.
16
19
  'map-array-builder-body': [{ code: 'BF021', severity: 'error' }],
17
20
  'map-array-builder-escaping': [{ code: 'BF021', severity: 'error' }],
18
21
  // `.fill(value)` mutates the receiver in place — no template lowering
@@ -70,14 +73,22 @@ export const conformancePins: ConformancePins = {
70
73
  // simply unreachable before because BF104 refused the destructure shape
71
74
  // first.
72
75
  'static-array-from-props': [
73
- { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2321' },
76
+ {
77
+ code: 'BF101',
78
+ severity: 'error',
79
+ issue: 'https://github.com/piconic-ai/barefootjs/issues/2321',
80
+ },
74
81
  ],
75
82
  // The BF101 above fires; BF104 no longer does (see above), and BF103
76
83
  // (sibling-imported `<Tag>` child component in the loop body) no longer
77
84
  // does either now that the conformance harness passes
78
85
  // `siblingTemplatesRegistered: true` (#2205).
79
86
  'static-array-from-props-with-component': [
80
- { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2321' },
87
+ {
88
+ code: 'BF101',
89
+ severity: 'error',
90
+ issue: 'https://github.com/piconic-ai/barefootjs/issues/2321',
91
+ },
81
92
  ],
82
93
  // #1310 / #2087: rest destructure in .map() callback. All four shapes now
83
94
  // lower via #2087 Phase B's `segments`-walking accessor:
@@ -136,9 +147,7 @@ export const conformancePins: ConformancePins = {
136
147
  'filter-nested-callback-predicate': [
137
148
  { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2320' },
138
149
  ],
139
- 'filter-nested-find-predicate': [
140
- { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2320' },
141
- ],
150
+ 'filter-nested-find-predicate': [{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2320' }],
142
151
  // NB: TOP-LEVEL `.find` / `.findIndex` / `.findLast` / `.findLastIndex`
143
152
  // (text position) are NOT pinned here — unlike mojo (which refuses them),
144
153
  // Xslate lowers them to `$bf.find` / `find_index` / `find_last` /