@barefootjs/go-template 0.31.3 → 0.31.5

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/go-template",
3
- "version": "0.31.3",
3
+ "version": "0.31.5",
4
4
  "description": "Go html/template adapter for BarefootJS - generates Go template files from IR",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -49,7 +49,7 @@
49
49
  "directory": "packages/adapter-go-template"
50
50
  },
51
51
  "dependencies": {
52
- "@barefootjs/shared": "0.31.3"
52
+ "@barefootjs/shared": "0.31.5"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@barefootjs/jsx": ">=0.2.0",
@@ -67,9 +67,9 @@
67
67
  },
68
68
  "devDependencies": {
69
69
  "@barefootjs/adapter-tests": "0.1.0",
70
- "@barefootjs/client": "0.31.3",
71
- "@barefootjs/jsx": "0.31.3",
72
- "@barefootjs/vite": "0.31.3",
70
+ "@barefootjs/client": "0.31.5",
71
+ "@barefootjs/jsx": "0.31.5",
72
+ "@barefootjs/vite": "0.31.5",
73
73
  "vite": "^6.0.0"
74
74
  }
75
75
  }
@@ -32,6 +32,7 @@ import type {
32
32
  IRMetadata,
33
33
  TemplatePrimitiveRegistry,
34
34
  LoopBindingPathSegment,
35
+ LoopBindingSource,
35
36
  } from '@barefootjs/jsx'
