@barefootjs/go-template 0.18.5 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/emit-context.d.ts +11 -5
- package/dist/adapter/emit-context.d.ts.map +1 -1
- package/dist/adapter/go-template-adapter.d.ts +153 -6
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +514 -53
- package/dist/adapter/lib/compile-state.d.ts +24 -0
- package/dist/adapter/lib/compile-state.d.ts.map +1 -1
- package/dist/adapter/lib/types.d.ts +9 -0
- package/dist/adapter/lib/types.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/adapter/props/prop-types.d.ts +45 -0
- package/dist/adapter/props/prop-types.d.ts.map +1 -1
- package/dist/build.js +514 -53
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +516 -62
- 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 +876 -21
- 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/emit-context.ts +14 -5
- package/src/adapter/go-template-adapter.ts +620 -45
- package/src/adapter/lib/compile-state.ts +27 -0
- package/src/adapter/lib/types.ts +9 -0
- package/src/adapter/props/prop-classes.ts +34 -9
- package/src/adapter/props/prop-types.ts +178 -1
- package/src/adapter/value/value-lowering.ts +1 -1
- package/src/conformance-pins.ts +30 -31
- package/src/render-divergences.ts +12 -0
- package/src/test-render.ts +127 -10
|
@@ -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"
|
|
@@ -126,7 +135,7 @@ import { lowerCtorExpr } from "./memo/ctor-lowering.ts"
|
|
|
126
135
|
import { resolveBlockBodyMemoModuleConst } from "./memo/memo-value.ts"
|
|
127
136
|
import { computeMemoInitialValue, computeMemoInitialValueOrNull, filterArmEarlierSiblingRefs } from "./memo/memo-compute.ts"
|
|
128
137
|
import { collectSpreadSlots, buildSpreadInitializer } from "./spread/spread-codegen.ts"
|
|
129
|
-
import { buildPropTypeOverrides, resolvePropGoType, collectNillablePropNames } from "./props/prop-types.ts"
|
|
138
|
+
import { buildPropTypeOverrides, resolvePropGoType, collectNillablePropNames, collectNullishConsumedPropNames, collectOmittableAttrConsumedPropNames, NULLISH_SCALAR_GO_TYPES } from "./props/prop-types.ts"
|
|
130
139
|
import { collectStringValueNames } from "./props/prop-classes.ts"
|
|
131
140
|
|
|
132
141
|
export type { GoTemplateAdapterOptions } from "./lib/types.ts"
|
|
@@ -215,8 +224,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
215
224
|
this.convertExpressionToGo(jsExpr, out, preParsed),
|
|
216
225
|
convertConditionToGo: (jsCondition, preParsed) =>
|
|
217
226
|
this.convertConditionToGo(jsCondition, preParsed),
|
|
218
|
-
extractPropNameFromInitialValue: (initialValue) =>
|
|
219
|
-
|
|
227
|
+
extractPropNameFromInitialValue: (initialValue, preParsed) =>
|
|
228
|
+
this.extractPropNameFromInitialValue(initialValue, preParsed),
|
|
229
|
+
extractPropFallback: (initialValue, preParsed) => this.extractPropFallback(initialValue, preParsed),
|
|
220
230
|
resolveModuleStringConst: (name) => this.resolveModuleStringConst(name),
|
|
221
231
|
}
|
|
222
232
|
|
|
@@ -226,6 +236,22 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
226
236
|
}
|
|
227
237
|
|
|
228
238
|
private inLoop: boolean = false
|
|
239
|
+
/**
|
|
240
|
+
* Memoized `analyzeBakeableStaticChildLoop` result per loop marker id
|
|
241
|
+
* (#2208). Consulted from three sites that all need the SAME verdict for
|
|
242
|
+
* the SAME loop — the `renderLoop` gate, the Input struct's field list,
|
|
243
|
+
* and the constructor's per-item construction. `renderLoop`'s gate runs
|
|
244
|
+
* during `generate()`'s render pass; the other two run during
|
|
245
|
+
* `generateTypes()`'s constructor-generation pass, which `generate()`
|
|
246
|
+
* also invokes internally partway through — so this cache is reset (in
|
|
247
|
+
* `primeCompileState`, not here) between those two passes too. Agreement
|
|
248
|
+
* across all three sites is therefore guaranteed by `analyzeBakeable-
|
|
249
|
+
* StaticChildLoop` being a deterministic pure function of the (re-primed)
|
|
250
|
+
* per-compile state, not by one shared memo spanning every read; the
|
|
251
|
+
* cache's value is avoiding redundant recomputation WITHIN the pass that
|
|
252
|
+
* populated it, not correctness across passes.
|
|
253
|
+
*/
|
|
254
|
+
private bakedStaticChildLoopCache = new Map<string, BakedStaticChildLoop | null>()
|
|
229
255
|
private loopParamStack: string[] = []
|
|
230
256
|
/**
|
|
231
257
|
* Stack of `IRLoop.depth` values (innermost last), pushed/popped around
|
|
@@ -272,6 +298,30 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
272
298
|
* alone (#2087 Phase B).
|
|
273
299
|
*/
|
|
274
300
|
private loopRestExcludeStack: Array<Map<string, { parent: string; excludeKeys: string[] }>> = []
|
|
301
|
+
/**
|
|
302
|
+
* Active per-item static bindings for a #2224 unrolled plain-element loop
|
|
303
|
+
* (`renderUnrolledStaticElementLoop`) — innermost last, so a nested static
|
|
304
|
+
* unroll inside another one resolves against its own item, not an outer
|
|
305
|
+
* one's. When non-empty, `convertExpressionToGo`'s top-of-function check
|
|
306
|
+
* resolves EVERY expression against the innermost entry's item via
|
|
307
|
+
* `evaluateStaticLiteral` and returns a literal Go value instead of
|
|
308
|
+
* descending into the normal `.Field`-style dot-context lowering — there
|
|
309
|
+
* is no real `{{range}}` establishing that context during an unroll, so
|
|
310
|
+
* `identifier()`'s `currentLoopParam → '.'` branch would otherwise resolve
|
|
311
|
+
* against the WRONG (enclosing) dot context. `analyzeBakeableStaticElementLoop`
|
|
312
|
+
* has already verified every expression in the body resolves this way for
|
|
313
|
+
* every item, so the fallback failure branch below is a defensive
|
|
314
|
+
* invariant check, not a real code path.
|
|
315
|
+
*/
|
|
316
|
+
private staticLoopItemStack: Array<{ param: string; item: unknown }> = []
|
|
317
|
+
/**
|
|
318
|
+
* Set by the `convertExpressionToGo` override above when a
|
|
319
|
+
* `staticLoopItemStack` entry is active but the current expression fails
|
|
320
|
+
* to resolve against it — should never happen (see that check's comment),
|
|
321
|
+
* but `renderUnrolledStaticElementLoop` asserts this stays `false` after
|
|
322
|
+
* every item render rather than silently shipping a `""` sentinel.
|
|
323
|
+
*/
|
|
324
|
+
private staticLoopBakeFailed = false
|
|
275
325
|
|
|
276
326
|
/**
|
|
277
327
|
* Cross-component child shapes, keyed by child component name. Populated via
|
|
@@ -307,6 +357,32 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
307
357
|
this.state.restPropsName = ir.metadata.restPropsName ?? null
|
|
308
358
|
this.state.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants)
|
|
309
359
|
this.state.localConstants = ir.metadata.localConstants ?? []
|
|
360
|
+
// #2208 fable review: every name a `.map()`/`.filter()` loop callback
|
|
361
|
+
// binds as its item/index parameter anywhere in the component. Static
|
|
362
|
+
// loop-source resolution (`getBakedStaticChildLoop` /
|
|
363
|
+
// `analyzeBakeableStaticChildLoop`) must never resolve a const whose
|
|
364
|
+
// name a DIFFERENT, enclosing loop's own callback param shadows.
|
|
365
|
+
// Computed here (not from live render-time stack state) because this
|
|
366
|
+
// must agree across THREE call sites, two of which (`generateTypes`'s
|
|
367
|
+
// Input-struct + constructor generation) run OUTSIDE the live
|
|
368
|
+
// `renderLoop` tree-walk that would otherwise track shadowing via
|
|
369
|
+
// stack push/pop — same coarse-but-safe mitigation as #2212.
|
|
370
|
+
this.state.staticLoopSourceBoundNames = collectLoopBoundNames(ir)
|
|
371
|
+
// #2208 fable re-review: the adapter instance is a reused singleton —
|
|
372
|
+
// `generate()` calls this once per component, but `generateTypes()` is
|
|
373
|
+
// ALSO a standalone public entry point (the Go conformance harness in
|
|
374
|
+
// `test-render.ts` calls it directly on an already-`generate()`d
|
|
375
|
+
// adapter for a sibling/child IR). Resetting the bake cache HERE, not
|
|
376
|
+
// just in `generate()`, closes that door too: a stale entry keyed by a
|
|
377
|
+
// marker id that collides with a PREVIOUS component (marker ids
|
|
378
|
+
// restart at `l0` per component) would otherwise either silently
|
|
379
|
+
// suppress this fix or leak that other component's baked data into
|
|
380
|
+
// this one's constructor. `generate()` itself calls `generateTypes()`
|
|
381
|
+
// partway through — re-priming (and so re-clearing the cache) there is
|
|
382
|
+
// harmless: `analyzeBakeableStaticChildLoop` is deterministic over
|
|
383
|
+
// identically-primed state, so a cache miss on the second pass just
|
|
384
|
+
// recomputes the same answer.
|
|
385
|
+
this.bakedStaticChildLoopCache = new Map()
|
|
310
386
|
this.state.localHelperNames = new Set(
|
|
311
387
|
this.state.localConstants.filter(c => !c.isModule && c.containsArrow).map(c => c.name),
|
|
312
388
|
)
|
|
@@ -331,6 +407,19 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
331
407
|
}
|
|
332
408
|
this.state.loweringMatchers = prepareLoweringMatchers(ir.metadata)
|
|
333
409
|
augmentInheritedPropAccesses(ir)
|
|
410
|
+
// The consumed-prop sets feed `resolvePropGoType`'s interface{} flips
|
|
411
|
+
// (#2248/#2259) and `collectNillablePropNames` is derived from
|
|
412
|
+
// `resolvePropGoType`, which also consults the local type tables — so
|
|
413
|
+
// build the tables first and populate all three HERE, where both entry
|
|
414
|
+
// points share them. Computing them only in `generate()` left the
|
|
415
|
+
// standalone `generateTypes()` entry (sibling IRs in the conformance
|
|
416
|
+
// harness) resolving structs against another component's sets, and
|
|
417
|
+
// `generate()` itself computing nillability against the PREVIOUS
|
|
418
|
+
// compile's type tables.
|
|
419
|
+
this.buildLocalTypeTables(ir, ir.metadata.componentName)
|
|
420
|
+
this.state.nullishConsumedPropNames = collectNullishConsumedPropNames(this.emitCtx, ir)
|
|
421
|
+
this.state.omittableAttrConsumedPropNames = collectOmittableAttrConsumedPropNames(this.emitCtx, ir)
|
|
422
|
+
this.state.nillablePropNames = collectNillablePropNames(this.emitCtx, ir)
|
|
334
423
|
}
|
|
335
424
|
|
|
336
425
|
/** Generate template output for a component. */
|
|
@@ -341,7 +430,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
341
430
|
this.state.templateVarCounter = 0
|
|
342
431
|
this.state.pendingChildrenDefines = []
|
|
343
432
|
this.primeCompileState(ir)
|
|
344
|
-
this.state.nillablePropNames = collectNillablePropNames(this.emitCtx, ir)
|
|
345
433
|
this.state.stringValueNames = collectStringValueNames(ir)
|
|
346
434
|
|
|
347
435
|
// Surface loop-body usages of sibling-imported components (see
|
|
@@ -856,6 +944,18 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
856
944
|
}
|
|
857
945
|
|
|
858
946
|
for (const nested of inputNested) {
|
|
947
|
+
// #2208: a static loop whose array source is itself fully-static
|
|
948
|
+
// (baked directly in the constructor — see `generateNewPropsFunction`)
|
|
949
|
+
// has no caller-supplied data to accept; the Input struct carries no
|
|
950
|
+
// field for it at all (there was never a working `in.<Name>s` path
|
|
951
|
+
// for this shape before this fix — it refused with BF101).
|
|
952
|
+
if (nested.loopMarkerId && this.getBakedStaticChildLoop(
|
|
953
|
+
nested.loopMarkerId,
|
|
954
|
+
nested,
|
|
955
|
+
nested.loopArrayParsed,
|
|
956
|
+
nested.loopParam,
|
|
957
|
+
nested.loopKey,
|
|
958
|
+
)) continue
|
|
859
959
|
lines.push(`\t${nested.name}s []${nested.name}Input`)
|
|
860
960
|
}
|
|
861
961
|
|
|
@@ -1074,6 +1174,36 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1074
1174
|
// Static nested WITHOUT body children.
|
|
1075
1175
|
for (const nested of staticWithoutBody) {
|
|
1076
1176
|
const varName = `${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`
|
|
1177
|
+
// #2208: a static loop whose ARRAY SOURCE is itself fully-static
|
|
1178
|
+
// (`const items = [{ label: 'Alpha' }, ...]`) has no caller input to
|
|
1179
|
+
// wait for — every item's props/data-key are already known at
|
|
1180
|
+
// compile time. Bake them directly instead of ranging over
|
|
1181
|
+
// `in.<Name>s` (which stays empty forever for this shape, since the
|
|
1182
|
+
// loop-source gate above only lets this fixture through BECAUSE it's
|
|
1183
|
+
// baked here).
|
|
1184
|
+
const baked = nested.loopMarkerId
|
|
1185
|
+
? this.getBakedStaticChildLoop(
|
|
1186
|
+
nested.loopMarkerId,
|
|
1187
|
+
nested,
|
|
1188
|
+
nested.loopArrayParsed,
|
|
1189
|
+
nested.loopParam,
|
|
1190
|
+
nested.loopKey,
|
|
1191
|
+
)
|
|
1192
|
+
: null
|
|
1193
|
+
if (baked) {
|
|
1194
|
+
lines.push(`\t${varName} := make([]${nested.name}Props, ${baked.items.length})`)
|
|
1195
|
+
baked.items.forEach((item, i) => {
|
|
1196
|
+
const fields = item.inputFields.map(f => `${f.goField}: ${f.goValue}`).join(', ')
|
|
1197
|
+
lines.push(`\t${varName}[${i}] = New${nested.name}Props(${nested.name}Input{${fields}})`)
|
|
1198
|
+
lines.push(`\t${varName}[${i}].BfParent = scopeID`)
|
|
1199
|
+
lines.push(`\t${varName}[${i}].BfMount = "${nested.slotId}"`)
|
|
1200
|
+
if (item.dataKey !== null) {
|
|
1201
|
+
lines.push(`\t${varName}[${i}].BfDataKey = ${JSON.stringify(item.dataKey)}`)
|
|
1202
|
+
}
|
|
1203
|
+
})
|
|
1204
|
+
lines.push('')
|
|
1205
|
+
continue
|
|
1206
|
+
}
|
|
1077
1207
|
lines.push(`\t${varName} := make([]${nested.name}Props, len(in.${nested.name}s))`)
|
|
1078
1208
|
lines.push(`\tfor i, item := range in.${nested.name}s {`)
|
|
1079
1209
|
lines.push(`\t\t${varName}[i] = New${nested.name}Props(item)`)
|
|
@@ -1096,10 +1226,26 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1096
1226
|
// from an omitted field, so it also fires on the type's zero value.
|
|
1097
1227
|
const propFallbackVars = this.collectPropFallbackVars(ir)
|
|
1098
1228
|
for (const [, info] of propFallbackVars) {
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1229
|
+
if (info.assertType) {
|
|
1230
|
+
// Nillable-lowered prop (#2248): the `interface{}` field makes
|
|
1231
|
+
// "absent" (nil) distinguishable from an explicit `''`/`0`/`false`,
|
|
1232
|
+
// so the fallback applies ONLY on nil — JS `??` semantics. Numbers
|
|
1233
|
+
// coerce through the runtime (an untyped `Size: 3` literal boxes as
|
|
1234
|
+
// int even into a float64-shaped prop); string/bool assert directly.
|
|
1235
|
+
const deref =
|
|
1236
|
+
info.assertType === 'int' ? `bf.ToInt(in.${info.fieldName})`
|
|
1237
|
+
: info.assertType === 'float64' ? `bf.ToFloat64(in.${info.fieldName})`
|
|
1238
|
+
: `in.${info.fieldName}.(${info.assertType})`
|
|
1239
|
+
lines.push(`\tvar ${info.varName} ${info.assertType} = ${info.goFallback}`)
|
|
1240
|
+
lines.push(`\tif in.${info.fieldName} != nil {`)
|
|
1241
|
+
lines.push(`\t\t${info.varName} = ${deref}`)
|
|
1242
|
+
lines.push(`\t}`)
|
|
1243
|
+
} else {
|
|
1244
|
+
lines.push(`\t${info.varName} := in.${info.fieldName}`)
|
|
1245
|
+
lines.push(`\tif ${info.varName} == ${info.zeroLiteral} {`)
|
|
1246
|
+
lines.push(`\t\t${info.varName} = ${info.goFallback}`)
|
|
1247
|
+
lines.push(`\t}`)
|
|
1248
|
+
}
|
|
1103
1249
|
}
|
|
1104
1250
|
if (propFallbackVars.size > 0) lines.push('')
|
|
1105
1251
|
|
|
@@ -1208,7 +1354,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
1208
1354
|
if (propFieldNames.has(fieldName)) continue
|
|
1209
1355
|
// `props.X ?? N` reuses the hoisted fallback var so signal and memo share
|
|
1210
1356
|
// one value.
|
|
1211
|
-
const fallbackMatch = this.extractPropFallback(signal.initialValue)
|
|
1357
|
+
const fallbackMatch = this.extractPropFallback(signal.initialValue, signal.parsed)
|
|
1212
1358
|
const hoisted = fallbackMatch ? propFallbackVars.get(fallbackMatch.propName) : undefined
|
|
1213
1359
|
if (hoisted) {
|
|
1214
1360
|
lines.push(`\t\t${fieldName}: ${hoisted.varName},`)
|
|
@@ -2598,8 +2744,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2598
2744
|
localTaken.add(`${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`)
|
|
2599
2745
|
}
|
|
2600
2746
|
|
|
2747
|
+
const propTypeOverrides = buildPropTypeOverrides(this.emitCtx, ir)
|
|
2601
2748
|
for (const signal of ir.metadata.signals) {
|
|
2602
|
-
const match = this.extractPropFallback(signal.initialValue)
|
|
2749
|
+
const match = this.extractPropFallback(signal.initialValue, signal.parsed)
|
|
2603
2750
|
if (!match) continue
|
|
2604
2751
|
if (result.has(match.propName)) continue
|
|
2605
2752
|
const param = ir.metadata.propsParams.find(p => p.name === match.propName)
|
|
@@ -2607,6 +2754,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2607
2754
|
// A destructure default already wins via applyGoFallback below.
|
|
2608
2755
|
if (goPropDefault(param.defaultValue) !== null) continue
|
|
2609
2756
|
const fieldName = capitalizeFieldName(match.propName)
|
|
2757
|
+
// A `??`-consumed optional scalar lowered to `interface{}` (#2248) —
|
|
2758
|
+
// detected off the SAME `resolvePropGoType` pipeline the struct
|
|
2759
|
+
// generators use, so this can't drift from the emitted field type. The
|
|
2760
|
+
// concrete pre-flip type is what the hoisted local materializes as.
|
|
2761
|
+
const concreteType = propTypeOverrides.get(param.name) ?? typeInfoToGo(this.emitCtx, param.type, param.defaultValue)
|
|
2762
|
+
const nullishLowered =
|
|
2763
|
+
NULLISH_SCALAR_GO_TYPES.has(concreteType) &&
|
|
2764
|
+
resolvePropGoType(this.emitCtx, param, propTypeOverrides) === 'interface{}'
|
|
2610
2765
|
// Pick the zero literal based on the fallback's literal shape. Bool
|
|
2611
2766
|
// fallbacks (`?? true`) hoist against the `false` zero — the same Go-zero
|
|
2612
2767
|
// conflation the int / string cases accept: the caller can't distinguish
|
|
@@ -2625,8 +2780,16 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2625
2780
|
// (`?? 0`, `?? ''`, `?? false`, `?? 0.0`). Compare against the computed
|
|
2626
2781
|
// zeroLiteral so spelling variants like `0.0` collapse to the same skip
|
|
2627
2782
|
// as `0`.
|
|
2628
|
-
|
|
2629
|
-
|
|
2783
|
+
//
|
|
2784
|
+
// NOT a no-op for a nillable-lowered prop (#2248): its `interface{}`
|
|
2785
|
+
// field can be nil, and the typed hoisted local is what downstream
|
|
2786
|
+
// consumers (`Count: size`, memo env maps) assign from — a nil
|
|
2787
|
+
// interface into a concrete field is a compile error, so the local
|
|
2788
|
+
// must exist even when the fallback equals the zero value.
|
|
2789
|
+
if (!nullishLowered) {
|
|
2790
|
+
if (match.goFallback === zeroLiteral) continue
|
|
2791
|
+
if (zeroLiteral === '0' && Number(match.goFallback) === 0) continue
|
|
2792
|
+
}
|
|
2630
2793
|
// The JSX-side identifier is the natural local name; suffix with `_` if it
|
|
2631
2794
|
// collides with a Go keyword or a local we already emit.
|
|
2632
2795
|
let varName = match.propName
|
|
@@ -2634,21 +2797,43 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2634
2797
|
varName += '_'
|
|
2635
2798
|
}
|
|
2636
2799
|
localTaken.add(varName)
|
|
2637
|
-
result.set(match.propName, {
|
|
2800
|
+
result.set(match.propName, {
|
|
2801
|
+
varName,
|
|
2802
|
+
fieldName,
|
|
2803
|
+
goFallback: match.goFallback,
|
|
2804
|
+
zeroLiteral,
|
|
2805
|
+
...(nullishLowered ? { assertType: concreteType } : {}),
|
|
2806
|
+
})
|
|
2638
2807
|
}
|
|
2639
2808
|
return result
|
|
2640
2809
|
}
|
|
2641
2810
|
|
|
2642
2811
|
/**
|
|
2643
|
-
* Parse a signal-time initial value of the form `props.X ?? <literal>`
|
|
2644
|
-
*
|
|
2645
|
-
*
|
|
2646
|
-
* the fallback isn't a simple literal `goPropDefault`
|
|
2812
|
+
* Parse a signal-time initial value of the form `props.X ?? <literal>` —
|
|
2813
|
+
* or, for destructured components, `x ?? <literal>` — into the source prop
|
|
2814
|
+
* name and the Go-formatted fallback. Returns null when the expression
|
|
2815
|
+
* isn't that shape or the fallback isn't a simple literal `goPropDefault`
|
|
2816
|
+
* can translate.
|
|
2817
|
+
*
|
|
2818
|
+
* `preParsed` (the signal's best-effort `ParsedExpr`) is matched
|
|
2819
|
+
* structurally when available; the regex handles only the props-object
|
|
2820
|
+
* member form for callers without a tree (memo computations). A bare
|
|
2821
|
+
* identifier can shadow a same-named prop (loop/callback params — the
|
|
2822
|
+
* `collectNullishConsumedPropNames` limitation class), so every caller
|
|
2823
|
+
* validates the returned name against `ir.metadata.propsParams`.
|
|
2647
2824
|
*
|
|
2648
2825
|
* Keeps the original prop reference (not just the resolved value) so
|
|
2649
2826
|
* caller-supplied non-zero inputs are honoured.
|
|
2650
2827
|
*/
|
|
2651
|
-
private extractPropFallback(
|
|
2828
|
+
private extractPropFallback(
|
|
2829
|
+
initialValue: string,
|
|
2830
|
+
preParsed?: ParsedExpr,
|
|
2831
|
+
): { propName: string; goFallback: string } | null {
|
|
2832
|
+
const structural = preParsed ? this.extractPropFallbackFromParsed(preParsed) : null
|
|
2833
|
+
if (structural) return structural
|
|
2834
|
+
|
|
2835
|
+
// Regex fallback for callers without a tree (memo computation strings)
|
|
2836
|
+
// and for props-object shapes the structural match doesn't model.
|
|
2652
2837
|
if (!this.state.propsObjectName) return null
|
|
2653
2838
|
const trimmed = initialValue.trim()
|
|
2654
2839
|
const name = this.state.propsObjectName
|
|
@@ -2662,12 +2847,64 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2662
2847
|
return { propName: m[1], goFallback }
|
|
2663
2848
|
}
|
|
2664
2849
|
|
|
2850
|
+
/** Structural half of {@link extractPropFallback}. */
|
|
2851
|
+
private extractPropFallbackFromParsed(
|
|
2852
|
+
preParsed: ParsedExpr,
|
|
2853
|
+
): { propName: string; goFallback: string } | null {
|
|
2854
|
+
if (preParsed.kind !== 'logical' || preParsed.op !== '??') return null
|
|
2855
|
+
const left = preParsed.left
|
|
2856
|
+
const propName =
|
|
2857
|
+
left.kind === 'identifier' && !this.state.propsObjectName
|
|
2858
|
+
? left.name
|
|
2859
|
+
: left.kind === 'member' &&
|
|
2860
|
+
!left.computed &&
|
|
2861
|
+
left.object.kind === 'identifier' &&
|
|
2862
|
+
left.object.name === this.state.propsObjectName
|
|
2863
|
+
? left.property
|
|
2864
|
+
: null
|
|
2865
|
+
if (!propName) return null
|
|
2866
|
+
// A negative fallback (`?? -1`) parses as unary minus around a number
|
|
2867
|
+
// literal, not a literal.
|
|
2868
|
+
let right = preParsed.right
|
|
2869
|
+
let negate = ''
|
|
2870
|
+
if (right.kind === 'unary' && right.op === '-') {
|
|
2871
|
+
negate = '-'
|
|
2872
|
+
right = right.argument
|
|
2873
|
+
}
|
|
2874
|
+
if (right.kind !== 'literal') return null
|
|
2875
|
+
if (negate && right.literalType !== 'number') return null
|
|
2876
|
+
// A string fallback is already the decoded value — quote it directly;
|
|
2877
|
+
// routing it through `goPropDefault` would strip JSON.stringify's outer
|
|
2878
|
+
// quotes and escape the body a second time (`"` → `\\\"`).
|
|
2879
|
+
if (right.literalType === 'string') {
|
|
2880
|
+
return { propName, goFallback: JSON.stringify(right.value) }
|
|
2881
|
+
}
|
|
2882
|
+
// Numbers keep their source spelling when available (`raw` is only
|
|
2883
|
+
// populated for numbers); booleans/null stringify losslessly.
|
|
2884
|
+
const goFallback = goPropDefault(negate + (right.raw ?? String(right.value)))
|
|
2885
|
+
if (goFallback === null) return null
|
|
2886
|
+
return { propName, goFallback }
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2665
2889
|
/**
|
|
2666
2890
|
* Extract the prop name from a signal's `props.xxx`-pattern initialValue,
|
|
2667
2891
|
* e.g. `"props.initial ?? 0"` → `"initial"`, `"props.checked"` → `"checked"`.
|
|
2892
|
+
* For destructured components (`propsObjectName` null) the same shapes are
|
|
2893
|
+
* matched structurally on `preParsed` with an identifier left operand
|
|
2894
|
+
* (`size ?? 0` → `"size"`); callers validate the name against
|
|
2895
|
+
* `propsParams`, which filters shadowing locals.
|
|
2668
2896
|
*/
|
|
2669
|
-
private extractPropNameFromInitialValue(initialValue: string): string | null {
|
|
2670
|
-
if (!this.state.propsObjectName)
|
|
2897
|
+
private extractPropNameFromInitialValue(initialValue: string, preParsed?: ParsedExpr): string | null {
|
|
2898
|
+
if (!this.state.propsObjectName) {
|
|
2899
|
+
if (
|
|
2900
|
+
preParsed?.kind === 'logical' &&
|
|
2901
|
+
(preParsed.op === '??' || preParsed.op === '||') &&
|
|
2902
|
+
preParsed.left.kind === 'identifier'
|
|
2903
|
+
) {
|
|
2904
|
+
return preParsed.left.name
|
|
2905
|
+
}
|
|
2906
|
+
return null
|
|
2907
|
+
}
|
|
2671
2908
|
const trimmed = initialValue.trim()
|
|
2672
2909
|
const name = this.state.propsObjectName
|
|
2673
2910
|
|
|
@@ -2757,7 +2994,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2757
2994
|
renderElement(element: IRElement): string {
|
|
2758
2995
|
const tag = element.tag
|
|
2759
2996
|
const attrs = this.renderAttributes(element)
|
|
2760
|
-
const
|
|
2997
|
+
const dangerousHtml = this.renderDangerousInnerHtml(element)
|
|
2998
|
+
const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children)
|
|
2761
2999
|
|
|
2762
3000
|
let hydrationAttrs = ''
|
|
2763
3001
|
if (element.needsScope) {
|
|
@@ -2792,6 +3030,28 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
2792
3030
|
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`
|
|
2793
3031
|
}
|
|
2794
3032
|
|
|
3033
|
+
/**
|
|
3034
|
+
* `dangerouslySetInnerHTML={{ __html: '...' }}` (#2207) — see the Blade
|
|
3035
|
+
* adapter's identical helper for the full rationale. `null` means the
|
|
3036
|
+
* attribute is absent (caller falls through to normal `renderChildren`);
|
|
3037
|
+
* a non-`null` string (possibly `''`) replaces the children outright.
|
|
3038
|
+
*/
|
|
3039
|
+
private renderDangerousInnerHtml(element: IRElement): string | null {
|
|
3040
|
+
const resolution = resolveDangerousInnerHtml(element)
|
|
3041
|
+
if (!resolution) return null
|
|
3042
|
+
if (resolution.kind === 'dynamic') {
|
|
3043
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc))
|
|
3044
|
+
return ''
|
|
3045
|
+
}
|
|
3046
|
+
const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name)
|
|
3047
|
+
if (violation) {
|
|
3048
|
+
const attr = element.attrs.find(isDangerousInnerHtmlAttr)!
|
|
3049
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation))
|
|
3050
|
+
return ''
|
|
3051
|
+
}
|
|
3052
|
+
return resolution.html
|
|
3053
|
+
}
|
|
3054
|
+
|
|
2795
3055
|
renderExpression(expr: IRExpression): string {
|
|
2796
3056
|
// @client directive: render a comment marker; ClientJS evaluates the
|
|
2797
3057
|
// expression via updateClientMarker().
|
|
@@ -3386,9 +3646,54 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
3386
3646
|
const wrapLeft = wrapIfMultiToken(emit(left))
|
|
3387
3647
|
const wrapRight = wrapIfMultiToken(emit(right))
|
|
3388
3648
|
if (op === '&&') return `and ${wrapLeft} ${wrapRight}`
|
|
3649
|
+
// `??` on a nillable prop needs true JS nullish semantics (#2248): Go's
|
|
3650
|
+
// `or` is truthiness-based, so `{{or .Label "Default"}}` falls back on a
|
|
3651
|
+
// present-but-empty `""` — JS `??` keeps it. `bf_nullish` tests nil-ness
|
|
3652
|
+
// only. Non-nillable operands keep `or`: their concrete Go type can't
|
|
3653
|
+
// represent "absent" at all, so `or` and `??` are indistinguishable there.
|
|
3654
|
+
if (op === '??' && this.nillablePropNameOf(left) !== null) {
|
|
3655
|
+
return `bf_nullish ${wrapLeft} ${wrapRight}`
|
|
3656
|
+
}
|
|
3389
3657
|
return `or ${wrapLeft} ${wrapRight}`
|
|
3390
3658
|
}
|
|
3391
3659
|
|
|
3660
|
+
/**
|
|
3661
|
+
* The nillable-prop name a `??` left operand refers to, or null. Matches
|
|
3662
|
+
* the two prop-reference shapes (`label` destructured, `props.label`
|
|
3663
|
+
* object-style) against `nullishConsumedPropNames ∩ nillablePropNames`.
|
|
3664
|
+
*
|
|
3665
|
+
* The intersection matters: `nillablePropNames` alone OVERAPPROXIMATES —
|
|
3666
|
+
* it is collected before local type aliases are registered, so an
|
|
3667
|
+
* alias-typed prop (`placement?: TooltipPlacement`) can sit in the set
|
|
3668
|
+
* while its emitted struct field is the concrete alias type. On such a
|
|
3669
|
+
* concrete field "absent" is invisible (the zero value), so the
|
|
3670
|
+
* truthiness-based `or` is the correct approximation and `bf_nullish`
|
|
3671
|
+
* would wrongly KEEP the zero value (e.g. Tooltip's
|
|
3672
|
+
* `placementClasses[props.placement ?? 'top']` would resolve to no
|
|
3673
|
+
* class for an omitted placement). Requiring `nullishConsumedPropNames`
|
|
3674
|
+
* membership pins the
|
|
3675
|
+
* gate to props the `??` analysis actually saw — the same set that drives
|
|
3676
|
+
* the `interface{}` flip in `resolvePropGoType`.
|
|
3677
|
+
*/
|
|
3678
|
+
private nillablePropNameOf(expr: ParsedExpr): string | null {
|
|
3679
|
+
let name: string | null = null
|
|
3680
|
+
if (expr.kind === 'identifier') {
|
|
3681
|
+
name = expr.name
|
|
3682
|
+
} else if (
|
|
3683
|
+
expr.kind === 'member' &&
|
|
3684
|
+
!expr.computed &&
|
|
3685
|
+
expr.object.kind === 'identifier' &&
|
|
3686
|
+
expr.object.name === this.state.propsObjectName
|
|
3687
|
+
) {
|
|
3688
|
+
name = expr.property
|
|
3689
|
+
}
|
|
3690
|
+
return name !== null &&
|
|
3691
|
+
this.state.nullishConsumedPropNames.has(name) &&
|
|
3692
|
+
this.state.nillablePropNames.has(name)
|
|
3693
|
+
? name
|
|
3694
|
+
: null
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3392
3697
|
// JSX-level ternaries (`{expr ? a : b}`) are handled at the IR level as
|
|
3393
3698
|
// IRConditional (via convertConditionToGo → renderConditionExpr). This method
|
|
3394
3699
|
// is only reached for ternaries nested inside other ParsedExpr trees (e.g.
|
|
@@ -4101,10 +4406,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4101
4406
|
|
|
4102
4407
|
/**
|
|
4103
4408
|
* Render a predicate for use in Go template `{{if}}` conditions, substituting
|
|
4104
|
-
* the loop parameter (e.g. `t` in `t.done`) with dot notation.
|
|
4409
|
+
* the loop parameter (e.g. `t` in `t.done`) with dot notation. `datumField`
|
|
4410
|
+
* (#2228) is the wrapper struct's datum-carrying field name (e.g. `"Todo"`)
|
|
4411
|
+
* for a loop whose body is a child component — see `wrapperDatumField` — so
|
|
4412
|
+
* `t.done` qualifies through it (`.Todo.Done`) instead of the bare `.Done`
|
|
4413
|
+
* `html/template` can't resolve on the wrapper Props struct. `undefined` for
|
|
4414
|
+
* a non-wrapper (plain-element-body) loop, where `.` already IS the datum.
|
|
4105
4415
|
*/
|
|
4106
|
-
private renderPredicateCondition(pred: ParsedExpr, param: string): string {
|
|
4107
|
-
return this.renderFilterExpr(pred, param)
|
|
4416
|
+
private renderPredicateCondition(pred: ParsedExpr, param: string, datumField?: string | null): string {
|
|
4417
|
+
return this.renderFilterExpr(pred, param, new Map(), datumField ?? undefined)
|
|
4108
4418
|
}
|
|
4109
4419
|
|
|
4110
4420
|
/** Whether an expression needs parentheses when used in and/or. */
|
|
@@ -4138,12 +4448,19 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4138
4448
|
* Render a filter predicate expression (`t => !t.done`, or a block body
|
|
4139
4449
|
* normalized to one — #2040). `localVarMap` is a vestigial empty default kept
|
|
4140
4450
|
* on the recursion; block-body locals are now inlined upstream, so no caller
|
|
4141
|
-
* populates it.
|
|
4451
|
+
* populates it. `datumField` (#2228) qualifies a bare `param` reference (and
|
|
4452
|
+
* `param.xxx` member/call access) through the wrapper struct's
|
|
4453
|
+
* datum-carrying field — see `wrapperDatumField` — for a loop whose `.` is a
|
|
4454
|
+
* child-component wrapper Props struct rather than the raw datum itself.
|
|
4455
|
+
* `undefined` for every other caller (non-loop `.filter()`/`.find()`/etc.,
|
|
4456
|
+
* or a plain-element-body loop), which keeps emitting the bare `.`/`.Field`
|
|
4457
|
+
* this method always has.
|
|
4142
4458
|
*/
|
|
4143
4459
|
private renderFilterExpr(
|
|
4144
4460
|
expr: ParsedExpr,
|
|
4145
4461
|
param: string,
|
|
4146
|
-
localVarMap: Map<string, string> = new Map()
|
|
4462
|
+
localVarMap: Map<string, string> = new Map(),
|
|
4463
|
+
datumField?: string
|
|
4147
4464
|
): string {
|
|
4148
4465
|
// Top-of-recursion: clear the unsupported sentinel so a previous filter
|
|
4149
4466
|
// expression's failure doesn't poison this one. Parents (`member` /
|
|
@@ -4153,7 +4470,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4153
4470
|
if (this.filterExprDepth === 0) this.filterExprUnsupported = false
|
|
4154
4471
|
this.filterExprDepth++
|
|
4155
4472
|
try {
|
|
4156
|
-
return this.renderFilterExprNode(expr, param, localVarMap)
|
|
4473
|
+
return this.renderFilterExprNode(expr, param, localVarMap, datumField)
|
|
4157
4474
|
} finally {
|
|
4158
4475
|
this.filterExprDepth--
|
|
4159
4476
|
}
|
|
@@ -4162,12 +4479,22 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4162
4479
|
private renderFilterExprNode(
|
|
4163
4480
|
expr: ParsedExpr,
|
|
4164
4481
|
param: string,
|
|
4165
|
-
localVarMap: Map<string, string
|
|
4482
|
+
localVarMap: Map<string, string>,
|
|
4483
|
+
datumField?: string
|
|
4166
4484
|
): string {
|
|
4485
|
+
// #2228: `paramPrefix` prepends the wrapper's datum-carrying field to a
|
|
4486
|
+
// loop-param access (`.Todo` under a wrapper, `''` otherwise). Two derived
|
|
4487
|
+
// forms because Go template spells "the dot itself" as `.` but "field on
|
|
4488
|
+
// the dot" as `.Field` — a naive shared `'.'` prefix would emit `..Done`
|
|
4489
|
+
// for the non-wrapper member case:
|
|
4490
|
+
// bare `t` → `paramDot` (`.Todo` / `.`)
|
|
4491
|
+
// `t.done` → `${paramPrefix}.Done` (`.Todo.Done` / `.Done`)
|
|
4492
|
+
const paramPrefix = datumField ? `.${datumField}` : ''
|
|
4493
|
+
const paramDot = paramPrefix || '.'
|
|
4167
4494
|
switch (expr.kind) {
|
|
4168
4495
|
case 'identifier': {
|
|
4169
4496
|
if (expr.name === param) {
|
|
4170
|
-
return
|
|
4497
|
+
return paramDot
|
|
4171
4498
|
}
|
|
4172
4499
|
// A local variable mapped to a signal.
|
|
4173
4500
|
const signal = localVarMap.get(expr.name)
|
|
@@ -4187,9 +4514,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4187
4514
|
return String(expr.value)
|
|
4188
4515
|
|
|
4189
4516
|
case 'member': {
|
|
4190
|
-
// t.done -> .Done
|
|
4517
|
+
// t.done -> .Done (or .Todo.Done under a wrapper struct, #2228)
|
|
4191
4518
|
if (expr.object.kind === 'identifier' && expr.object.name === param) {
|
|
4192
|
-
return
|
|
4519
|
+
return `${paramPrefix}.${capitalizeFieldName(expr.property)}`
|
|
4193
4520
|
}
|
|
4194
4521
|
// `.length` on a higher-order filter result (e.g.
|
|
4195
4522
|
// `x.tags.filter(t => t.active).length > 0`). Reuse
|
|
@@ -4202,21 +4529,21 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4202
4529
|
const innerHO = this.higherOrderShapeOf(expr.object)
|
|
4203
4530
|
if (innerHO && innerHO.method === 'filter') {
|
|
4204
4531
|
const lenExpr = this.renderFilterLengthExpr(innerHO, e =>
|
|
4205
|
-
this.renderFilterExpr(e, param, localVarMap),
|
|
4532
|
+
this.renderFilterExpr(e, param, localVarMap, datumField),
|
|
4206
4533
|
)
|
|
4207
4534
|
if (lenExpr) return `(${lenExpr})`
|
|
4208
4535
|
}
|
|
4209
4536
|
}
|
|
4210
4537
|
// Nested member access or local var.prop.
|
|
4211
|
-
const obj = this.renderFilterExpr(expr.object, param, localVarMap)
|
|
4538
|
+
const obj = this.renderFilterExpr(expr.object, param, localVarMap, datumField)
|
|
4212
4539
|
if (this.filterExprUnsupported) return 'false'
|
|
4213
4540
|
return `${obj}.${capitalizeFieldName(expr.property)}`
|
|
4214
4541
|
}
|
|
4215
4542
|
|
|
4216
4543
|
case 'call': {
|
|
4217
|
-
// `t.isDone()` -> `.IsDone`
|
|
4544
|
+
// `t.isDone()` -> `.IsDone` (or `.Todo.IsDone` under a wrapper, #2228)
|
|
4218
4545
|
if (expr.callee.kind === 'member' && expr.callee.object.kind === 'identifier' && expr.callee.object.name === param) {
|
|
4219
|
-
return
|
|
4546
|
+
return `${paramPrefix}.${capitalizeFieldName(expr.callee.property)}`
|
|
4220
4547
|
}
|
|
4221
4548
|
// Signal calls: `filter()` -> `$.Filter`
|
|
4222
4549
|
if (expr.callee.kind === 'identifier' && expr.args.length === 0) {
|
|
@@ -4232,13 +4559,13 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4232
4559
|
if (asCallbackMethodCall(expr) !== null) {
|
|
4233
4560
|
return this.refuseFilterExprNode(expr)
|
|
4234
4561
|
}
|
|
4235
|
-
const result = this.renderFilterExpr(expr.callee, param, localVarMap)
|
|
4562
|
+
const result = this.renderFilterExpr(expr.callee, param, localVarMap, datumField)
|
|
4236
4563
|
if (this.filterExprUnsupported) return 'false'
|
|
4237
4564
|
return result
|
|
4238
4565
|
}
|
|
4239
4566
|
|
|
4240
4567
|
case 'unary': {
|
|
4241
|
-
const arg = this.renderFilterExpr(expr.argument, param, localVarMap)
|
|
4568
|
+
const arg = this.renderFilterExpr(expr.argument, param, localVarMap, datumField)
|
|
4242
4569
|
if (this.filterExprUnsupported) return 'false'
|
|
4243
4570
|
if (expr.op === '!') {
|
|
4244
4571
|
// Wrap in parens if arg is a function call (eq, ne, gt, …).
|
|
@@ -4252,9 +4579,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4252
4579
|
}
|
|
4253
4580
|
|
|
4254
4581
|
case 'binary': {
|
|
4255
|
-
const left = this.renderFilterExpr(expr.left, param, localVarMap)
|
|
4582
|
+
const left = this.renderFilterExpr(expr.left, param, localVarMap, datumField)
|
|
4256
4583
|
if (this.filterExprUnsupported) return 'false'
|
|
4257
|
-
const right = this.renderFilterExpr(expr.right, param, localVarMap)
|
|
4584
|
+
const right = this.renderFilterExpr(expr.right, param, localVarMap, datumField)
|
|
4258
4585
|
if (this.filterExprUnsupported) return 'false'
|
|
4259
4586
|
|
|
4260
4587
|
switch (expr.op) {
|
|
@@ -4286,9 +4613,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4286
4613
|
}
|
|
4287
4614
|
|
|
4288
4615
|
case 'logical': {
|
|
4289
|
-
const left = this.renderFilterExpr(expr.left, param, localVarMap)
|
|
4616
|
+
const left = this.renderFilterExpr(expr.left, param, localVarMap, datumField)
|
|
4290
4617
|
if (this.filterExprUnsupported) return 'false'
|
|
4291
|
-
const right = this.renderFilterExpr(expr.right, param, localVarMap)
|
|
4618
|
+
const right = this.renderFilterExpr(expr.right, param, localVarMap, datumField)
|
|
4292
4619
|
if (this.filterExprUnsupported) return 'false'
|
|
4293
4620
|
if (expr.op === '&&') {
|
|
4294
4621
|
return `and (${left}) (${right})`
|
|
@@ -4409,6 +4736,36 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4409
4736
|
return '""'
|
|
4410
4737
|
}
|
|
4411
4738
|
|
|
4739
|
+
// #2224: inside a `renderUnrolledStaticElementLoop` pass, there is no
|
|
4740
|
+
// real `{{range}}` establishing a per-item dot context — every
|
|
4741
|
+
// expression in the body must instead resolve directly against the
|
|
4742
|
+
// active item via `evaluateStaticLiteral` and lower to a literal Go
|
|
4743
|
+
// value. Highest priority (even over the static-record-index / inlined-
|
|
4744
|
+
// const early returns below): those string-keyed checks were never
|
|
4745
|
+
// designed to reason about a loop item and could otherwise misfire on
|
|
4746
|
+
// text that merely happens to match their shape. `analyzeBakeable-
|
|
4747
|
+
// StaticElementLoop` has already verified every expression in this body
|
|
4748
|
+
// resolves for every item, so the `staticLoopBakeFailed` branch is a
|
|
4749
|
+
// defensive invariant, not a real code path — if it ever fires, the two
|
|
4750
|
+
// passes disagreed and the safest move is a sentinel, not a `.Field`
|
|
4751
|
+
// reference with no range context behind it.
|
|
4752
|
+
if (this.staticLoopItemStack.length > 0) {
|
|
4753
|
+
const top = this.staticLoopItemStack[this.staticLoopItemStack.length - 1]
|
|
4754
|
+
const parsedForBake = preParsed ?? parseExpression(trimmed)
|
|
4755
|
+
const resolved = evaluateStaticLiteral(parsedForBake, new Map([[top.param, top.item]]))
|
|
4756
|
+
const literal = resolved !== null ? scalarToGoLiteral(resolved.value) : null
|
|
4757
|
+
if (literal !== null) {
|
|
4758
|
+
// Deliberately leave `out.parsed` unset — a `template-literal`-kind
|
|
4759
|
+
// source (`` `Hi ${item.label}` ``) must NOT be treated as
|
|
4760
|
+
// already-fragment text by `renderExpression`'s `isTemplateFragment`
|
|
4761
|
+
// check (it would skip the `{{...}}` wrap and print the Go literal's
|
|
4762
|
+
// quote characters raw into the HTML).
|
|
4763
|
+
return literal
|
|
4764
|
+
}
|
|
4765
|
+
this.staticLoopBakeFailed = true
|
|
4766
|
+
return '""'
|
|
4767
|
+
}
|
|
4768
|
+
|
|
4412
4769
|
// `IDENT['key']` over a module object-literal const with a STRING-LITERAL key
|
|
4413
4770
|
// is a fully static lookup — resolve it at compile time. The generic member
|
|
4414
4771
|
// lowering below would otherwise capitalize the bracket access into a field
|
|
@@ -4424,7 +4781,15 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4424
4781
|
// the generic lowering would reference a nonexistent `.TotalPages` field).
|
|
4425
4782
|
// Only pure numeric / single-quoted-string initializers qualify; anything
|
|
4426
4783
|
// else may be runtime-dependent.
|
|
4427
|
-
|
|
4784
|
+
//
|
|
4785
|
+
// #2236: this is a string-keyed fast path over `jsExpr` reached directly
|
|
4786
|
+
// by call sites like attribute emission (`key={count}` → `data-key`) that
|
|
4787
|
+
// never go through `identifier()`'s loop-shadow guards below — so it must
|
|
4788
|
+
// carry its OWN guard. When `.map((count) => ...)` shadows the outer
|
|
4789
|
+
// `const count = 7`, the occurrence inside the loop body must resolve to
|
|
4790
|
+
// the range value (via the normal parse-and-lower fallthrough), not the
|
|
4791
|
+
// outer literal.
|
|
4792
|
+
if (!this.isLoopShadowedName(trimmed) && /^[A-Za-z_$][\w$]*$/.test(trimmed)) {
|
|
4428
4793
|
const litConst = (this.state.localConstants ?? []).find(c => c.name === trimmed)
|
|
4429
4794
|
if (litConst?.value !== undefined) {
|
|
4430
4795
|
const v = litConst.value.trim()
|
|
@@ -4495,6 +4860,26 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4495
4860
|
return this.renderParsedExpr(parsed)
|
|
4496
4861
|
}
|
|
4497
4862
|
|
|
4863
|
+
/**
|
|
4864
|
+
* Whether `name` at the CURRENT emission position is bound by an enclosing
|
|
4865
|
+
* loop callback — its item param (`loopParamStack` top), an outer loop's
|
|
4866
|
+
* range variable, a hoisted loop var, or a destructured binding name
|
|
4867
|
+
* (`loopBindingStack`, which is the ONLY place destructured callbacks
|
|
4868
|
+
* record their names; they push `''` onto `loopParamStack`). Shared by the
|
|
4869
|
+
* string-keyed fast paths (#2236, #2242 Copilot review) that resolve raw
|
|
4870
|
+
* `jsExpr` text before `identifier()`'s own guards can see it. Mirrors the
|
|
4871
|
+
* checks in `resolveModuleStringConst` / `resolveModuleNumericConst`.
|
|
4872
|
+
*/
|
|
4873
|
+
private isLoopShadowedName(name: string): boolean {
|
|
4874
|
+
return (
|
|
4875
|
+
(this.loopParamStack.length > 0 &&
|
|
4876
|
+
this.loopParamStack[this.loopParamStack.length - 1] === name) ||
|
|
4877
|
+
this.loopVarRefCount.has(name) ||
|
|
4878
|
+
this.isOuterLoopParam(name) ||
|
|
4879
|
+
this.loopBindingStack.some(bindings => bindings.has(name))
|
|
4880
|
+
)
|
|
4881
|
+
}
|
|
4882
|
+
|
|
4498
4883
|
/**
|
|
4499
4884
|
* Resolve `IDENT['key']` / `IDENT["key"]` where `IDENT` is a module-scope
|
|
4500
4885
|
* object-literal const and the key is a string literal — a compile-time-static
|
|
@@ -4512,6 +4897,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4512
4897
|
// ordinary props/locals never match.
|
|
4513
4898
|
/^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)$/.exec(jsExpr)
|
|
4514
4899
|
if (!m) return null
|
|
4900
|
+
// The base name may be an enclosing loop callback's own (shadowing)
|
|
4901
|
+
// param (`rows.map((cfg) => cfg.x)` under a module `const cfg = {...}`)
|
|
4902
|
+
// — the record-member sibling of the #2236 bare-identifier gap, found
|
|
4903
|
+
// by the loop-param-shadows-record-const fixture. Fall through to the
|
|
4904
|
+
// generic lowering, which resolves the member through the loop binding.
|
|
4905
|
+
if (this.isLoopShadowedName(m[1])) return null
|
|
4515
4906
|
const key = m[2] ?? m[3]
|
|
4516
4907
|
const constInfo = (this.state.localConstants ?? []).find(
|
|
4517
4908
|
c => c.name === m[1] && c.isModule,
|
|
@@ -4974,6 +5365,68 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
4974
5365
|
return undefined
|
|
4975
5366
|
}
|
|
4976
5367
|
|
|
5368
|
+
/**
|
|
5369
|
+
* #2228: the Go field name on the wrapper Props struct that carries the
|
|
5370
|
+
* loop datum, for a loop whose body IS a child component (`loop.childComponent`,
|
|
5371
|
+
* e.g. `.TodoItems` ranging over `TodoItemProps`). `{{range}}`'s dot context
|
|
5372
|
+
* for such a loop is the WHOLE wrapper struct (`TodoItemProps{ Todo Todo,
|
|
5373
|
+
* OnToggle ..., ... }`), not the raw per-item datum — so a filter predicate
|
|
5374
|
+
* (`t => !t.done`) referencing the loop param can't lower `t.done` straight
|
|
5375
|
+
* to `.Done` (no such top-level field; `html/template` fails at execute time
|
|
5376
|
+
* with `can't evaluate field Done in type TodoItemProps`). The datum lives
|
|
5377
|
+
* nested under whichever child prop was PASSED the loop param verbatim
|
|
5378
|
+
* (`todo={todo}` → field `Todo`, from `capitalizeFieldName('todo')` — the
|
|
5379
|
+
* SAME derivation `generateInputStruct`/`generatePropsStruct` use for every
|
|
5380
|
+
* other prop-to-field mapping, so this never invents a field name the
|
|
5381
|
+
* generated struct doesn't actually have). Returns `null` for a non-wrapper
|
|
5382
|
+
* loop, or when no prop's value is a bare reference to the loop param (the
|
|
5383
|
+
* datum isn't forwarded at all — nothing to qualify through).
|
|
5384
|
+
*/
|
|
5385
|
+
private wrapperDatumField(loop: {
|
|
5386
|
+
childComponent?: IRLoopChildComponent
|
|
5387
|
+
param: string
|
|
5388
|
+
}): string | null {
|
|
5389
|
+
if (!loop.childComponent) return null
|
|
5390
|
+
for (const prop of loop.childComponent.props) {
|
|
5391
|
+
if (prop.isEventHandler) continue
|
|
5392
|
+
if (prop.value.kind !== 'expression') continue
|
|
5393
|
+
const parsed = prop.value.parsed
|
|
5394
|
+
const isBareParamRef = parsed
|
|
5395
|
+
? parsed.kind === 'identifier' && parsed.name === loop.param
|
|
5396
|
+
: prop.value.expr.trim() === loop.param
|
|
5397
|
+
if (isBareParamRef) return capitalizeFieldName(prop.name)
|
|
5398
|
+
}
|
|
5399
|
+
return null
|
|
5400
|
+
}
|
|
5401
|
+
|
|
5402
|
+
/**
|
|
5403
|
+
* Memoized bakeability check for a static-array loop whose body is a
|
|
5404
|
+
* single child component (#2208) — see `analyzeBakeableStaticChildLoop`'s
|
|
5405
|
+
* docstring. Accepts either an `IRLoop` (the `renderLoop` gate) or a
|
|
5406
|
+
* `NestedComponentInfo` (`generateNewPropsFunction`/Input-struct sites) —
|
|
5407
|
+
* both carry the same `loopArrayParsed`/`loopParam`/`loopKey`/props data,
|
|
5408
|
+
* just under different field names, so this normalizes to one shape and
|
|
5409
|
+
* caches by marker id so all three call sites agree.
|
|
5410
|
+
*/
|
|
5411
|
+
private getBakedStaticChildLoop(
|
|
5412
|
+
markerId: string,
|
|
5413
|
+
childComponent: { props: IRLoopChildComponent['props'] },
|
|
5414
|
+
arrayParsed: ParsedExpr | undefined,
|
|
5415
|
+
param: string | undefined,
|
|
5416
|
+
key: string | undefined,
|
|
5417
|
+
): BakedStaticChildLoop | null {
|
|
5418
|
+
if (this.bakedStaticChildLoopCache.has(markerId)) {
|
|
5419
|
+
return this.bakedStaticChildLoopCache.get(markerId) ?? null
|
|
5420
|
+
}
|
|
5421
|
+
const result = analyzeBakeableStaticChildLoop(
|
|
5422
|
+
{ props: childComponent.props, loopArrayParsed: arrayParsed, loopParam: param, loopKey: key },
|
|
5423
|
+
this.state.localConstants,
|
|
5424
|
+
{ isNameShadowed: name => this.state.staticLoopSourceBoundNames.has(name) },
|
|
5425
|
+
)
|
|
5426
|
+
this.bakedStaticChildLoopCache.set(markerId, result)
|
|
5427
|
+
return result
|
|
5428
|
+
}
|
|
5429
|
+
|
|
4977
5430
|
renderLoop(loop: IRLoop): string {
|
|
4978
5431
|
// clientOnly loops: emit SSR markers so the client can insert DOM nodes. The
|
|
4979
5432
|
// marker id disambiguates sibling `.map()` calls under the same parent.
|
|
@@ -5036,8 +5489,44 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5036
5489
|
// Phase A/B) no longer refuses `static-array-from-props`'s `([emoji,
|
|
5037
5490
|
// users]) => ...` param first. Cross-adapter policy: Jinja / ERB apply the
|
|
5038
5491
|
// same narrow check in their own `renderLoop` (see `jinja-adapter.ts`).
|
|
5492
|
+
// #2208: a static-array loop whose body is a single child component
|
|
5493
|
+
// (`loop.childComponent`) with a plain-value prop set can be BAKED —
|
|
5494
|
+
// every per-item prop and data-key resolves to a compile-time-known Go
|
|
5495
|
+
// literal (see `analyzeBakeableStaticChildLoop`), so the constructor
|
|
5496
|
+
// (`generateNewPropsFunction`'s `staticWithoutBody` path) can emit the
|
|
5497
|
+
// child instances directly instead of requiring the loop array bind as
|
|
5498
|
+
// a template variable at all. Memoized by marker id so this gate and
|
|
5499
|
+
// the constructor's later baking agree. A plain-ELEMENT body (no
|
|
5500
|
+
// `childComponent`) is NOT handled by baking and keeps refusing below —
|
|
5501
|
+
// see the go-only follow-up issue for that narrower gap.
|
|
5502
|
+
const bakedChildLoop = loop.childComponent
|
|
5503
|
+
? this.getBakedStaticChildLoop(loop.markerId, loop.childComponent, loop.arrayParsed, loop.param, loop.key ?? undefined)
|
|
5504
|
+
: null
|
|
5505
|
+
|
|
5506
|
+
// #2224 shape 1: a static-array loop whose body is a plain ELEMENT tree
|
|
5507
|
+
// (no child component) has no `.{Name}s`-shaped template target for
|
|
5508
|
+
// #2208's baking to feed — there's nothing for `{{range}}` to iterate at
|
|
5509
|
+
// all once the array itself can't bind as a template variable. Rather
|
|
5510
|
+
// than synthesizing a Go struct type for the item shape (see the #2224
|
|
5511
|
+
// issue body), unroll the body once per item at template-generation
|
|
5512
|
+
// time instead, substituting each item's statically-known field values
|
|
5513
|
+
// directly — see `analyzeBakeableStaticElementLoop`'s docstring for the
|
|
5514
|
+
// exact (conservative) acceptance gate. `null` here means the shape
|
|
5515
|
+
// isn't (yet) bakeable this way; the existing gates below keep firing
|
|
5516
|
+
// exactly as before.
|
|
5517
|
+
const bakedElementLoop = loop.childComponent
|
|
5518
|
+
? null
|
|
5519
|
+
: analyzeBakeableStaticElementLoop(
|
|
5520
|
+
loop,
|
|
5521
|
+
this.state.localConstants,
|
|
5522
|
+
{ isNameShadowed: name => this.state.staticLoopSourceBoundNames.has(name) },
|
|
5523
|
+
)
|
|
5524
|
+
if (bakedElementLoop) {
|
|
5525
|
+
return this.renderUnrolledStaticElementLoop(loop, bakedElementLoop.items)
|
|
5526
|
+
}
|
|
5527
|
+
|
|
5039
5528
|
const arrayName = loop.array.trim()
|
|
5040
|
-
if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
5529
|
+
if (bakedChildLoop === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
5041
5530
|
const arrayConst = this.state.localConstants.find(c => c.name === arrayName)
|
|
5042
5531
|
if (arrayConst && !arrayConst.isModule && arrayConst.parsed && !this.isStringExpr(arrayConst.parsed, new Set())) {
|
|
5043
5532
|
this.state.errors.push({
|
|
@@ -5053,7 +5542,22 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5053
5542
|
}
|
|
5054
5543
|
}
|
|
5055
5544
|
|
|
5056
|
-
|
|
5545
|
+
// #2224 shape 2: when the body IS a child component, `goArray` gets
|
|
5546
|
+
// unconditionally overwritten to `.${componentName}s` below regardless
|
|
5547
|
+
// of what this call returns — so for an INLINE array-literal source
|
|
5548
|
+
// (`[{ label: 'Alpha' }, ...].map(item => <ListItem .../>)`), calling
|
|
5549
|
+
// `convertExpressionToGo` on the raw literal text here is pure waste at
|
|
5550
|
+
// best. At worst it's actively harmful: an array-literal-of-objects
|
|
5551
|
+
// fails the shared `isSupported` gate (`object-literal` is refused
|
|
5552
|
+
// standalone — expression-parser.ts), so this call would push a BF101
|
|
5553
|
+
// as a side effect even though `bakedChildLoop` above (via
|
|
5554
|
+
// `resolveStaticLoopSource`, which evaluates the literal directly
|
|
5555
|
+
// instead of going through `isSupported`) already resolved the SAME
|
|
5556
|
+
// loop just fine. Skip the call entirely for a child-component body —
|
|
5557
|
+
// baked or not, dynamic `.map()` over a real prop/signal array included
|
|
5558
|
+
// — so no spurious diagnostic is ever recorded for a value nothing ends
|
|
5559
|
+
// up consuming.
|
|
5560
|
+
let goArray = loop.childComponent ? '' : this.convertExpressionToGo(loop.array)
|
|
5057
5561
|
const param = loop.param
|
|
5058
5562
|
let index = loop.index || '_'
|
|
5059
5563
|
|
|
@@ -5174,9 +5678,19 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5174
5678
|
let filterCond: string
|
|
5175
5679
|
|
|
5176
5680
|
if (loop.filterPredicate.predicate) {
|
|
5681
|
+
// #2228: for a wrapper-slice loop (`.TodoItems` ranging over
|
|
5682
|
+
// TodoItemProps), `.` in the predicate is the WHOLE wrapper struct —
|
|
5683
|
+
// qualify `loop.filterPredicate.param` references through the
|
|
5684
|
+
// datum-carrying field (`.Todo.Done`, not `.Done`) so the emitted
|
|
5685
|
+
// `{{if}}` only ever dereferences fields the wrapper struct actually
|
|
5686
|
+
// has. `wrapperDatumField` returns `null` for a plain-element-body
|
|
5687
|
+
// loop, where `.` already IS the datum — `renderPredicateCondition`
|
|
5688
|
+
// then keeps emitting the bare `.`/`.Field` form unchanged.
|
|
5689
|
+
const datumField = this.wrapperDatumField(loop)
|
|
5177
5690
|
filterCond = this.renderPredicateCondition(
|
|
5178
5691
|
loop.filterPredicate.predicate,
|
|
5179
|
-
loop.filterPredicate.param
|
|
5692
|
+
loop.filterPredicate.param,
|
|
5693
|
+
datumField
|
|
5180
5694
|
)
|
|
5181
5695
|
} else {
|
|
5182
5696
|
filterCond = 'true'
|
|
@@ -5188,6 +5702,61 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5188
5702
|
return `{{bfComment "loop:${loop.markerId}"}}{{range $${rangeIndex}, $${rangeValue} := ${goArray}}}${itemMarker}${children}{{end}}{{bfComment "/loop:${loop.markerId}"}}`
|
|
5189
5703
|
}
|
|
5190
5704
|
|
|
5705
|
+
/**
|
|
5706
|
+
* #2224 shape 1: render a static-array, plain-element-body loop's per-item
|
|
5707
|
+
* markup ONCE PER ITEM at template-generation time instead of a Go
|
|
5708
|
+
* `{{range}}` — `analyzeBakeableStaticElementLoop` has already verified
|
|
5709
|
+
* every expression in `loop.children` resolves against each item. Keeps
|
|
5710
|
+
* the SAME `<!--bf-loop:id--> ... <!--/bf-loop:id-->` marker pair a
|
|
5711
|
+
* dynamic loop emits (so the CSR-side static `forEach` wiring, compiled by
|
|
5712
|
+
* the separate `ir-to-client-js.ts` pass and untouched by this change,
|
|
5713
|
+
* still finds the same DOM range), and pushes `loop.param` /
|
|
5714
|
+
* `loop.depth` onto the SAME stacks `renderLoop`'s `{{range}}` path uses,
|
|
5715
|
+
* so `data-key`/`data-key-N` attribute-name derivation
|
|
5716
|
+
* (`renderAttributes`) is unaffected by which path rendered the loop. No
|
|
5717
|
+
* `itemMarker` (`loopItemMarker`) call: the analysis gate already refused
|
|
5718
|
+
* `bodyIsMultiRoot` / `bodyIsItemConditional` bodies, so it would always
|
|
5719
|
+
* return `''` here anyway.
|
|
5720
|
+
*/
|
|
5721
|
+
private renderUnrolledStaticElementLoop(loop: IRLoop, items: readonly unknown[]): string {
|
|
5722
|
+
this.inLoop = true
|
|
5723
|
+
this.loopWrapperStack.push(false)
|
|
5724
|
+
this.loopKeyDepthStack.push(loop.depth)
|
|
5725
|
+
this.loopScalarItemStack.push(this.scalarLiteralLoopGoType(loop.arrayParsed, loop.itemType) !== null)
|
|
5726
|
+
this.loopParamStack.push(loop.param)
|
|
5727
|
+
|
|
5728
|
+
let body = ''
|
|
5729
|
+
for (const item of items) {
|
|
5730
|
+
this.staticLoopItemStack.push({ param: loop.param, item })
|
|
5731
|
+
body += this.renderChildren(loop.children)
|
|
5732
|
+
this.staticLoopItemStack.pop()
|
|
5733
|
+
if (this.staticLoopBakeFailed) {
|
|
5734
|
+
// Invariant violation (see `staticLoopBakeFailed`'s docstring): the
|
|
5735
|
+
// gate and this render pass disagreed. Surface it loudly instead of
|
|
5736
|
+
// shipping a template with `""` sentinels silently spliced in.
|
|
5737
|
+
this.staticLoopBakeFailed = false
|
|
5738
|
+
this.state.errors.push({
|
|
5739
|
+
code: 'BF101',
|
|
5740
|
+
severity: 'error',
|
|
5741
|
+
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.`,
|
|
5742
|
+
loc: loop.loc ?? this.makeLoc(),
|
|
5743
|
+
suggestion: {
|
|
5744
|
+
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.',
|
|
5745
|
+
},
|
|
5746
|
+
})
|
|
5747
|
+
break
|
|
5748
|
+
}
|
|
5749
|
+
}
|
|
5750
|
+
|
|
5751
|
+
this.loopParamStack.pop()
|
|
5752
|
+
this.loopScalarItemStack.pop()
|
|
5753
|
+
this.loopKeyDepthStack.pop()
|
|
5754
|
+
this.loopWrapperStack.pop()
|
|
5755
|
+
this.inLoop = false
|
|
5756
|
+
|
|
5757
|
+
return `{{bfComment "loop:${loop.markerId}"}}${body}{{bfComment "/loop:${loop.markerId}"}}`
|
|
5758
|
+
}
|
|
5759
|
+
|
|
5191
5760
|
/**
|
|
5192
5761
|
* Per-item `<!--bf-loop-i-->` / `<!--bf-loop-i:KEY-->` start marker emitted
|
|
5193
5762
|
* inside a `{{range}}` body. Multi-root Fragment items get the bare anchor;
|
|
@@ -5574,6 +6143,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
|
|
|
5574
6143
|
// predicate (no BF101 / BF102). This keeps the BF102 remediation ("defer
|
|
5575
6144
|
// it with /* @client */") accurate for attribute-only state.
|
|
5576
6145
|
if (attr.clientOnly) continue
|
|
6146
|
+
// `dangerouslySetInnerHTML` never renders as an HTML attribute — it's
|
|
6147
|
+
// handled by `renderDangerousInnerHtml` instead, which replaces the
|
|
6148
|
+
// element's children. Skip it here so its `{ __html: ... }` object
|
|
6149
|
+
// literal never reaches the generic object-literal BF101 refusal
|
|
6150
|
+
// (which would double-report alongside the purpose-built one).
|
|
6151
|
+
if (isDangerousInnerHtmlAttr(attr)) continue
|
|
5577
6152
|
// Rewrite JSX special-prop names to their HTML-attribute counterparts. The
|
|
5578
6153
|
// Go template adapter has no JSX runtime to strip `key` / emit `data-key`,
|
|
5579
6154
|
// so the rewrite happens at attribute-emit time. Mirror of the `key`
|