@barefootjs/go-template 0.18.5 → 0.18.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/analysis/static-child-loop-bake.d.ts +61 -0
- package/dist/adapter/analysis/static-child-loop-bake.d.ts.map +1 -0
- package/dist/adapter/analysis/static-element-loop-bake.d.ts +83 -0
- package/dist/adapter/analysis/static-element-loop-bake.d.ts.map +1 -0
- package/dist/adapter/go-template-adapter.d.ts +116 -2
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +339 -32
- package/dist/adapter/lib/compile-state.d.ts +8 -0
- package/dist/adapter/lib/compile-state.d.ts.map +1 -1
- package/dist/adapter/props/prop-classes.d.ts +28 -9
- package/dist/adapter/props/prop-classes.d.ts.map +1 -1
- package/dist/build.js +339 -32
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +341 -41
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/test-render.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/go-template-adapter.test.ts +707 -3
- package/src/adapter/analysis/static-child-loop-bake.ts +119 -0
- package/src/adapter/analysis/static-element-loop-bake.ts +211 -0
- package/src/adapter/go-template-adapter.ts +435 -25
- package/src/adapter/lib/compile-state.ts +9 -0
- package/src/adapter/props/prop-classes.ts +34 -9
- package/src/conformance-pins.ts +30 -31
- package/src/render-divergences.ts +12 -0
- package/src/test-render.ts +109 -6
|
@@ -67,6 +67,13 @@ import {
|
|
|
67
67
|
envSignalReaderFor,
|
|
68
68
|
computeSsrSeedPlan,
|
|
69
69
|
isStringConcatBinary,
|
|
70
|
+
isDangerousInnerHtmlAttr,
|
|
71
|
+
resolveDangerousInnerHtml,
|
|
72
|
+
dangerousInnerHtmlMetacharViolation,
|
|
73
|
+
dangerousInnerHtmlDiagnostic,
|
|
74
|
+
resolveStaticLoopSource,
|
|
75
|
+
collectLoopBoundNames,
|
|
76
|
+
evaluateStaticLiteral,
|
|
70
77
|
} from '@barefootjs/jsx'
|
|
71
78
|
import { findInterpolationEnd } from '@barefootjs/jsx/scanner'
|
|
72
79
|
import { BF_REGION, escapeHtml } from '@barefootjs/shared'
|
|
@@ -111,6 +118,8 @@ import { collectRootScopeNodes } from "./lib/ir-scope.ts"
|
|
|
111
118
|
import { GO_TEMPLATE_PRIMITIVES } from "./lib/constants.ts"
|
|
112
119
|
import { CompileState } from "./lib/compile-state.ts"
|
|
113
120
|
import { hasClientInteractivity, findNestedComponents } from "./analysis/component-tree.ts"
|
|
121
|
+
import { analyzeBakeableStaticChildLoop, scalarToGoLiteral, type BakedStaticChildLoop } from "./analysis/static-child-loop-bake.ts"
|
|
122
|
+
import { analyzeBakeableStaticElementLoop } from "./analysis/static-element-loop-bake.ts"
|
|
114
123
|
import type { GoEmitContext } from "./emit-context.ts"
|
|
115
124
|
import { inlineLocalHelperCall } from "./expr/helper-inline.ts"
|
|
116
125
|
import { lowerRegisteredCall } from "./expr/url-builder.ts"
|
|
@@ -226,6 +235,22 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
226
235
|
}
|
|
227
236
|
|
|
228
237
|
private inLoop: boolean = false
|
|
238
|
+
/**
|
|
239
|
+
* Memoized `analyzeBakeableStaticChildLoop` result per loop marker id
|
|
240
|
+
* (#2208). Consulted from three sites that all need the SAME verdict for
|
|
241
|
+
* the SAME loop — the `renderLoop` gate, the Input struct's field list,
|
|
242
|
+
* and the constructor's per-item construction. `renderLoop`'s gate runs
|
|
243
|
+
* during `generate()`'s render pass; the other two run during
|
|
244
|
+
* `generateTypes()`'s constructor-generation pass, which `generate()`
|
|
245
|
+
* also invokes internally partway through — so this cache is reset (in
|
|
246
|
+
* `primeCompileState`, not here) between those two passes too. Agreement
|
|
247
|
+
* across all three sites is therefore guaranteed by `analyzeBakeable-
|
|
248
|
+
* StaticChildLoop` being a deterministic pure function of the (re-primed)
|
|
249
|
+
* per-compile state, not by one shared memo spanning every read; the
|
|
250
|
+
* cache's value is avoiding redundant recomputation WITHIN the pass that
|
|
251
|
+
* populated it, not correctness across passes.
|
|
252
|
+
*/
|
|
253
|
+
private bakedStaticChildLoopCache = new Map<string, BakedStaticChildLoop | null>()
|
|
229
254
|
private loopParamStack: string[] = []
|
|
230
255
|
/**
|
|
231
256
|
* Stack of `IRLoop.depth` values (innermost last), pushed/popped around
|
|
@@ -272,6 +297,30 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
272
297
|
* alone (#2087 Phase B).
|
|
273
298
|
*/
|
|
274
299
|
private loopRestExcludeStack: Array<Map<string, { parent: string; excludeKeys: string[] }>> = []
|
|
300
|
+
/**
|
|
301
|
+
* Active per-item static bindings for a #2224 unrolled plain-element loop
|
|
302
|
+
* (`renderUnrolledStaticElementLoop`) — innermost last, so a nested static
|
|
303
|
+
* unroll inside another one resolves against its own item, not an outer
|
|
304
|
+
* one's. When non-empty, `convertExpressionToGo`'s top-of-function check
|
|
305
|
+
* resolves EVERY expression against the innermost entry's item via
|
|
306
|
+
* `evaluateStaticLiteral` and returns a literal Go value instead of
|
|
307
|
+
* descending into the normal `.Field`-style dot-context lowering — there
|
|
308
|
+
* is no real `{{range}}` establishing that context during an unroll, so
|
|
309
|
+
* `identifier()`'s `currentLoopParam → '.'` branch would otherwise resolve
|
|
310
|
+
* against the WRONG (enclosing) dot context. `analyzeBakeableStaticElementLoop`
|
|
311
|
+
* has already verified every expression in the body resolves this way for
|
|
312
|
+
* every item, so the fallback failure branch below is a defensive
|
|
313
|
+
* invariant check, not a real code path.
|
|
314
|
+
*/
|
|
315
|
+
private staticLoopItemStack: Array<{ param: string; item: unknown }> = []
|
|
316
|
+
/**
|
|
317
|
+
* Set by the `convertExpressionToGo` override above when a
|
|
318
|
+
* `staticLoopItemStack` entry is active but the current expression fails
|
|
319
|
+
* to resolve against it — should never happen (see that check's comment),
|
|
320
|
+
* but `renderUnrolledStaticElementLoop` asserts this stays `false` after
|
|
321
|
+
* every item render rather than silently shipping a `""` sentinel.
|
|
322
|
+
*/
|
|
323
|
+
private staticLoopBakeFailed = false
|
|
275
324
|
|
|
276
325
|
/**
|
|
277
326
|
* Cross-component child shapes, keyed by child component name. Populated via
|
|
@@ -307,6 +356,32 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
307
356
|
this.state.restPropsName = ir.metadata.restPropsName ?? null
|
|
308
357
|
this.state.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants)
|
|
309
358
|
this.state.localConstants = ir.metadata.localConstants ?? []
|
|
359
|
+
// #2208 fable review: every name a `.map()`/`.filter()` loop callback
|
|
360
|
+
// binds as its item/index parameter anywhere in the component. Static
|
|
361
|
+
// loop-source resolution (`getBakedStaticChildLoop` /
|
|
362
|
+
// `analyzeBakeableStaticChildLoop`) must never resolve a const whose
|
|
363
|
+
// name a DIFFERENT, enclosing loop's own callback param shadows.
|
|
364
|
+
// Computed here (not from live render-time stack state) because this
|
|
365
|
+
// must agree across THREE call sites, two of which (`generateTypes`'s
|
|
366
|
+
// Input-struct + constructor generation) run OUTSIDE the live
|
|
367
|
+
// `renderLoop` tree-walk that would otherwise track shadowing via
|
|
368
|
+
// stack push/pop — same coarse-but-safe mitigation as #2212.
|
|
369
|
+
this.state.staticLoopSourceBoundNames = collectLoopBoundNames(ir)
|
|
370
|
+
// #2208 fable re-review: the adapter instance is a reused singleton —
|
|
371
|
+
// `generate()` calls this once per component, but `generateTypes()` is
|
|
372
|
+
// ALSO a standalone public entry point (the Go conformance harness in
|
|
373
|
+
// `test-render.ts` calls it directly on an already-`generate()`d
|
|
374
|
+
// adapter for a sibling/child IR). Resetting the bake cache HERE, not
|
|
375
|
+
// just in `generate()`, closes that door too: a stale entry keyed by a
|
|
376
|
+
// marker id that collides with a PREVIOUS component (marker ids
|
|
377
|
+
// restart at `l0` per component) would otherwise either silently
|
|
378
|
+
// suppress this fix or leak that other component's baked data into
|
|
379
|
+
// this one's constructor. `generate()` itself calls `generateTypes()`
|
|
380
|
+
// partway through — re-priming (and so re-clearing the cache) there is
|
|
381
|
+
// harmless: `analyzeBakeableStaticChildLoop` is deterministic over
|
|
382
|
+
// identically-primed state, so a cache miss on the second pass just
|
|
383
|
+
// recomputes the same answer.
|
|
384
|
+
this.bakedStaticChildLoopCache = new Map()
|
|
310
385
|
this.state.localHelperNames = new Set(
|
|
311
386
|
this.state.localConstants.filter(c => !c.isModule && c.containsArrow).map(c => c.name),
|
|
312
387
|
)
|
|
@@ -856,6 +931,18 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
856
931
|
}
|
|
857
932
|
|
|
858
933
|
for (const nested of inputNested) {
|
|
934
|
+
// #2208: a static loop whose array source is itself fully-static
|
|
935
|
+
// (baked directly in the constructor — see `generateNewPropsFunction`)
|
|
936
|
+
// has no caller-supplied data to accept; the Input struct carries no
|
|
937
|
+
// field for it at all (there was never a working `in.<Name>s` path
|
|
938
|
+
// for this shape before this fix — it refused with BF101).
|
|
939
|
+
if (nested.loopMarkerId && this.getBakedStaticChildLoop(
|
|
940
|
+
nested.loopMarkerId,
|
|
941
|
+
nested,
|
|
942
|
+
nested.loopArrayParsed,
|
|
943
|
+
nested.loopParam,
|
|
944
|
+
nested.loopKey,
|
|
945
|
+
)) continue
|
|
859
946
|
lines.push(`\t${nested.name}s []${nested.name}Input`)
|
|
860
947
|
}
|
|
861
948
|
|
|
@@ -1074,6 +1161,36 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1074
1161
|
// Static nested WITHOUT body children.
|
|
1075
1162
|
for (const nested of staticWithoutBody) {
|
|
1076
1163
|
const varName = `${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`
|
|
1164
|
+
// #2208: a static loop whose ARRAY SOURCE is itself fully-static
|
|
1165
|
+
// (`const items = [{ label: 'Alpha' }, ...]`) has no caller input to
|
|
1166
|
+
// wait for — every item's props/data-key are already known at
|
|
1167
|
+
// compile time. Bake them directly instead of ranging over
|
|
1168
|
+
// `in.<Name>s` (which stays empty forever for this shape, since the
|
|
1169
|
+
// loop-source gate above only lets this fixture through BECAUSE it's
|
|
1170
|
+
// baked here).
|
|
1171
|
+
const baked = nested.loopMarkerId
|
|
1172
|
+
? this.getBakedStaticChildLoop(
|
|
1173
|
+
nested.loopMarkerId,
|
|
1174
|
+
nested,
|
|
1175
|
+
nested.loopArrayParsed,
|
|
1176
|
+
nested.loopParam,
|
|
1177
|
+
nested.loopKey,
|
|
1178
|
+
)
|
|
1179
|
+
: null
|
|
1180
|
+
if (baked) {
|
|
1181
|
+
lines.push(`\t${varName} := make([]${nested.name}Props, ${baked.items.length})`)
|
|
1182
|
+
baked.items.forEach((item, i) => {
|
|
1183
|
+
const fields = item.inputFields.map(f => `${f.goField}: ${f.goValue}`).join(', ')
|
|
1184
|
+
lines.push(`\t${varName}[${i}] = New${nested.name}Props(${nested.name}Input{${fields}})`)
|
|
1185
|
+
lines.push(`\t${varName}[${i}].BfParent = scopeID`)
|
|
1186
|
+
lines.push(`\t${varName}[${i}].BfMount = "${nested.slotId}"`)
|
|
1187
|
+
if (item.dataKey !== null) {
|
|
1188
|
+
lines.push(`\t${varName}[${i}].BfDataKey = ${JSON.stringify(item.dataKey)}`)
|
|
1189
|
+
}
|
|
1190
|
+
})
|
|
1191
|
+
lines.push('')
|
|
1192
|
+
continue
|
|
1193
|
+
}
|
|
1077
1194
|
lines.push(`\t${varName} := make([]${nested.name}Props, len(in.${nested.name}s))`)
|
|
1078
1195
|
lines.push(`\tfor i, item := range in.${nested.name}s {`)
|
|
1079
1196
|
lines.push(`\t\t${varName}[i] = New${nested.name}Props(item)`)
|
|
@@ -2757,7 +2874,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2757
2874
|
renderElement(element: IRElement): string {
|
|
2758
2875
|
const tag = element.tag
|
|
2759
2876
|
const attrs = this.renderAttributes(element)
|
|
2760
|
-
const
|
|
2877
|
+
const dangerousHtml = this.renderDangerousInnerHtml(element)
|
|
2878
|
+
const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children)
|
|
2761
2879
|
|
|
2762
2880
|
let hydrationAttrs = ''
|
|
2763
2881
|
if (element.needsScope) {
|
|
@@ -2792,6 +2910,28 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2792
2910
|
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`
|
|
2793
2911
|
}
|
|
2794
2912
|
|
|
2913
|
+
/**
|
|
2914
|
+
* `dangerouslySetInnerHTML={{ __html: '...' }}` (#2207) — see the Blade
|
|
2915
|
+
* adapter's identical helper for the full rationale. `null` means the
|
|
2916
|
+
* attribute is absent (caller falls through to normal `renderChildren`);
|
|
2917
|
+
* a non-`null` string (possibly `''`) replaces the children outright.
|
|
2918
|
+
*/
|
|
2919
|
+
private renderDangerousInnerHtml(element: IRElement): string | null {
|
|
2920
|
+
const resolution = resolveDangerousInnerHtml(element)
|
|
2921
|
+
if (!resolution) return null
|
|
2922
|
+
if (resolution.kind === 'dynamic') {
|
|
2923
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc))
|
|
2924
|
+
return ''
|
|
2925
|
+
}
|
|
2926
|
+
const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name)
|
|
2927
|
+
if (violation) {
|
|
2928
|
+
const attr = element.attrs.find(isDangerousInnerHtmlAttr)!
|
|
2929
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation))
|
|
2930
|
+
return ''
|
|
2931
|
+
}
|
|
2932
|
+
return resolution.html
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2795
2935
|
renderExpression(expr: IRExpression): string {
|
|
2796
2936
|
// @client directive: render a comment marker; ClientJS evaluates the
|
|
2797
2937
|
// expression via updateClientMarker().
|
|
@@ -4101,10 +4241,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4101
4241
|
|
|
4102
4242
|
/**
|
|
4103
4243
|
* Render a predicate for use in Go template `{{if}}` conditions, substituting
|
|
4104
|
-
* the loop parameter (e.g. `t` in `t.done`) with dot notation.
|
|
4244
|
+
* the loop parameter (e.g. `t` in `t.done`) with dot notation. `datumField`
|
|
4245
|
+
* (#2228) is the wrapper struct's datum-carrying field name (e.g. `"Todo"`)
|
|
4246
|
+
* for a loop whose body is a child component — see `wrapperDatumField` — so
|
|
4247
|
+
* `t.done` qualifies through it (`.Todo.Done`) instead of the bare `.Done`
|
|
4248
|
+
* `html/template` can't resolve on the wrapper Props struct. `undefined` for
|
|
4249
|
+
* a non-wrapper (plain-element-body) loop, where `.` already IS the datum.
|
|
4105
4250
|
*/
|
|
4106
|
-
private renderPredicateCondition(pred: ParsedExpr, param: string): string {
|
|
4107
|
-
return this.renderFilterExpr(pred, param)
|
|
4251
|
+
private renderPredicateCondition(pred: ParsedExpr, param: string, datumField?: string | null): string {
|
|
4252
|
+
return this.renderFilterExpr(pred, param, new Map(), datumField ?? undefined)
|
|
4108
4253
|
}
|
|
4109
4254
|
|
|
4110
4255
|
/** Whether an expression needs parentheses when used in and/or. */
|
|
@@ -4138,12 +4283,19 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4138
4283
|
* Render a filter predicate expression (`t => !t.done`, or a block body
|
|
4139
4284
|
* normalized to one — #2040). `localVarMap` is a vestigial empty default kept
|
|
4140
4285
|
* on the recursion; block-body locals are now inlined upstream, so no caller
|
|
4141
|
-
* populates it.
|
|
4286
|
+
* populates it. `datumField` (#2228) qualifies a bare `param` reference (and
|
|
4287
|
+
* `param.xxx` member/call access) through the wrapper struct's
|
|
4288
|
+
* datum-carrying field — see `wrapperDatumField` — for a loop whose `.` is a
|
|
4289
|
+
* child-component wrapper Props struct rather than the raw datum itself.
|
|
4290
|
+
* `undefined` for every other caller (non-loop `.filter()`/`.find()`/etc.,
|
|
4291
|
+
* or a plain-element-body loop), which keeps emitting the bare `.`/`.Field`
|
|
4292
|
+
* this method always has.
|
|
4142
4293
|
*/
|
|
4143
4294
|
private renderFilterExpr(
|
|
4144
4295
|
expr: ParsedExpr,
|
|
4145
4296
|
param: string,
|
|
4146
|
-
localVarMap: Map<string, string> = new Map()
|
|
4297
|
+
localVarMap: Map<string, string> = new Map(),
|
|
4298
|
+
datumField?: string
|
|
4147
4299
|
): string {
|
|
4148
4300
|
// Top-of-recursion: clear the unsupported sentinel so a previous filter
|
|
4149
4301
|
// expression's failure doesn't poison this one. Parents (`member` /
|
|
@@ -4153,7 +4305,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4153
4305
|
if (this.filterExprDepth === 0) this.filterExprUnsupported = false
|
|
4154
4306
|
this.filterExprDepth++
|
|
4155
4307
|
try {
|
|
4156
|
-
return this.renderFilterExprNode(expr, param, localVarMap)
|
|
4308
|
+
return this.renderFilterExprNode(expr, param, localVarMap, datumField)
|
|
4157
4309
|
} finally {
|
|
4158
4310
|
this.filterExprDepth--
|
|
4159
4311
|
}
|
|
@@ -4162,12 +4314,22 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4162
4314
|
private renderFilterExprNode(
|
|
4163
4315
|
expr: ParsedExpr,
|
|
4164
4316
|
param: string,
|
|
4165
|
-
localVarMap: Map<string, string
|
|
4317
|
+
localVarMap: Map<string, string>,
|
|
4318
|
+
datumField?: string
|
|
4166
4319
|
): string {
|
|
4320
|
+
// #2228: `paramPrefix` prepends the wrapper's datum-carrying field to a
|
|
4321
|
+
// loop-param access (`.Todo` under a wrapper, `''` otherwise). Two derived
|
|
4322
|
+
// forms because Go template spells "the dot itself" as `.` but "field on
|
|
4323
|
+
// the dot" as `.Field` — a naive shared `'.'` prefix would emit `..Done`
|
|
4324
|
+
// for the non-wrapper member case:
|
|
4325
|
+
// bare `t` → `paramDot` (`.Todo` / `.`)
|
|
4326
|
+
// `t.done` → `${paramPrefix}.Done` (`.Todo.Done` / `.Done`)
|
|
4327
|
+
const paramPrefix = datumField ? `.${datumField}` : ''
|
|
4328
|
+
const paramDot = paramPrefix || '.'
|
|
4167
4329
|
switch (expr.kind) {
|
|
4168
4330
|
case 'identifier': {
|
|
4169
4331
|
if (expr.name === param) {
|
|
4170
|
-
return
|
|
4332
|
+
return paramDot
|
|
4171
4333
|
}
|
|
4172
4334
|
// A local variable mapped to a signal.
|
|
4173
4335
|
const signal = localVarMap.get(expr.name)
|
|
@@ -4187,9 +4349,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4187
4349
|
return String(expr.value)
|
|
4188
4350
|
|
|
4189
4351
|
case 'member': {
|
|
4190
|
-
// t.done -> .Done
|
|
4352
|
+
// t.done -> .Done (or .Todo.Done under a wrapper struct, #2228)
|
|
4191
4353
|
if (expr.object.kind === 'identifier' && expr.object.name === param) {
|
|
4192
|
-
return
|
|
4354
|
+
return `${paramPrefix}.${capitalizeFieldName(expr.property)}`
|
|
4193
4355
|
}
|
|
4194
4356
|
// `.length` on a higher-order filter result (e.g.
|
|
4195
4357
|
// `x.tags.filter(t => t.active).length > 0`). Reuse
|
|
@@ -4202,21 +4364,21 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4202
4364
|
const innerHO = this.higherOrderShapeOf(expr.object)
|
|
4203
4365
|
if (innerHO && innerHO.method === 'filter') {
|
|
4204
4366
|
const lenExpr = this.renderFilterLengthExpr(innerHO, e =>
|
|
4205
|
-
this.renderFilterExpr(e, param, localVarMap),
|
|
4367
|
+
this.renderFilterExpr(e, param, localVarMap, datumField),
|
|
4206
4368
|
)
|
|
4207
4369
|
if (lenExpr) return `(${lenExpr})`
|
|
4208
4370
|
}
|
|
4209
4371
|
}
|
|
4210
4372
|
// Nested member access or local var.prop.
|
|
4211
|
-
const obj = this.renderFilterExpr(expr.object, param, localVarMap)
|
|
4373
|
+
const obj = this.renderFilterExpr(expr.object, param, localVarMap, datumField)
|
|
4212
4374
|
if (this.filterExprUnsupported) return 'false'
|
|
4213
4375
|
return `${obj}.${capitalizeFieldName(expr.property)}`
|
|
4214
4376
|
}
|
|
4215
4377
|
|
|
4216
4378
|
case 'call': {
|
|
4217
|
-
// `t.isDone()` -> `.IsDone`
|
|
4379
|
+
// `t.isDone()` -> `.IsDone` (or `.Todo.IsDone` under a wrapper, #2228)
|
|
4218
4380
|
if (expr.callee.kind === 'member' && expr.callee.object.kind === 'identifier' && expr.callee.object.name === param) {
|
|
4219
|
-
return
|
|
4381
|
+
return `${paramPrefix}.${capitalizeFieldName(expr.callee.property)}`
|
|
4220
4382
|
}
|
|
4221
4383
|
// Signal calls: `filter()` -> `$.Filter`
|
|
4222
4384
|
if (expr.callee.kind === 'identifier' && expr.args.length === 0) {
|
|
@@ -4232,13 +4394,13 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4232
4394
|
if (asCallbackMethodCall(expr) !== null) {
|
|
4233
4395
|
return this.refuseFilterExprNode(expr)
|
|
4234
4396
|
}
|
|
4235
|
-
const result = this.renderFilterExpr(expr.callee, param, localVarMap)
|
|
4397
|
+
const result = this.renderFilterExpr(expr.callee, param, localVarMap, datumField)
|
|
4236
4398
|
if (this.filterExprUnsupported) return 'false'
|
|
4237
4399
|
return result
|
|
4238
4400
|
}
|
|
4239
4401
|
|
|
4240
4402
|
case 'unary': {
|
|
4241
|
-
const arg = this.renderFilterExpr(expr.argument, param, localVarMap)
|
|
4403
|
+
const arg = this.renderFilterExpr(expr.argument, param, localVarMap, datumField)
|
|
4242
4404
|
if (this.filterExprUnsupported) return 'false'
|
|
4243
4405
|
if (expr.op === '!') {
|
|
4244
4406
|
// Wrap in parens if arg is a function call (eq, ne, gt, …).
|
|
@@ -4252,9 +4414,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4252
4414
|
}
|
|
4253
4415
|
|
|
4254
4416
|
case 'binary': {
|
|
4255
|
-
const left = this.renderFilterExpr(expr.left, param, localVarMap)
|
|
4417
|
+
const left = this.renderFilterExpr(expr.left, param, localVarMap, datumField)
|
|
4256
4418
|
if (this.filterExprUnsupported) return 'false'
|
|
4257
|
-
const right = this.renderFilterExpr(expr.right, param, localVarMap)
|
|
4419
|
+
const right = this.renderFilterExpr(expr.right, param, localVarMap, datumField)
|
|
4258
4420
|
if (this.filterExprUnsupported) return 'false'
|
|
4259
4421
|
|
|
4260
4422
|
switch (expr.op) {
|
|
@@ -4286,9 +4448,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4286
4448
|
}
|
|
4287
4449
|
|
|
4288
4450
|
case 'logical': {
|
|
4289
|
-
const left = this.renderFilterExpr(expr.left, param, localVarMap)
|
|
4451
|
+
const left = this.renderFilterExpr(expr.left, param, localVarMap, datumField)
|
|
4290
4452
|
if (this.filterExprUnsupported) return 'false'
|
|
4291
|
-
const right = this.renderFilterExpr(expr.right, param, localVarMap)
|
|
4453
|
+
const right = this.renderFilterExpr(expr.right, param, localVarMap, datumField)
|
|
4292
4454
|
if (this.filterExprUnsupported) return 'false'
|
|
4293
4455
|
if (expr.op === '&&') {
|
|
4294
4456
|
return `and (${left}) (${right})`
|
|
@@ -4409,6 +4571,36 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4409
4571
|
return '""'
|
|
4410
4572
|
}
|
|
4411
4573
|
|
|
4574
|
+
// #2224: inside a `renderUnrolledStaticElementLoop` pass, there is no
|
|
4575
|
+
// real `{{range}}` establishing a per-item dot context — every
|
|
4576
|
+
// expression in the body must instead resolve directly against the
|
|
4577
|
+
// active item via `evaluateStaticLiteral` and lower to a literal Go
|
|
4578
|
+
// value. Highest priority (even over the static-record-index / inlined-
|
|
4579
|
+
// const early returns below): those string-keyed checks were never
|
|
4580
|
+
// designed to reason about a loop item and could otherwise misfire on
|
|
4581
|
+
// text that merely happens to match their shape. `analyzeBakeable-
|
|
4582
|
+
// StaticElementLoop` has already verified every expression in this body
|
|
4583
|
+
// resolves for every item, so the `staticLoopBakeFailed` branch is a
|
|
4584
|
+
// defensive invariant, not a real code path — if it ever fires, the two
|
|
4585
|
+
// passes disagreed and the safest move is a sentinel, not a `.Field`
|
|
4586
|
+
// reference with no range context behind it.
|
|
4587
|
+
if (this.staticLoopItemStack.length > 0) {
|
|
4588
|
+
const top = this.staticLoopItemStack[this.staticLoopItemStack.length - 1]
|
|
4589
|
+
const parsedForBake = preParsed ?? parseExpression(trimmed)
|
|
4590
|
+
const resolved = evaluateStaticLiteral(parsedForBake, new Map([[top.param, top.item]]))
|
|
4591
|
+
const literal = resolved !== null ? scalarToGoLiteral(resolved.value) : null
|
|
4592
|
+
if (literal !== null) {
|
|
4593
|
+
// Deliberately leave `out.parsed` unset — a `template-literal`-kind
|
|
4594
|
+
// source (`` `Hi ${item.label}` ``) must NOT be treated as
|
|
4595
|
+
// already-fragment text by `renderExpression`'s `isTemplateFragment`
|
|
4596
|
+
// check (it would skip the `{{...}}` wrap and print the Go literal's
|
|
4597
|
+
// quote characters raw into the HTML).
|
|
4598
|
+
return literal
|
|
4599
|
+
}
|
|
4600
|
+
this.staticLoopBakeFailed = true
|
|
4601
|
+
return '""'
|
|
4602
|
+
}
|
|
4603
|
+
|
|
4412
4604
|
// `IDENT['key']` over a module object-literal const with a STRING-LITERAL key
|
|
4413
4605
|
// is a fully static lookup — resolve it at compile time. The generic member
|
|
4414
4606
|
// lowering below would otherwise capitalize the bracket access into a field
|
|
@@ -4424,7 +4616,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4424
4616
|
// the generic lowering would reference a nonexistent `.TotalPages` field).
|
|
4425
4617
|
// Only pure numeric / single-quoted-string initializers qualify; anything
|
|
4426
4618
|
// else may be runtime-dependent.
|
|
4427
|
-
|
|
4619
|
+
//
|
|
4620
|
+
// #2236: this is a string-keyed fast path over `jsExpr` reached directly
|
|
4621
|
+
// by call sites like attribute emission (`key={count}` → `data-key`) that
|
|
4622
|
+
// never go through `identifier()`'s loop-shadow guards below — so it must
|
|
4623
|
+
// carry its OWN guard. When `.map((count) => ...)` shadows the outer
|
|
4624
|
+
// `const count = 7`, the occurrence inside the loop body must resolve to
|
|
4625
|
+
// the range value (via the normal parse-and-lower fallthrough), not the
|
|
4626
|
+
// outer literal.
|
|
4627
|
+
if (!this.isLoopShadowedName(trimmed) && /^[A-Za-z_$][\w$]*$/.test(trimmed)) {
|
|
4428
4628
|
const litConst = (this.state.localConstants ?? []).find(c => c.name === trimmed)
|
|
4429
4629
|
if (litConst?.value !== undefined) {
|
|
4430
4630
|
const v = litConst.value.trim()
|
|
@@ -4495,6 +4695,26 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4495
4695
|
return this.renderParsedExpr(parsed)
|
|
4496
4696
|
}
|
|
4497
4697
|
|
|
4698
|
+
/**
|
|
4699
|
+
* Whether `name` at the CURRENT emission position is bound by an enclosing
|
|
4700
|
+
* loop callback — its item param (`loopParamStack` top), an outer loop's
|
|
4701
|
+
* range variable, a hoisted loop var, or a destructured binding name
|
|
4702
|
+
* (`loopBindingStack`, which is the ONLY place destructured callbacks
|
|
4703
|
+
* record their names; they push `''` onto `loopParamStack`). Shared by the
|
|
4704
|
+
* string-keyed fast paths (#2236, #2242 Copilot review) that resolve raw
|
|
4705
|
+
* `jsExpr` text before `identifier()`'s own guards can see it. Mirrors the
|
|
4706
|
+
* checks in `resolveModuleStringConst` / `resolveModuleNumericConst`.
|
|
4707
|
+
*/
|
|
4708
|
+
private isLoopShadowedName(name: string): boolean {
|
|
4709
|
+
return (
|
|
4710
|
+
(this.loopParamStack.length > 0 &&
|
|
4711
|
+
this.loopParamStack[this.loopParamStack.length - 1] === name) ||
|
|
4712
|
+
this.loopVarRefCount.has(name) ||
|
|
4713
|
+
this.isOuterLoopParam(name) ||
|
|
4714
|
+
this.loopBindingStack.some(bindings => bindings.has(name))
|
|
4715
|
+
)
|
|
4716
|
+
}
|
|
4717
|
+
|
|
4498
4718
|
/**
|
|
4499
4719
|
* Resolve `IDENT['key']` / `IDENT["key"]` where `IDENT` is a module-scope
|
|
4500
4720
|
* object-literal const and the key is a string literal — a compile-time-static
|
|
@@ -4512,6 +4732,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4512
4732
|
// ordinary props/locals never match.
|
|
4513
4733
|
/^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)$/.exec(jsExpr)
|
|
4514
4734
|
if (!m) return null
|
|
4735
|
+
// The base name may be an enclosing loop callback's own (shadowing)
|
|
4736
|
+
// param (`rows.map((cfg) => cfg.x)` under a module `const cfg = {...}`)
|
|
4737
|
+
// — the record-member sibling of the #2236 bare-identifier gap, found
|
|
4738
|
+
// by the loop-param-shadows-record-const fixture. Fall through to the
|
|
4739
|
+
// generic lowering, which resolves the member through the loop binding.
|
|
4740
|
+
if (this.isLoopShadowedName(m[1])) return null
|
|
4515
4741
|
const key = m[2] ?? m[3]
|
|
4516
4742
|
const constInfo = (this.state.localConstants ?? []).find(
|
|
4517
4743
|
c => c.name === m[1] && c.isModule,
|
|
@@ -4974,6 +5200,68 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4974
5200
|
return undefined
|
|
4975
5201
|
}
|
|
4976
5202
|
|
|
5203
|
+
/**
|
|
5204
|
+
* #2228: the Go field name on the wrapper Props struct that carries the
|
|
5205
|
+
* loop datum, for a loop whose body IS a child component (`loop.childComponent`,
|
|
5206
|
+
* e.g. `.TodoItems` ranging over `TodoItemProps`). `{{range}}`'s dot context
|
|
5207
|
+
* for such a loop is the WHOLE wrapper struct (`TodoItemProps{ Todo Todo,
|
|
5208
|
+
* OnToggle ..., ... }`), not the raw per-item datum — so a filter predicate
|
|
5209
|
+
* (`t => !t.done`) referencing the loop param can't lower `t.done` straight
|
|
5210
|
+
* to `.Done` (no such top-level field; `html/template` fails at execute time
|
|
5211
|
+
* with `can't evaluate field Done in type TodoItemProps`). The datum lives
|
|
5212
|
+
* nested under whichever child prop was PASSED the loop param verbatim
|
|
5213
|
+
* (`todo={todo}` → field `Todo`, from `capitalizeFieldName('todo')` — the
|
|
5214
|
+
* SAME derivation `generateInputStruct`/`generatePropsStruct` use for every
|
|
5215
|
+
* other prop-to-field mapping, so this never invents a field name the
|
|
5216
|
+
* generated struct doesn't actually have). Returns `null` for a non-wrapper
|
|
5217
|
+
* loop, or when no prop's value is a bare reference to the loop param (the
|
|
5218
|
+
* datum isn't forwarded at all — nothing to qualify through).
|
|
5219
|
+
*/
|
|
5220
|
+
private wrapperDatumField(loop: {
|
|
5221
|
+
childComponent?: IRLoopChildComponent
|
|
5222
|
+
param: string
|
|
5223
|
+
}): string | null {
|
|
5224
|
+
if (!loop.childComponent) return null
|
|
5225
|
+
for (const prop of loop.childComponent.props) {
|
|
5226
|
+
if (prop.isEventHandler) continue
|
|
5227
|
+
if (prop.value.kind !== 'expression') continue
|
|
5228
|
+
const parsed = prop.value.parsed
|
|
5229
|
+
const isBareParamRef = parsed
|
|
5230
|
+
? parsed.kind === 'identifier' && parsed.name === loop.param
|
|
5231
|
+
: prop.value.expr.trim() === loop.param
|
|
5232
|
+
if (isBareParamRef) return capitalizeFieldName(prop.name)
|
|
5233
|
+
}
|
|
5234
|
+
return null
|
|
5235
|
+
}
|
|
5236
|
+
|
|
5237
|
+
/**
|
|
5238
|
+
* Memoized bakeability check for a static-array loop whose body is a
|
|
5239
|
+
* single child component (#2208) — see `analyzeBakeableStaticChildLoop`'s
|
|
5240
|
+
* docstring. Accepts either an `IRLoop` (the `renderLoop` gate) or a
|
|
5241
|
+
* `NestedComponentInfo` (`generateNewPropsFunction`/Input-struct sites) —
|
|
5242
|
+
* both carry the same `loopArrayParsed`/`loopParam`/`loopKey`/props data,
|
|
5243
|
+
* just under different field names, so this normalizes to one shape and
|
|
5244
|
+
* caches by marker id so all three call sites agree.
|
|
5245
|
+
*/
|
|
5246
|
+
private getBakedStaticChildLoop(
|
|
5247
|
+
markerId: string,
|
|
5248
|
+
childComponent: { props: IRLoopChildComponent['props'] },
|
|
5249
|
+
arrayParsed: ParsedExpr | undefined,
|
|
5250
|
+
param: string | undefined,
|
|
5251
|
+
key: string | undefined,
|
|
5252
|
+
): BakedStaticChildLoop | null {
|
|
5253
|
+
if (this.bakedStaticChildLoopCache.has(markerId)) {
|
|
5254
|
+
return this.bakedStaticChildLoopCache.get(markerId) ?? null
|
|
5255
|
+
}
|
|
5256
|
+
const result = analyzeBakeableStaticChildLoop(
|
|
5257
|
+
{ props: childComponent.props, loopArrayParsed: arrayParsed, loopParam: param, loopKey: key },
|
|
5258
|
+
this.state.localConstants,
|
|
5259
|
+
{ isNameShadowed: name => this.state.staticLoopSourceBoundNames.has(name) },
|
|
5260
|
+
)
|
|
5261
|
+
this.bakedStaticChildLoopCache.set(markerId, result)
|
|
5262
|
+
return result
|
|
5263
|
+
}
|
|
5264
|
+
|
|
4977
5265
|
renderLoop(loop: IRLoop): string {
|
|
4978
5266
|
// clientOnly loops: emit SSR markers so the client can insert DOM nodes. The
|
|
4979
5267
|
// marker id disambiguates sibling `.map()` calls under the same parent.
|
|
@@ -5036,8 +5324,44 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5036
5324
|
// Phase A/B) no longer refuses `static-array-from-props`'s `([emoji,
|
|
5037
5325
|
// users]) => ...` param first. Cross-adapter policy: Jinja / ERB apply the
|
|
5038
5326
|
// same narrow check in their own `renderLoop` (see `jinja-adapter.ts`).
|
|
5327
|
+
// #2208: a static-array loop whose body is a single child component
|
|
5328
|
+
// (`loop.childComponent`) with a plain-value prop set can be BAKED —
|
|
5329
|
+
// every per-item prop and data-key resolves to a compile-time-known Go
|
|
5330
|
+
// literal (see `analyzeBakeableStaticChildLoop`), so the constructor
|
|
5331
|
+
// (`generateNewPropsFunction`'s `staticWithoutBody` path) can emit the
|
|
5332
|
+
// child instances directly instead of requiring the loop array bind as
|
|
5333
|
+
// a template variable at all. Memoized by marker id so this gate and
|
|
5334
|
+
// the constructor's later baking agree. A plain-ELEMENT body (no
|
|
5335
|
+
// `childComponent`) is NOT handled by baking and keeps refusing below —
|
|
5336
|
+
// see the go-only follow-up issue for that narrower gap.
|
|
5337
|
+
const bakedChildLoop = loop.childComponent
|
|
5338
|
+
? this.getBakedStaticChildLoop(loop.markerId, loop.childComponent, loop.arrayParsed, loop.param, loop.key ?? undefined)
|
|
5339
|
+
: null
|
|
5340
|
+
|
|
5341
|
+
// #2224 shape 1: a static-array loop whose body is a plain ELEMENT tree
|
|
5342
|
+
// (no child component) has no `.{Name}s`-shaped template target for
|
|
5343
|
+
// #2208's baking to feed — there's nothing for `{{range}}` to iterate at
|
|
5344
|
+
// all once the array itself can't bind as a template variable. Rather
|
|
5345
|
+
// than synthesizing a Go struct type for the item shape (see the #2224
|
|
5346
|
+
// issue body), unroll the body once per item at template-generation
|
|
5347
|
+
// time instead, substituting each item's statically-known field values
|
|
5348
|
+
// directly — see `analyzeBakeableStaticElementLoop`'s docstring for the
|
|
5349
|
+
// exact (conservative) acceptance gate. `null` here means the shape
|
|
5350
|
+
// isn't (yet) bakeable this way; the existing gates below keep firing
|
|
5351
|
+
// exactly as before.
|
|
5352
|
+
const bakedElementLoop = loop.childComponent
|
|
5353
|
+
? null
|
|
5354
|
+
: analyzeBakeableStaticElementLoop(
|
|
5355
|
+
loop,
|
|
5356
|
+
this.state.localConstants,
|
|
5357
|
+
{ isNameShadowed: name => this.state.staticLoopSourceBoundNames.has(name) },
|
|
5358
|
+
)
|
|
5359
|
+
if (bakedElementLoop) {
|
|
5360
|
+
return this.renderUnrolledStaticElementLoop(loop, bakedElementLoop.items)
|
|
5361
|
+
}
|
|
5362
|
+
|
|
5039
5363
|
const arrayName = loop.array.trim()
|
|
5040
|
-
if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
5364
|
+
if (bakedChildLoop === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
5041
5365
|
const arrayConst = this.state.localConstants.find(c => c.name === arrayName)
|
|
5042
5366
|
if (arrayConst && !arrayConst.isModule && arrayConst.parsed && !this.isStringExpr(arrayConst.parsed, new Set())) {
|
|
5043
5367
|
this.state.errors.push({
|
|
@@ -5053,7 +5377,22 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5053
5377
|
}
|
|
5054
5378
|
}
|
|
5055
5379
|
|
|
5056
|
-
|
|
5380
|
+
// #2224 shape 2: when the body IS a child component, `goArray` gets
|
|
5381
|
+
// unconditionally overwritten to `.${componentName}s` below regardless
|
|
5382
|
+
// of what this call returns — so for an INLINE array-literal source
|
|
5383
|
+
// (`[{ label: 'Alpha' }, ...].map(item => <ListItem .../>)`), calling
|
|
5384
|
+
// `convertExpressionToGo` on the raw literal text here is pure waste at
|
|
5385
|
+
// best. At worst it's actively harmful: an array-literal-of-objects
|
|
5386
|
+
// fails the shared `isSupported` gate (`object-literal` is refused
|
|
5387
|
+
// standalone — expression-parser.ts), so this call would push a BF101
|
|
5388
|
+
// as a side effect even though `bakedChildLoop` above (via
|
|
5389
|
+
// `resolveStaticLoopSource`, which evaluates the literal directly
|
|
5390
|
+
// instead of going through `isSupported`) already resolved the SAME
|
|
5391
|
+
// loop just fine. Skip the call entirely for a child-component body —
|
|
5392
|
+
// baked or not, dynamic `.map()` over a real prop/signal array included
|
|
5393
|
+
// — so no spurious diagnostic is ever recorded for a value nothing ends
|
|
5394
|
+
// up consuming.
|
|
5395
|
+
let goArray = loop.childComponent ? '' : this.convertExpressionToGo(loop.array)
|
|
5057
5396
|
const param = loop.param
|
|
5058
5397
|
let index = loop.index || '_'
|
|
5059
5398
|
|
|
@@ -5174,9 +5513,19 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5174
5513
|
let filterCond: string
|
|
5175
5514
|
|
|
5176
5515
|
if (loop.filterPredicate.predicate) {
|
|
5516
|
+
// #2228: for a wrapper-slice loop (`.TodoItems` ranging over
|
|
5517
|
+
// TodoItemProps), `.` in the predicate is the WHOLE wrapper struct —
|
|
5518
|
+
// qualify `loop.filterPredicate.param` references through the
|
|
5519
|
+
// datum-carrying field (`.Todo.Done`, not `.Done`) so the emitted
|
|
5520
|
+
// `{{if}}` only ever dereferences fields the wrapper struct actually
|
|
5521
|
+
// has. `wrapperDatumField` returns `null` for a plain-element-body
|
|
5522
|
+
// loop, where `.` already IS the datum — `renderPredicateCondition`
|
|
5523
|
+
// then keeps emitting the bare `.`/`.Field` form unchanged.
|
|
5524
|
+
const datumField = this.wrapperDatumField(loop)
|
|
5177
5525
|
filterCond = this.renderPredicateCondition(
|
|
5178
5526
|
loop.filterPredicate.predicate,
|
|
5179
|
-
loop.filterPredicate.param
|
|
5527
|
+
loop.filterPredicate.param,
|
|
5528
|
+
datumField
|
|
5180
5529
|
)
|
|
5181
5530
|
} else {
|
|
5182
5531
|
filterCond = 'true'
|
|
@@ -5188,6 +5537,61 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5188
5537
|
return `{{bfComment "loop:${loop.markerId}"}}{{range $${rangeIndex}, $${rangeValue} := ${goArray}}}${itemMarker}${children}{{end}}{{bfComment "/loop:${loop.markerId}"}}`
|
|
5189
5538
|
}
|
|
5190
5539
|
|
|
5540
|
+
/**
|
|
5541
|
+
* #2224 shape 1: render a static-array, plain-element-body loop's per-item
|
|
5542
|
+
* markup ONCE PER ITEM at template-generation time instead of a Go
|
|
5543
|
+
* `{{range}}` — `analyzeBakeableStaticElementLoop` has already verified
|
|
5544
|
+
* every expression in `loop.children` resolves against each item. Keeps
|
|
5545
|
+
* the SAME `<!--bf-loop:id--> ... <!--/bf-loop:id-->` marker pair a
|
|
5546
|
+
* dynamic loop emits (so the CSR-side static `forEach` wiring, compiled by
|
|
5547
|
+
* the separate `ir-to-client-js.ts` pass and untouched by this change,
|
|
5548
|
+
* still finds the same DOM range), and pushes `loop.param` /
|
|
5549
|
+
* `loop.depth` onto the SAME stacks `renderLoop`'s `{{range}}` path uses,
|
|
5550
|
+
* so `data-key`/`data-key-N` attribute-name derivation
|
|
5551
|
+
* (`renderAttributes`) is unaffected by which path rendered the loop. No
|
|
5552
|
+
* `itemMarker` (`loopItemMarker`) call: the analysis gate already refused
|
|
5553
|
+
* `bodyIsMultiRoot` / `bodyIsItemConditional` bodies, so it would always
|
|
5554
|
+
* return `''` here anyway.
|
|
5555
|
+
*/
|
|
5556
|
+
private renderUnrolledStaticElementLoop(loop: IRLoop, items: readonly unknown[]): string {
|
|
5557
|
+
this.inLoop = true
|
|
5558
|
+
this.loopWrapperStack.push(false)
|
|
5559
|
+
this.loopKeyDepthStack.push(loop.depth)
|
|
5560
|
+
this.loopScalarItemStack.push(this.scalarLiteralLoopGoType(loop.arrayParsed, loop.itemType) !== null)
|
|
5561
|
+
this.loopParamStack.push(loop.param)
|
|
5562
|
+
|
|
5563
|
+
let body = ''
|
|
5564
|
+
for (const item of items) {
|
|
5565
|
+
this.staticLoopItemStack.push({ param: loop.param, item })
|
|
5566
|
+
body += this.renderChildren(loop.children)
|
|
5567
|
+
this.staticLoopItemStack.pop()
|
|
5568
|
+
if (this.staticLoopBakeFailed) {
|
|
5569
|
+
// Invariant violation (see `staticLoopBakeFailed`'s docstring): the
|
|
5570
|
+
// gate and this render pass disagreed. Surface it loudly instead of
|
|
5571
|
+
// shipping a template with `""` sentinels silently spliced in.
|
|
5572
|
+
this.staticLoopBakeFailed = false
|
|
5573
|
+
this.state.errors.push({
|
|
5574
|
+
code: 'BF101',
|
|
5575
|
+
severity: 'error',
|
|
5576
|
+
message: `Loop array \`${loop.array.trim()}\` could not be fully unrolled — an expression in the loop body did not resolve against every item as the compile-time analysis expected.`,
|
|
5577
|
+
loc: loop.loc ?? this.makeLoc(),
|
|
5578
|
+
suggestion: {
|
|
5579
|
+
message: 'This indicates a bug in the Go adapter\'s static-loop unrolling (#2224) rather than an unsupported source pattern; please file a bug with a reproduction.',
|
|
5580
|
+
},
|
|
5581
|
+
})
|
|
5582
|
+
break
|
|
5583
|
+
}
|
|
5584
|
+
}
|
|
5585
|
+
|
|
5586
|
+
this.loopParamStack.pop()
|
|
5587
|
+
this.loopScalarItemStack.pop()
|
|
5588
|
+
this.loopKeyDepthStack.pop()
|
|
5589
|
+
this.loopWrapperStack.pop()
|
|
5590
|
+
this.inLoop = false
|
|
5591
|
+
|
|
5592
|
+
return `{{bfComment "loop:${loop.markerId}"}}${body}{{bfComment "/loop:${loop.markerId}"}}`
|
|
5593
|
+
}
|
|
5594
|
+
|
|
5191
5595
|
/**
|
|
5192
5596
|
* Per-item `<!--bf-loop-i-->` / `<!--bf-loop-i:KEY-->` start marker emitted
|
|
5193
5597
|
* inside a `{{range}}` body. Multi-root Fragment items get the bare anchor;
|
|
@@ -5574,6 +5978,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5574
5978
|
// predicate (no BF101 / BF102). This keeps the BF102 remediation ("defer
|
|
5575
5979
|
// it with /* @client */") accurate for attribute-only state.
|
|
5576
5980
|
if (attr.clientOnly) continue
|
|
5981
|
+
// `dangerouslySetInnerHTML` never renders as an HTML attribute — it's
|
|
5982
|
+
// handled by `renderDangerousInnerHtml` instead, which replaces the
|
|
5983
|
+
// element's children. Skip it here so its `{ __html: ... }` object
|
|
5984
|
+
// literal never reaches the generic object-literal BF101 refusal
|
|
5985
|
+
// (which would double-report alongside the purpose-built one).
|
|
5986
|
+
if (isDangerousInnerHtmlAttr(attr)) continue
|
|
5577
5987
|
// Rewrite JSX special-prop names to their HTML-attribute counterparts. The
|
|
5578
5988
|
// Go template adapter has no JSX runtime to strip `key` / emit `data-key`,
|
|
5579
5989
|
// so the rewrite happens at attribute-emit time. Mirror of the `key`
|