36
37
  import {
37
38
  BaseAdapter,
@@ -74,6 +75,7 @@ import {
74
75
  resolveStaticLoopSource,
75
76
  collectLoopBoundNames,
76
77
  evaluateStaticLiteral,
78
+ BindingScope,
77
79
  } from '@barefootjs/jsx'
78
80
  import { findInterpolationEnd } from '@barefootjs/jsx/scanner'
79
81
  import { BF_REGION, escapeHtml } from '@barefootjs/shared'
@@ -263,7 +265,37 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
263
265
  * populated it, not correctness across passes.
264
266
  */
265
267
  private bakedStaticChildLoopCache = new Map<string, BakedStaticChildLoop | null>()
266
- private loopParamStack: string[] = []
268
+ /**
269
+ * The one threaded, immutable scope of "names bound by an enclosing loop
270
+ * callback" (#2482 Stage 3 — replaces this adapter's own order-sensitive
271
+ * loop-param stack). Entered via `enterLoopRow(loop)` in `renderLoop` /
272
+ * `renderUnrolledStaticElementLoop`, bracketing preamble conversion,
273
+ * children rendering, AND the key-anchor (`loopItemMarker`) conversion —
274
+ * mirrors the Stage 1a/1b `ctx.scope` precedent in `jsx-to-ir.ts` and the
275
+ * Stage 2 template-string-adapter migration. `IRLoop` already structurally
276
+ * satisfies `LoopBindingSource` (`param`/`index`/`paramBindings`/
277
+ * `preamble`), so most call sites pass the loop node straight through.
278
+ *
279
+ * ONE Go-specific wrinkle `BindingScope`'s generic 'item' semantics don't
280
+ * know about: a `.keys()`-shape loop's callback param is the RANGE INDEX,
281
+ * not the range value — Go's `{{range}}` dot context there is the
282
+ * (discarded) value, not the key, so the key must never win the
283
+ * depth-0-item "resolves to `.`" fast path `isCurrentLoopItem` grants a
284
+ * plain `.map()` param. `renderLoop` enters such a loop's scope with an
285
+ * overridden EMPTY `param`, mirroring the historical empty-string push
286
+ * onto the old stack for the same shape — the key name stays
287
+ * resolvable only through `loopVarRefCount`'s existing `$name` machinery,
288
+ * unchanged by this migration.
289
+ *
290
+ * Destructured-binding ACCESSOR TEXT (`id` → `$__bf_item0.Id`) is Go-
291
+ * specific rendering payload `ScopeBinding` has no field for (by design —
292
+ * `BindingScope` only carries EXISTENCE/kind, not per-adapter accessor
293
+ * strings), so `loopBindingStack` stays as the accessor source of truth,
294
+ * pushed/popped in lockstep with `scope` at the same points. `scope`
295
+ * subsumes its EXISTENCE role (shadow-guard / dot-vs-`$name` decisions)
296
+ * everywhere except the accessor lookup itself.
297
+ */
298
+ private scope: BindingScope = BindingScope.EMPTY
267
299
  /**
268
300
  * Stack of `IRLoop.depth` values (innermost last), pushed/popped around
269
301
  * `renderChildren(loop.children)` in `renderLoop`. `renderAttributes`
@@ -450,6 +482,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
450
482
  this.state.referencedDerivedConsts = new Set()
451
483
  this.state.templateVarCounter = 0
452
484
  this.state.pendingChildrenDefines = []
485
+ this.scope = BindingScope.EMPTY
453
486
  this.primeCompileState(ir)
454
487
  this.state.stringValueNames = collectStringValueNames(ir)
455
488
  // #2448: self-register this component's derived-memo dependencies, so a
@@ -3757,8 +3790,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
3757
3790
  // field that never exists. Mirrors the string-const inlining above.
3758
3791
  const inlinedNum = this.resolveModuleNumericConst(name)
3759
3792
  if (inlinedNum !== null) return inlinedNum
3760
- const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1]
3761
- if (currentLoopParam && name === currentLoopParam) return '.'
3793
+ if (this.isCurrentLoopItem(name)) return '.'
3762
3794
  // An *outer* loop's value variable (we're in a nested loop) is in scope as
3763
3795
  // the Go range variable `$name` declared by that loop's `{{range … := …}}`;
3764
3796
  // the inner dot no longer refers to it, and it's not a root field.
@@ -3858,18 +3890,31 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
3858
3890
  return false
3859
3891
  }
3860
3892
 
3893
+ /**
3894
+ * True when `name` is bound as the CURRENT (innermost) loop row's own
3895
+ * plain `.map()` item param — i.e. it resolves to Go template's dot
3896
+ * context (`.`), not a `$name` range variable or a root field. `depth 0`
3897
+ * is `scope`'s innermost frame; `source === 'item'` excludes a
3898
+ * destructured binding (source `'destructure'`, resolved through
3899
+ * `loopBindingStack` instead — see `scope`'s field docstring) and a
3900
+ * `.keys()`-shape loop's key param (never bound as `'item'` at all —
3901
+ * `renderLoop` enters that shape's scope with an overridden empty param).
3902
+ */
3903
+ private isCurrentLoopItem(name: string): boolean {
3904
+ const hit = this.scope.lookup(name)
3905
+ return hit !== null && hit.depth === 0 && hit.binding.source === 'item'
3906
+ }
3907
+
3861
3908
  /**
3862
3909
  * True when `name` is a loop value variable from an enclosing (not the
3863
- * current) loop — i.e. it sits on `loopParamStack` below the top. Such a
3864
- * reference resolves to the Go range variable `$name`, not the inner dot or
3865
- * the root data.
3910
+ * current) loop — i.e. it's bound in `scope` as an OUTER frame's own
3911
+ * plain item param (`depth > 0`, source `'item'`). Such a reference
3912
+ * resolves to the Go range variable `$name`, not the inner dot or the
3913
+ * root data.
3866
3914
  */
3867
3915
  private isOuterLoopParam(name: string): boolean {
3868
- const top = this.loopParamStack.length - 1
3869
- for (let i = 0; i < top; i++) {
3870
- if (this.loopParamStack[i] === name) return true
3871
- }
3872
- return false
3916
+ const hit = this.scope.lookup(name)
3917
+ return hit !== null && hit.depth > 0 && hit.binding.source === 'item'
3873
3918
  }
3874
3919
 
3875
3920
  /**
@@ -3880,7 +3925,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
3880
3925
  * rebinds. Outside any loop the root *is* the dot, so we emit `.Field`.
3881
3926
  */
3882
3927
  private rootFieldRef(name: string): string {
3883
- const prefix = this.loopParamStack.length > 0 ? '$.' : '.'
3928
+ const prefix = this.inLoop ? '$.' : '.'
3884
3929
  return `${prefix}${capitalizeFieldName(name)}`
3885
3930
  }
3886
3931
 
@@ -3922,9 +3967,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
3922
3967
  * auto-escaping.
3923
3968
  */
3924
3969
  private resolveModuleStringConst(name: string): string | null {
3925
- if (this.loopParamStack.length > 0 && this.loopParamStack[this.loopParamStack.length - 1] === name) {
3926
- return null
3927
- }
3970
+ if (this.isCurrentLoopItem(name)) return null
3928
3971
  if (this.loopVarRefCount.has(name)) return null
3929
3972
  if (this.isOuterLoopParam(name)) return null
3930
3973
  const value = this.state.moduleStringConsts.get(name)
@@ -3940,9 +3983,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
3940
3983
  * range variable that shadows a const name still wins.
3941
3984
  */
3942
3985
  private resolveModuleNumericConst(name: string): string | null {
3943
- if (this.loopParamStack.length > 0 && this.loopParamStack[this.loopParamStack.length - 1] === name) {
3944
- return null
3945
- }
3986
+ if (this.isCurrentLoopItem(name)) return null
3946
3987
  if (this.loopVarRefCount.has(name)) return null
3947
3988
  if (this.isOuterLoopParam(name)) return null
3948
3989
  const c = this.state.localConstants.find(
@@ -4033,7 +4074,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
4033
4074
  const slice = this.state.memoBackedLoopSlice.get(object.callee.name)
4034
4075
  if (slice) {
4035
4076
  // Root field, so reach it through `$.` inside a loop.
4036
- const prefix = this.loopParamStack.length > 0 ? '$.' : '.'
4077
+ const prefix = this.inLoop ? '$.' : '.'
4037
4078
  return `len ${prefix}${slice}`
4038
4079
  }
4039
4080
  }
@@ -4094,8 +4135,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
4094
4135
  // template syntax, let alone a reachable field (PR #2089 review). For
4095
4136
  // identifier keys (snake_case included) the two functions agree, so
4096
4137
  // nothing previously reachable changes shape.
4097
- const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1]
4098
- if (object.kind === 'identifier' && currentLoopParam && object.name === currentLoopParam) {
4138
+ if (object.kind === 'identifier' && this.isCurrentLoopItem(object.name)) {
4099
4139
  return `.${goFieldNameForKey(property)}`
4100
4140
  }
4101
4141
 
@@ -5466,22 +5506,18 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
5466
5506
 
5467
5507
  /**
5468
5508
  * Whether `name` at the CURRENT emission position is bound by an enclosing
5469
- * loop callback — its item param (`loopParamStack` top), an outer loop's
5470
- * range variable, a hoisted loop var, or a destructured binding name
5471
- * (`loopBindingStack`, which is the ONLY place destructured callbacks
5472
- * record their names; they push `''` onto `loopParamStack`). Shared by the
5509
+ * loop callback — its item param, an outer loop's range variable, a
5510
+ * destructured binding name, an index var, or a preamble local. `scope`
5511
+ * (`BindingScope`, #2482 Stage 3) covers all of those EXCEPT a
5512
+ * `.keys()`-shape loop's own key param (never bound in `scope` at all
5513
+ * see `scope`'s field docstring), which stays reachable only through
5514
+ * `loopVarRefCount` — so both are still consulted. Shared by the
5473
5515
  * string-keyed fast paths (#2236, #2242 Copilot review) that resolve raw
5474
5516
  * `jsExpr` text before `identifier()`'s own guards can see it. Mirrors the
5475
5517
  * checks in `resolveModuleStringConst` / `resolveModuleNumericConst`.
5476
5518
  */
5477
5519
  private isLoopShadowedName(name: string): boolean {
5478
- return (
5479
- (this.loopParamStack.length > 0 &&
5480
- this.loopParamStack[this.loopParamStack.length - 1] === name) ||
5481
- this.loopVarRefCount.has(name) ||
5482
- this.isOuterLoopParam(name) ||
5483
- this.loopBindingStack.some(bindings => bindings.has(name))
5484
- )
5520
+ return this.scope.isBound(name) || this.loopVarRefCount.has(name)
5485
5521
  }
5486
5522
 
5487
5523
  /**
@@ -5871,8 +5907,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
5871
5907
  }
5872
5908
  const inlined = this.resolveModuleStringConst(expr.name)
5873
5909
  if (inlined !== null) return plain(inlined)
5874
- const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1]
5875
- if (currentLoopParam && expr.name === currentLoopParam) {
5910
+ if (this.isCurrentLoopItem(expr.name)) {
5876
5911
  return plain('.')
5877
5912
  }
5878
5913
  // Outer loop value variable (nested loop) → its range var `$name`.
@@ -5971,11 +6006,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
5971
6006
  return plain(this.rootFieldRef(expr.property))
5972
6007
  }
5973
6008
 
5974
- {
5975
- const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1]
5976
- if (expr.object.kind === 'identifier' && currentLoopParam && expr.object.name === currentLoopParam) {
5977
- return plain(`.${capitalizeFieldName(expr.property)}`)
5978
- }
6009
+ if (expr.object.kind === 'identifier' && this.isCurrentLoopItem(expr.object.name)) {
6010
+ return plain(`.${capitalizeFieldName(expr.property)}`)
5979
6011
  }
5980
6012
 
5981
6013
  const obj = this.renderConditionExpr(expr.object)
@@ -6241,6 +6273,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
6241
6273
  if (this.bakedStaticChildLoopCache.has(markerId)) {
6242
6274
  return this.bakedStaticChildLoopCache.get(markerId) ?? null
6243
6275
  }
6276
+ // #2482 Stage 3: this method is called from TWO sites outside the live
6277
+ // `renderLoop` tree walk (`generateNewPropsFunction`'s Input-struct
6278
+ // field list and constructor-generation pass, per the docstring above)
6279
+ // where there is no live `this.scope` to consult — so, unlike
6280
+ // `renderLoop`'s own `analyzeBakeableStaticElementLoop` call, this stays
6281
+ // on the coarse whole-component shadow-name Set `primeCompileState`
6282
+ // populates. A genuinely-legitimate surviving use of the pre-#2482
6283
+ // device (flagged for Stage 4 rather than migrated here).
6244
6284
  const result = analyzeBakeableStaticChildLoop(
6245
6285
  { props: childComponent.props, loopArrayParsed: arrayParsed, loopParam: param, loopKey: key },
6246
6286
  this.state.localConstants,
@@ -6337,19 +6377,33 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
6337
6377
  // exact (conservative) acceptance gate. `null` here means the shape
6338
6378
  // isn't (yet) bakeable this way; the existing gates below keep firing
6339
6379
  // exactly as before.
6380
+ // #2482 Stage 3: `renderLoop` is a live tree-walk, so `this.scope` (not
6381
+ // yet entered for THIS loop's own row — that happens further below)
6382
+ // gives the position-accurate ancestor-shadowing answer directly, unlike
6383
+ // `getBakedStaticChildLoop` above, which is memoized across sites that
6384
+ // run OUTSIDE the tree walk and so must keep its coarse whole-component
6385
+ // shadow-name Set (see that method's own comment).
6340
6386
  const bakedElementLoop = loop.childComponent
6341
6387
  ? null
6342
6388
  : analyzeBakeableStaticElementLoop(
6343
6389
  loop,
6344
6390
  this.state.localConstants,
6345
- { isNameShadowed: name => this.state.staticLoopSourceBoundNames.has(name) },
6391
+ { isNameShadowed: this.scope.asShadowPredicate() },
6346
6392
  )
6347
6393
  if (bakedElementLoop) {
6348
6394
  return this.renderUnrolledStaticElementLoop(loop, bakedElementLoop.items)
6349
6395
  }
6350
6396
 
6351
6397
  const arrayName = loop.array.trim()
6352
- if (bakedChildLoop === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
6398
+ // #2482 Stage 4: guard against an ENCLOSING loop's own item param
6399
+ // shadowing a same-named module/component const (`items.map(items =>
6400
+ // items.map(...))`) — without this, a bare-identifier array reference
6401
+ // that resolves to the outer loop's per-row value could misresolve to
6402
+ // the shadowed const below and raise a false BF101. `this.scope` here
6403
+ // is the position-accurate ancestor scope (see the comment above
6404
+ // `bakedElementLoop`) — same shadow-guard shape as
6405
+ // `prop-handling.ts`'s `expandDynamicPropValue`.
6406
+ if (bakedChildLoop === null && /^[A-Za-z_$][\w$]*$/.test(arrayName) && !this.scope.isBound(arrayName)) {
6353
6407
  const arrayConst = this.state.localConstants.find(c => c.name === arrayName)
6354
6408
  if (arrayConst && !arrayConst.isModule && arrayConst.parsed && !this.isStringExpr(arrayConst.parsed, new Set())) {
6355
6409
  this.state.errors.push({
@@ -6423,15 +6477,26 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
6423
6477
  // of the row-scoped field) (#2487).
6424
6478
  const wasInLoopOuter = this.inLoop
6425
6479
  this.inLoop = true
6426
- // Track Go template loop variables. The range *value* variable is the dot
6427
- // context (`.`) and goes on `loopParamStack`; the range *index* variable
6428
- // needs `$name` notation and goes on `loopVarRefCount`. For `.keys()`, the
6429
- // user's param IS the index (the `$k, $_` position), so it needs `$name` —
6430
- // don't push it to loopParamStack (`.` would resolve to the value, not key);
6431
- // push falsy `''` so the `currentLoopParam &&` guard in `identifier()` /
6432
- // `renderConditionExpr` short-circuits. Ref-counting (not a flat Set) keeps
6433
- // nested loops with the same index var name from clobbering the outer entry
6434
- // on cleanup.
6480
+ // Enter this loop's row scope (#2482 Stage 3). `IRLoop` already
6481
+ // structurally satisfies `LoopBindingSource`, so the plain/destructured
6482
+ // cases pass `loop` straight through `enterLoopRow` binds
6483
+ // `paramBindings` (destructure) OR `param` (plain item) automatically.
6484
+ // `.keys()` is the one shape `BindingScope`'s generic 'item' semantics
6485
+ // don't fit: the callback param there IS the index, not the row value
6486
+ // (the dot context is the discarded value), so it must never win
6487
+ // `isCurrentLoopItem`'s dot-shortcut enter with an overridden empty
6488
+ // `param`, mirroring the historical empty-string push onto the old
6489
+ // loop-param stack for the same shape. The key stays reachable only
6490
+ // via `loopVarRefCount`'s `$name` bookkeeping below, unchanged by this
6491
+ // migration.
6492
+ const scopeLoop: LoopBindingSource = loop.iterationShape === 'keys' ? { ...loop, param: '' } : loop
6493
+ const prevScope = this.scope
6494
+ this.scope = prevScope.enterLoopRow(scopeLoop)
6495
+ // Track Go template loop variables. The range *index* variable needs
6496
+ // `$name` notation and goes on `loopVarRefCount` (the range *value*
6497
+ // variable's dot-context resolution now comes from `scope` above).
6498
+ // Ref-counting (not a flat Set) keeps nested loops with the same index
6499
+ // var name from clobbering the outer entry on cleanup.
6435
6500
  const addedLoopVars: string[] = []
6436
6501
  // A `.map()` callback preamble lowers to one `{{$cls := …}}` per-row
6437
6502
  // variable per declaration (#2447). Registering the names on
@@ -6446,23 +6511,19 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
6446
6511
  }
6447
6512
  let pushedBindingMap = false
6448
6513
  if (supportableDestructure) {
6449
- // Bindings resolve against the synthetic `$__bf_item` range var; don't push
6450
- // a loop param (the param is a pattern, not a name).
6514
+ // Bindings resolve against the synthetic `$__bf_item` range var.
6451
6515
  const built = this.buildDestructureBindingMap(loop, rangeValue)
6452
6516
  this.loopBindingStack.push(built.bindings)
6453
6517
  this.loopRestExcludeStack.push(built.restExcludes)
6454
6518
  pushedBindingMap = true
6455
- this.loopParamStack.push('')
6456
6519
  if (rangeIndex !== '_') {
6457
6520
  this.loopVarRefCount.set(rangeIndex, (this.loopVarRefCount.get(rangeIndex) ?? 0) + 1)
6458
6521
  addedLoopVars.push(rangeIndex)
6459
6522
  }
6460
6523
  } else if (loop.iterationShape === 'keys') {
6461
- this.loopParamStack.push('')
6462
6524
  this.loopVarRefCount.set(param, (this.loopVarRefCount.get(param) ?? 0) + 1)
6463
6525
  addedLoopVars.push(param)
6464
6526
  } else {
6465
- this.loopParamStack.push(param)
6466
6527
  if (rangeIndex !== '_') {
6467
6528
  this.loopVarRefCount.set(rangeIndex, (this.loopVarRefCount.get(rangeIndex) ?? 0) + 1)
6468
6529
  addedLoopVars.push(rangeIndex)
@@ -6487,9 +6548,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
6487
6548
  this.loopKeyDepthStack.pop()
6488
6549
  this.loopWrapperStack.pop()
6489
6550
  this.loopScalarItemStack.pop()
6490
- // Build the per-item anchor marker while the loop param is still on the
6491
- // stack, so a `bodyIsItemConditional` key expression resolves against the
6492
- // range item (`.` context) like `data-key` does — popping first would
6551
+ // Build the per-item anchor marker while the row scope is still active,
6552
+ // so a `bodyIsItemConditional` key expression resolves against the
6553
+ // range item (`.` context) like `data-key` does — restoring first would
6493
6554
  // rewrite `t.id` to `.T.ID` instead of `.ID`.
6494
6555
  const itemMarker = this.loopItemMarker(loop)
6495
6556
  for (const v of addedLoopVars) {
@@ -6497,7 +6558,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
6497
6558
  if (rc <= 0) this.loopVarRefCount.delete(v)
6498
6559
  else this.loopVarRefCount.set(v, rc)
6499
6560
  }
6500
- this.loopParamStack.pop()
6561
+ this.scope = prevScope
6501
6562
  if (pushedBindingMap) {
6502
6563
  this.loopBindingStack.pop()
6503
6564
  this.loopRestExcludeStack.pop()
@@ -6578,7 +6639,11 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
6578
6639
  this.loopWrapperStack.push(false)
6579
6640
  this.loopKeyDepthStack.push(loop.depth)
6580
6641
  this.loopScalarItemStack.push(this.scalarLiteralLoopGoType(loop.arrayParsed, loop.itemType) !== null)
6581
- this.loopParamStack.push(loop.param)
6642
+ // The bake gate (`analyzeBakeableStaticElementLoop`) already refused a
6643
+ // destructured or `.keys()`-shape param, so `loop` binds via `enterLoopRow`
6644
+ // unadjusted — always a plain item (#2482 Stage 3).
6645
+ const prevScope = this.scope
6646
+ this.scope = prevScope.enterLoopRow(loop)
6582
6647
 
6583
6648
  let body = ''
6584
6649
  for (const item of items) {
@@ -6603,7 +6668,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
6603
6668
  }
6604
6669
  }
6605
6670
 
6606
- this.loopParamStack.pop()
6671
+ this.scope = prevScope
6607
6672
  this.loopScalarItemStack.pop()
6608
6673
  this.loopKeyDepthStack.pop()
6609
6674
  this.loopWrapperStack.pop()
@@ -6947,8 +7012,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
6947
7012
  // (#2087), which lifted the flat-object-only restriction for the
6948
7013
  // destructure-residual fixtures below.
6949
7014
  const trimmed = value.expr.trim()
6950
- const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1]
6951
- if (currentLoopParam && trimmed === currentLoopParam) {
7015
+ if (this.isCurrentLoopItem(trimmed)) {
6952
7016
  // The row's keys are Go-cased by the field-access contract: the
6953
7017
  // same lowering that makes `attrs.id` emit `{{.ID}}`
6954
7018
  // (`goFieldNameForKey` → `capitalizeFieldName`) requires the
@@ -6,8 +6,12 @@
6
6
  *
7
7
  * NOT included (deliberately): cross-compile child-shape registries
8
8
  * (`childComponentShapes`, `childContextConsumers`, populated before a parent
9
- * compiles), the render-recursion cursor stacks (`loopParamStack`,
10
- * `filterExprDepth`, …), and constant config (`options`, `templatePrimitives`).
9
+ * compiles), the render-recursion cursor state (`scope: BindingScope`,
10
+ * `loopBindingStack`, `filterExprDepth`, …), and constant config (`options`,
11
+ * `templatePrimitives`). #2482 Stage 4: `scope` replaced the old mutable
12
+ * stack this comment used to name here (see the field's own docstring on
13
+ * `GoTemplateAdapter` for the full migration story) — this file never held
14
+ * that stack itself, only described where it lived.
11
15
  */
12
16
 
13
17
  import type {