@barefootjs/rust 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/rust",
3
- "version": "0.31.4",
3
+ "version": "0.31.6",
4
4
  "description": "minijinja (Rust) adapter for BarefootJS — compiles IR to .j2 templates and ships a Rust rendering runtime (packages/adapter-rust/runtime/); runs under any Rust web framework (axum, etc.)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -54,7 +54,7 @@
54
54
  "directory": "packages/adapter-rust"
55
55
  },
56
56
  "dependencies": {
57
- "@barefootjs/shared": "0.31.4"
57
+ "@barefootjs/shared": "0.31.6"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "@barefootjs/jsx": ">=0.2.0",
@@ -71,9 +71,9 @@
71
71
  },
72
72
  "devDependencies": {
73
73
  "@barefootjs/adapter-tests": "0.1.0",
74
- "@barefootjs/jsx": "0.31.4",
75
- "@barefootjs/vite": "0.31.4",
76
- "@barefootjs/client": "0.31.4",
74
+ "@barefootjs/jsx": "0.31.6",
75
+ "@barefootjs/vite": "0.31.6",
76
+ "@barefootjs/client": "0.31.6",
77
77
  "typescript": "^5.0.0",
78
78
  "vite": "^6.0.0"
79
79
  }
@@ -416,10 +416,11 @@ export function Parent() {
416
416
  // `ir.metadata.localConstants` with no notion of AST scope — it used to
417
417
  // substitute an outer const's literal value even at an occurrence that is
418
418
  // actually an enclosing loop callback's own (shadowing) parameter, so every
419
- // iteration rendered the same hard-coded literal. Guarded with the same
420
- // coarse `collectLoopBoundNames` exclusion as #2212: any name a loop binds
421
- // anywhere in the component never inlines, falling back to the bare
422
- // identifier.
419
+ // iteration rendered the same hard-coded literal. Guarded via the threaded,
420
+ // position-accurate `BindingScope` (#2482 Stage 2 previously a coarse
421
+ // whole-component exclusion, same as #2212's, that also suppressed a
422
+ // genuinely non-shadowed occurrence outside the loop; the threaded scope
423
+ // only shadows AT the enclosing loop's own body).
423
424
  describe('MinijinjaAdapter - const inlining vs loop-param shadowing (#2221)', () => {
424
425
  test('a loop param shadowing an outer literal const emits the identifier, not the const value', () => {
425
426
  const { template } = compileAndGenerate(`
@@ -458,11 +459,11 @@ function Widget({ values }: { values: number[] }) {
458
459
  expect(template).toContain('1 + 5')
459
460
  })
460
461
 
461
- // The accepted coarse-exclusion trade-off (same as #2212): a name that is
462
- // loop-bound ANYWHERE in the component never inlines, even at a genuinely
463
- // non-shadowed occurrence outside the loop the bare identifier is
464
- // emitted instead of the value.
465
- test('a const referenced outside the loop whose name is loop-bound elsewhere falls back to the identifier (accepted trade-off)', () => {
462
+ // Position-accurate scope (#2482 Stage 2): a name that is loop-bound
463
+ // elsewhere in the component but NOT at THIS occurrence still inlines
464
+ // here the coarse whole-component exclusion this used to hit (and
465
+ // accept as a trade-off) is gone; only the loop's own body shadows.
466
+ test('a const referenced outside the loop whose name is loop-bound elsewhere still inlines outside, stays the identifier inside', () => {
466
467
  const { template } = compileAndGenerate(`
467
468
  function Widget({ values }: { values: number[] }) {
468
469
  const label: string = 'x'
@@ -472,7 +473,7 @@ function Widget({ values }: { values: number[] }) {
472
473
  </div>
473
474
  }
474
475
  `)
475
- expect(template).not.toContain("1 + 'x'")
476
+ expect(template).toContain("1 + 'x'")
476
477
  expect(template).toContain('2 + label')
477
478
  })
478
479
  })
@@ -484,9 +485,8 @@ function Widget({ values }: { values: number[] }) {
484
485
  // substitute the outer const's member value even at an occurrence that is
485
486
  // actually an enclosing loop callback's own (shadowing) parameter, so every
486
487
  // iteration rendered the same hard-coded literal instead of the per-item
487
- // value. Guarded with the same coarse `staticLoopSourceBoundNames`
488
- // exclusion as #2221: any name a loop binds anywhere in the component
489
- // never inlines, falling back to the bare `cfg.x` member expression.
488
+ // value. Guarded via the threaded, position-accurate `BindingScope` (#2482
489
+ // Stage 2), same as #2221's fix above.
490
490
  describe('MinijinjaAdapter - record-literal member lookup vs loop-param shadowing (#2237)', () => {
491
491
  test('a loop param shadowing an outer module object const emits the member access, not the outer literal', () => {
492
492
  const { template } = compileAndGenerate(`
@@ -511,12 +511,12 @@ function Widget({ variant }: { variant: 'solid' | 'ghost' }) {
511
511
  expect(template).toContain("bf.string('bg-ghost')")
512
512
  })
513
513
 
514
- // The accepted coarse-exclusion trade-off (same as #2221/#2212): an
515
- // object name that is loop-bound ANYWHERE in the component never
516
- // inlines its member lookups, even at a genuinely non-shadowed
517
- // occurrence outside the loop the bare member expression is emitted
518
- // instead of the value.
519
- test('a record member referenced outside the loop whose object name is loop-bound elsewhere falls back to the member expression (accepted trade-off)', () => {
514
+ // Position-accurate scope (#2482 Stage 2): an object name that is
515
+ // loop-bound elsewhere in the component but NOT at THIS occurrence still
516
+ // inlines its member lookup here the coarse whole-component exclusion
517
+ // this used to hit (and accept as a trade-off) is gone; only the loop's
518
+ // own body shadows.
519
+ test('a record member referenced outside the loop whose object name is loop-bound elsewhere still inlines outside, stays the member expression inside', () => {
520
520
  const { template } = compileAndGenerate(`
521
521
  const cfg = { x: 'outer-lit' }
522
522
  function Widget({ rows }: { rows: { x: string }[] }) {
@@ -526,11 +526,49 @@ function Widget({ rows }: { rows: { x: string }[] }) {
526
526
  </div>
527
527
  }
528
528
  `)
529
- expect(template).not.toContain("bf.string('outer-lit')")
529
+ expect(template).toContain("bf.string('outer-lit')")
530
530
  expect(template).toContain('bf.string(cfg.x)')
531
531
  })
532
532
  })
533
533
 
534
+ // Copilot review on #2600 (#2482 Stage 2 follow-up): the position-accurate
535
+ // `this.scope` (replacing the old coarse `staticLoopSourceBoundNames` set)
536
+ // was only threaded around `renderChildren(loop.children)` in `renderLoop`
537
+ // — narrower than the coarse set's always-on coverage. Two row-context
538
+ // conversions sit OUTSIDE that window: a `.map()` preamble local's own
539
+ // initializer, and the whole-item-conditional `loop-i:` key anchor
540
+ // (`loop.key`). Both genuinely evaluate PER ROW and must see the row's own
541
+ // bindings — a loop param shadowing a same-named outer const must resolve
542
+ // to the row value there too, exactly as it already does inside the loop
543
+ // body proper. `renderLoop` now enters the row scope before converting
544
+ // either and pops it after, closing the gap.
545
+ describe('MinijinjaAdapter - row scope covers preamble/key conversions outside renderChildren (#2482 Stage 2 Copilot follow-up)', () => {
546
+ test('a loop param shadowing an outer const resolves to the row value inside a whole-item-conditional loop-i: key anchor', () => {
547
+ const { template } = compileAndGenerate(`
548
+ function Widget({ values }: { values: number[] }) {
549
+ const label: string = 'x'
550
+ return <ul>{values.map((label) => (label > 0 ? <li key={label}>{label}</li> : null))}</ul>
551
+ }
552
+ `)
553
+ expect(template).toContain('bf.comment("loop-i:" ~ bf.string(label))')
554
+ expect(template).not.toContain("bf.comment(\"loop-i:\" ~ bf.string('x'))")
555
+ })
556
+
557
+ test('a loop param shadowing an outer const resolves to the row value inside a .map() preamble local initializer', () => {
558
+ const { template } = compileAndGenerate(`
559
+ function Widget({ values }: { values: number[] }) {
560
+ const label: string = 'x'
561
+ return <ul>{values.map((label) => {
562
+ const cls = label
563
+ return <li key={label} class={cls}>{label}</li>
564
+ })}</ul>
565
+ }
566
+ `)
567
+ expect(template).toContain('{% set cls = label %}')
568
+ expect(template).not.toContain("{% set cls = 'x' %}")
569
+ })
570
+ })
571
+
534
572
  describe('MinijinjaAdapter - fragment-root scope comment end marker (#2289)', () => {
535
573
  // A fragment-rooted component (multiple top-level nodes) uses a
536
574
  // comment-based scope marker instead of a real DOM element, so it needs a
@@ -170,8 +170,8 @@ import {
170
170
  dangerousInnerHtmlMetacharViolation,
171
171
  dangerousInnerHtmlDiagnostic,
172
172
  resolveStaticLoopSource,
173
- collectLoopBoundNames,
174
173
  derivesScopeFromSlot,
174
+ BindingScope,
175
175
  } from '@barefootjs/jsx'
176
176
  import { isAriaBooleanAttr, isBooleanResultExpr, isExplicitStringCall } from './boolean-result.ts'
177
177
  import type { ParsedExpr, LoweringMatcher, LoopBindingPathSegment } from '@barefootjs/jsx'
@@ -320,30 +320,24 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
320
320
  private localConstants: IRMetadata['localConstants'] = []
321
321
 
322
322
  /**
323
- * Every name a `.map()`/`.filter()` loop callback binds as its item/index
324
- * parameter anywhere in the component (#2208 fable review). A static
325
- * loop-SOURCE name (e.g. a function-scope `const items = [...]`) must
326
- * never resolve through `resolveStaticLoopSource` at a use site where a
327
- * DIFFERENT, enclosing loop's own callback param shadows it — same
328
- * shadowing hazard, and same coarse-but-safe mitigation, as #2212's
329
- * `collectLoopBoundNames` use in `collectStringValueNames`.
323
+ * The one canonical, position-accurate "names bound by an enclosing loop
324
+ * callback" service (#2482 Stage 2) replaces the two ad-hoc devices
325
+ * this adapter used to carry side by side: a coarse whole-component
326
+ * shadow-name Set (built once at `generate()` entry, used only to guard
327
+ * static-const inlining) and a ref-counted, position-accurate live map
328
+ * (pushed/popped around `renderLoop`'s body, used for the boolean-prop/
329
+ * nullable-optional classification sites (#2488) and `emitSpread`'s
330
+ * local-const fallback (#2489)). Both consulted the "is this name
331
+ * loop-bound" question with DIFFERENT meanings — the coarse/live drift
332
+ * #2482 Stage 2 ends. Every shadow-guard site now reads this ONE
333
+ * threaded, immutable scope: `enterLoopRow`/pop-by-reference around
334
+ * `renderChildren(loop.children)` in `renderLoop`, mirroring the Stage
335
+ * 1a/1b `ctx.scope` precedent in `jsx-to-ir.ts`. `IRLoop` already
336
+ * structurally satisfies `LoopBindingSource`
337
+ * (`param`/`index`/`paramBindings`/`preamble`), so `renderLoop` passes
338
+ * the loop node straight to `enterLoopRow`.
330
339
  */
331
- private staticLoopSourceBoundNames: Set<string> = new Set()
332
-
333
- /**
334
- * Names currently bound by an enclosing loop body — the block-param
335
- * locals `renderLoop` introduces (item, index, per-binding destructure
336
- * fields, `.map()` preamble declarations) — ref-counted so nested loops
337
- * compose. This is the POSITION-ACCURATE twin of the coarse
338
- * `staticLoopSourceBoundNames` above: that set is a whole-component
339
- * union used only to guard static-const inlining (safe to over-suppress
340
- * there — the fallback is just "don't inline"), whereas the boolean-prop
341
- * and nullable-optional classification sites (#2488) and `emitSpread`'s
342
- * local-const fallback (#2489) need to know whether a name is loop-bound
343
- * AT THIS POSITION, because for those the coarse fallback would silently
344
- * mis-render a genuine occurrence of the same name outside the loop too.
345
- */
346
- private loopBoundNames: Map<string, number> = new Map()
340
+ private scope: BindingScope = BindingScope.EMPTY
347
341
 
348
342
  /**
349
343
  * Optional, no-default props that are `None` when the caller omits them.
@@ -377,8 +371,7 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
377
371
  // ("True"/"False") (#1897, pagination's data-active).
378
372
  this.booleanTypedProps = collectBooleanTypedProps(ir)
379
373
  this.localConstants = ir.metadata.localConstants ?? []
380
- this.staticLoopSourceBoundNames = collectLoopBoundNames(ir)
381
- this.loopBoundNames.clear()
374
+ this.scope = BindingScope.EMPTY
382
375
  this.nullableOptionalProps = collectNullableOptionalProps(ir)
383
376
  this.stringValueNames = collectStringValueNames(ir)
384
377
  this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants)
@@ -878,9 +871,11 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
878
871
  // bare identifier expression below, same as before #2208 — which
879
872
  // still trips the pre-existing BF101 gate for an unresolvable local
880
873
  // const reference (a loud, conservative refusal, not a silent wrong
881
- // value).
874
+ // value). Canonical, position-accurate predicate (#2482 Stage 2) — the
875
+ // enclosing loop scope's own membership, not a coarse whole-component
876
+ // union.
882
877
  const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
883
- isNameShadowed: name => this.staticLoopSourceBoundNames.has(name),
878
+ isNameShadowed: this.scope.asShadowPredicate(),
884
879
  })
885
880
  const staticArray = staticItems !== null ? staticValueToMinijinja(staticItems) : null
886
881
 
@@ -896,6 +891,7 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
896
891
  suggestion: {
897
892
  message:
898
893
  '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.',
894
+ escape: [{ kind: 'prop-precompute' }, { kind: 'client-directive' }],
899
895
  },
900
896
  })
901
897
  }
@@ -985,6 +981,34 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
985
981
  // loop-child naming convention). renderedChildren above was computed with
986
982
  // the previous flag; recompute under the loop flag.
987
983
 
984
+ // This loop's row scope, for the position-accurate shadow guard
985
+ // (#2488/#2489, canonicalized on `BindingScope` in #2482 Stage 2).
986
+ // `IRLoop` already structurally satisfies `LoopBindingSource`
987
+ // (`param`/`index`/`paramBindings`/`preamble`) — `enterLoopRow(loop)`
988
+ // binds exactly the for-header target(s), each destructure binding,
989
+ // the index (when present), and the `.map()` preamble's declared
990
+ // locals, mirroring what THIS renderLoop's for-header + `indexLocalLines`
991
+ // actually introduce. Restored by reference (immutable — no ref-count
992
+ // bookkeeping) so nested loops compose for free. Entered BEFORE
993
+ // `preambleLines`/`bodyChildren` are converted and popped AFTER, not
994
+ // just around `renderChildren` — Copilot review on #2600 caught that
995
+ // the narrower window silently regressed the module-const shadow guard
996
+ // (`_resolveLiteralConst`/`_resolveStaticRecordLiteral`, now
997
+ // scope-driven instead of the old coarse whole-component set) for two
998
+ // row-context conversions that sit OUTSIDE `renderChildren`: a
999
+ // `.map()` preamble local's own initializer (`d.raw`, below) and the
1000
+ // whole-item-conditional `loop-i:` key anchor (`loop.key`, in
1001
+ // `bodyChildren` below) — both genuinely evaluate PER ROW and must see
1002
+ // the row's own bindings. `loop.filterPredicate` deliberately stays
1003
+ // OUTSIDE this window (further down, after the pop): per the Stage 0
1004
+ // design a filter/sort callback's own param is a separate `callback`
1005
+ // frame, never folded into the `.map()` row — and empirically
1006
+ // `JinjaFilterEmitter` (the filter predicate's dedicated,
1007
+ // self-contained emitter) never consults `this.scope` at all, so its
1008
+ // position relative to the pop is inert either way.
1009
+ const prevScope = this.scope
1010
+ this.scope = prevScope.enterLoopRow(loop)
1011
+
988
1012
  // Per-row locals for a `.map()` callback preamble (#2447), in source
989
1013
  // order so a later initializer sees an earlier local — same as the
990
1014
  // source block. Phase 1 refuses the loop outright when the preamble
@@ -993,42 +1017,7 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
993
1017
  const preambleLines = (loop.preamble?.declarations ?? []).map(
994
1018
  d => `{% set ${minijinjaIdent(d.name)} = ${this.convertExpressionToJinja(d.raw, d.valueParsed)} %}`,
995
1019
  )
996
- // Names this loop binds in body scope, for the position-accurate
997
- // `loopBoundNames` guard (#2488) — mirrors the ERB adapter's `loopBound`
998
- // derivation, adapted to what THIS renderLoop actually binds: the
999
- // for-header target(s) (`loopVar` / `objectIteration` key+value /
1000
- // `iterationShape === 'keys'`'s `param`), each `indexLocalLines` name
1001
- // (explicit index, or a destructure binding), and the `.map()`
1002
- // preamble's declared locals. Ref-counted so nested loops compose.
1003
- const loopBound: string[] = []
1004
- if (loop.objectIteration === 'entries') {
1005
- loopBound.push(loop.index ?? param, param)
1006
- } else if (loop.objectIteration === 'keys') {
1007
- // `|items` binds both slots; the unused one is a throwaway (#2488).
1008
- loopBound.push(param, '__bf_v')
1009
- } else if (loop.objectIteration === 'values') {
1010
- loopBound.push('__bf_k', param)
1011
- } else if (loop.iterationShape === 'keys') {
1012
- // The header still binds the throwaway loop var; `param` is only the
1013
- // index alias set beneath it (#2488).
1014
- loopBound.push('__bf_item', param)
1015
- } else if (supportableDestructure) {
1016
- loopBound.push('__bf_item', ...(loop.paramBindings ?? []).map(b => b.name))
1017
- if (loop.index) loopBound.push(loop.index)
1018
- } else {
1019
- loopBound.push(param)
1020
- if (loop.index) loopBound.push(loop.index)
1021
- }
1022
- for (const d of loop.preamble?.declarations ?? []) loopBound.push(d.name)
1023
- for (const n of loopBound) {
1024
- this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1)
1025
- }
1026
1020
  const childrenUnderLoop = this.renderChildren(loop.children)
1027
- for (const n of loopBound) {
1028
- const c = (this.loopBoundNames.get(n) ?? 1) - 1
1029
- if (c <= 0) this.loopBoundNames.delete(n)
1030
- else this.loopBoundNames.set(n, c)
1031
- }
1032
1021
  this.currentLoopKeyDepth = prevLoopKeyDepth
1033
1022
  this.inLoop = prevInLoop
1034
1023
  void renderedChildren
@@ -1036,11 +1025,13 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
1036
1025
  // Whole-item conditional: prepend an always-present `<!--bf-loop-i:KEY-->`
1037
1026
  // anchor before each item's (possibly empty) conditional content so the
1038
1027
  // client's `mapArrayAnchored` can hydrate every SSR-rendered item by its
1039
- // anchor.
1028
+ // anchor. Still under the row scope (see above) — `loop.key` is a
1029
+ // per-row expression.
1040
1030
  const bodyChildren =
1041
1031
  loop.bodyIsItemConditional && loop.key
1042
1032
  ? `{{ bf.comment("loop-i:" ~ bf.string(${this.convertExpressionToJinja(loop.key)})) | safe }}\n${childrenUnderLoop}`
1043
1033
  : childrenUnderLoop
1034
+ this.scope = prevScope
1044
1035
 
1045
1036
  const lines: string[] = []
1046
1037
  // Scoped per-call-site marker so sibling `.map()`s under the same parent
@@ -1527,13 +1518,13 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
1527
1518
  // function-scope (`!isModule`) consts whose value is NOT itself a bare
1528
1519
  // identifier (loop guard) are considered.
1529
1520
  //
1530
- // `loopBoundNames` guard (#2489): an enclosing `.map()` callback's own
1531
- // param can shadow this outer const's name (`.map((attrs) => <p
1521
+ // `this.scope` shadow guard (#2489): an enclosing `.map()` callback's
1522
+ // own param can shadow this outer const's name (`.map((attrs) => <p
1532
1523
  // {...attrs} />)`) — without the guard this forwarded the OUTER
1533
1524
  // const's value at every iteration instead of the per-item value.
1534
- // Must be the LIVE map, not `staticLoopSourceBoundNames` — the same
1535
- // name spread at ROOT must still resolve the const.
1536
- if (/^[A-Za-z_$][\w$]*$/.test(trimmed) && !this.loopBoundNames.has(trimmed)) {
1525
+ // Must be the threaded, position-accurate scope — the same name
1526
+ // spread at ROOT (no enclosing loop) must still resolve the const.
1527
+ if (/^[A-Za-z_$][\w$]*$/.test(trimmed) && !this.scope.isBound(trimmed)) {
1537
1528
  const localConst = (this.localConstants ?? []).find(
1538
1529
  c => c.name === trimmed && !c.isModule,
1539
1530
  )
@@ -1955,9 +1946,9 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
1955
1946
  return this.booleanTypedProps.has(bare)
1956
1947
  }
1957
1948
 
1958
- /** Position-accurate loop-bound-name check — see `loopBoundNames`'s docstring. */
1949
+ /** Position-accurate loop-bound-name check — see `this.scope`'s docstring. */
1959
1950
  private isLoopBoundName(name: string): boolean {
1960
- return this.loopBoundNames.has(name)
1951
+ return this.scope.isBound(name)
1961
1952
  }
1962
1953
 
1963
1954
  /**
@@ -1984,17 +1975,16 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
1984
1975
  * context, so a bare reference would resolve to Undefined.
1985
1976
  *
1986
1977
  * The lookup is a flat name match with no notion of AST scope, so a
1987
- * name that any loop callback binds as its item/index param never
1988
- * inlines (#2221) — the occurrence may be the loop's own (shadowing)
1989
- * binding, and substituting the outer const's value there renders every
1990
- * iteration with the same hard-coded literal. Coarse (a genuinely
1991
- * non-shadowed same-named const elsewhere in the component also stops
1992
- * inlining, falling back to the bare identifier) but safe — the same
1993
- * trade-off as #2212's `collectLoopBoundNames` use in
1994
- * `collectStringValueNames`.
1978
+ * name bound by the CURRENTLY ENCLOSING loop's item/index/destructure/
1979
+ * preamble param never inlines (#2221) — the occurrence may be the
1980
+ * loop's own (shadowing) binding, and substituting the outer const's
1981
+ * value there renders every iteration with the same hard-coded literal.
1982
+ * Position-accurate via the threaded `this.scope` (#2482 Stage 2) — a
1983
+ * same-named const elsewhere in the component, outside any loop that
1984
+ * shadows it, still inlines.
1995
1985
  */
1996
1986
  private _resolveLiteralConst(name: string): string | null {
1997
- if (this.staticLoopSourceBoundNames.has(name)) return null
1987
+ if (this.scope.isBound(name)) return null
1998
1988
  const c = (this.localConstants ?? []).find(lc => lc.name === name)
1999
1989
  if (c?.value === undefined) return null
2000
1990
  const v = c.value.trim()
@@ -2012,15 +2002,16 @@ export class MinijinjaAdapter extends BaseAdapter implements IRNodeEmitter<Jinja
2012
2002
  * scope, so an enclosing loop callback's own param of the same name
2013
2003
  * (`.map((cfg) => <li>{cfg.x}</li>)` shadowing a module `const cfg = {…}`)
2014
2004
  * still resolved to the OUTER const's member value at every iteration
2015
- * (#2237) — the sibling hazard to #2221's `_resolveLiteralConst`. Same
2016
- * coarse-but-safe `staticLoopSourceBoundNames` guard: any name a loop
2017
- * binds anywhere in the component never inlines, falling back to the bare
2018
- * `cfg.x` member expression (which a minijinja `for` loop binds correctly
2019
- * at the shadowed occurrences).
2005
+ * (#2237) — the sibling hazard to #2221's `_resolveLiteralConst`.
2006
+ * Position-accurate via the threaded `this.scope` (#2482 Stage 2), passed
2007
+ * as `lookupStaticRecordLiteral`'s required guard: a name the CURRENTLY
2008
+ * ENCLOSING loop binds never inlines, falling back to the bare `cfg.x`
2009
+ * member expression (which a minijinja `for` loop binds correctly at the
2010
+ * shadowed occurrence); a same-named const elsewhere, outside any loop
2011
+ * that shadows it, still inlines.
2020
2012
  */
2021
2013
  private _resolveStaticRecordLiteral(objectName: string, key: string): string | null {
2022
- if (this.staticLoopSourceBoundNames.has(objectName)) return null
2023
- const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants)
2014
+ const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants, name => this.scope.isBound(name))
2024
2015
  if (!hit) return null
2025
2016
  return hit.kind === 'number'
2026
2017
  ? hit.text
@@ -16,6 +16,9 @@ export const conformancePins: ConformancePins = {
16
16
  // JS-runtime target runs it, a DSL adapter surfaces BF021 + `/* @client */`.
17
17
  // See spec/callback-fidelity.md.
18
18
  'filter-typeof-predicate': [{ code: 'BF021', severity: 'error' }],
19
+ // Array-builder `.map()` body (imperative `push`-into-array preamble):
20
+ // BF021, with a verified `/* @client */` escape — `map-array-builder-
21
+ // body-client` (#2613). See that fixture's docstring.
19
22
  'map-array-builder-body': [{ code: 'BF021', severity: 'error' }],
20
23
  'map-array-builder-escaping': [{ code: 'BF021', severity: 'error' }],
21
24
  // `.fill(value)` mutates the receiver in place — no template lowering
@@ -82,13 +85,21 @@ export const conformancePins: ConformancePins = {
82
85
  // scope for #2087; tracked as a follow-up at
83
86
  // https://github.com/piconic-ai/barefootjs/issues/2321.
84
87
  'static-array-from-props': [
85
- { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2321' },
88
+ {
89
+ code: 'BF101',
90
+ severity: 'error',
91
+ issue: 'https://github.com/piconic-ai/barefootjs/issues/2321',
92
+ },
86
93
  ],
87
94
  // BF101 (unresolvable computed loop array, see above) fires; BF103
88
95
  // (imported child in the loop body) no longer does now that the
89
96
  // conformance harness passes `siblingTemplatesRegistered: true` (#2205).
90
97
  'static-array-from-props-with-component': [
91
- { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2321' },
98
+ {
99
+ code: 'BF101',
100
+ severity: 'error',
101
+ issue: 'https://github.com/piconic-ai/barefootjs/issues/2321',
102
+ },
92
103
  ],
93
104
  // Rest-destructure `.map()` callbacks (#2087 Phase B): every shape now
94
105
  // lowers natively — fixed bindings at any depth/shape via a chained
@@ -122,9 +133,7 @@ export const conformancePins: ConformancePins = {
122
133
  'filter-nested-callback-predicate': [
123
134
  { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2320' },
124
135
  ],
125
- 'filter-nested-find-predicate': [
126
- { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2320' },
127
- ],
136
+ 'filter-nested-find-predicate': [{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2320' }],
128
137
  // NB: TOP-LEVEL `.find` / `.findIndex` / `.findLast` / `.findLastIndex`
129
138
  // (text position) are NOT pinned here — like xslate (unlike mojo, which
130
139
  // refuses them), Jinja lowers them to `bf.find_eval` / `find_index_eval`