@barefootjs/go-template 0.31.0 → 0.31.2

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.
Files changed (35) hide show
  1. package/dist/adapter/go-template-adapter.d.ts +49 -41
  2. package/dist/adapter/go-template-adapter.d.ts.map +1 -1
  3. package/dist/adapter/index.js +167 -101
  4. package/dist/adapter/lib/types.d.ts +4 -1
  5. package/dist/adapter/lib/types.d.ts.map +1 -1
  6. package/dist/adapter/memo/memo-compute.d.ts +9 -0
  7. package/dist/adapter/memo/memo-compute.d.ts.map +1 -1
  8. package/dist/adapter/memo/memo-type.d.ts +2 -0
  9. package/dist/adapter/memo/memo-type.d.ts.map +1 -1
  10. package/dist/adapter/props/prop-types.d.ts +2 -1
  11. package/dist/adapter/props/prop-types.d.ts.map +1 -1
  12. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -1
  13. package/dist/adapter/type/type-codegen.d.ts +22 -5
  14. package/dist/adapter/type/type-codegen.d.ts.map +1 -1
  15. package/dist/adapter/value/parsed-literal-to-go.d.ts +12 -0
  16. package/dist/adapter/value/parsed-literal-to-go.d.ts.map +1 -1
  17. package/dist/adapter/value/value-lowering.d.ts +3 -0
  18. package/dist/adapter/value/value-lowering.d.ts.map +1 -1
  19. package/dist/index.js +168 -104
  20. package/dist/render-divergences.d.ts +6 -0
  21. package/dist/render-divergences.d.ts.map +1 -1
  22. package/dist/vite.js +493 -192
  23. package/package.json +5 -5
  24. package/src/__tests__/go-template-adapter.test.ts +155 -10
  25. package/src/adapter/go-template-adapter.ts +213 -128
  26. package/src/adapter/lib/types.ts +4 -1
  27. package/src/adapter/memo/memo-compute.ts +21 -17
  28. package/src/adapter/memo/memo-type.ts +5 -5
  29. package/src/adapter/props/prop-types.ts +6 -5
  30. package/src/adapter/spread/spread-codegen.ts +12 -5
  31. package/src/adapter/type/type-codegen.ts +86 -25
  32. package/src/adapter/value/parsed-literal-to-go.ts +28 -10
  33. package/src/adapter/value/value-lowering.ts +65 -29
  34. package/src/render-divergences.ts +6 -15
  35. package/src/test-render.ts +6 -1
@@ -134,7 +134,10 @@ export interface SpreadSlotInfo {
134
134
  export interface PropFallbackVar {
135
135
  /** Local variable name (typically the lowercase prop identifier). */
136
136
  varName: string
137
- /** Capitalised Go field name on the `Input` struct. */
137
+ /**
138
+ * Capitalised Go field name on the `Input` struct — caller-facing
139
+ * (`sourceName ?? name`, #2525), since every reader does `in.${fieldName}`.
140
+ */
138
141
  fieldName: string
139
142
  /** Go literal used when the input value equals its zero value. */
140
143
  goFallback: string
@@ -109,7 +109,7 @@ export function matchFilterArmMemo(
109
109
  ctx: GoEmitContext,
110
110
  body: ParsedExpr,
111
111
  signals: { getter: string; initialValue: string; type?: TypeInfo }[],
112
- propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
112
+ propsParams: { name: string; sourceName?: string; type?: TypeInfo; defaultValue?: string }[],
113
113
  ): { propName: string; predJSON: string; paramName: string; freeVars: string[] } | null {
114
114
  const cb = asCallbackMethodCall(body)
115
115
  if (!cb || cb.method !== 'filter') return null
@@ -146,7 +146,7 @@ export function filterArmEarlierSiblingRefs(
146
146
  ctx: GoEmitContext,
147
147
  memo: { name: string; parsed?: ParsedExpr },
148
148
  signals: { getter: string; initialValue: string; type?: TypeInfo }[],
149
- propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
149
+ propsParams: { name: string; sourceName?: string; type?: TypeInfo; defaultValue?: string }[],
150
150
  ): string[] {
151
151
  if (!memo.parsed) return []
152
152
  const match = matchFilterArmMemo(ctx, memo.parsed, signals, propsParams)
@@ -179,7 +179,7 @@ export function computeMemoInitialValue(
179
179
  ctx: GoEmitContext,
180
180
  memo: { name: string; computation: string; deps: string[]; parsed?: ParsedExpr },
181
181
  signals: { getter: string; initialValue: string; type?: TypeInfo }[],
182
- propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
182
+ propsParams: { name: string; sourceName?: string; type?: TypeInfo; defaultValue?: string }[],
183
183
  propFallbackVars: ReadonlyMap<string, PropFallbackVar> = EMPTY_PROP_FALLBACK_VARS,
184
184
  goType?: string,
185
185
  ): string {
@@ -221,7 +221,7 @@ export function memoInitialFromParsedBody(
221
221
  ctx: GoEmitContext,
222
222
  body: ParsedExpr,
223
223
  signals: { getter: string; initialValue: string; type?: TypeInfo }[],
224
- propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
224
+ propsParams: { name: string; sourceName?: string; type?: TypeInfo; defaultValue?: string; parsed?: ParsedExpr }[],
225
225
  propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
226
226
  currentMemoName: string,
227
227
  resolving: ReadonlySet<string> = new Set(),
@@ -229,7 +229,8 @@ export function memoInitialFromParsedBody(
229
229
  const propRef = (propName: string): string => {
230
230
  const hoisted = propFallbackVars.get(propName)
231
231
  if (hoisted) return hoisted.varName
232
- return `in.${capitalizeFieldName(propName)}`
232
+ const param = propsParams.find(p => p.name === propName)
233
+ return `in.${capitalizeFieldName(param?.sourceName ?? propName)}`
233
234
  }
234
235
  // A request-scoped env-signal read (`searchParams().get('k')`, any local
235
236
  // alias, #1922) → the literal key and the reader's canonical field name,
@@ -393,7 +394,9 @@ export function memoInitialFromParsedBody(
393
394
  // being concretely typed, wouldn't compile against `nil` either).
394
395
  if (param && ctx.state.nillablePropNames.has(propName)) {
395
396
  const isNe = body.op === '!==' || body.op === '!='
396
- return `in.${capitalizeFieldName(propName)} ${isNe ? '!=' : '=='} nil`
397
+ // `nillablePropNames` stays keyed by the LOCAL name (a source-level
398
+ // set); the emitted field is caller-facing (#2525).
399
+ return `in.${capitalizeFieldName(param.sourceName ?? propName)} ${isNe ? '!=' : '=='} nil`
397
400
  }
398
401
  }
399
402
  }
@@ -532,7 +535,7 @@ export function memoInitialFromParsedBody(
532
535
  if (hoisted) return `${hoisted.varName} ${operator} ${operand}`
533
536
  const fieldName = capitalizeFieldName(propName)
534
537
  if (param.type) {
535
- const goType = typeInfoToGo(ctx, param.type, param.defaultValue)
538
+ const goType = typeInfoToGo(ctx, param.type, param.defaultValue, param.parsed)
536
539
  if (goType === 'interface{}') return `in.${fieldName}.(int) ${operator} ${operand}`
537
540
  }
538
541
  return `in.${fieldName} ${operator} ${operand}`
@@ -544,9 +547,9 @@ export function memoInitialFromParsedBody(
544
547
  const varName = body.left.name
545
548
  const param = propsParams.find(p => p.name === varName)
546
549
  if (param) {
547
- const fieldName = capitalizeFieldName(varName)
550
+ const fieldName = capitalizeFieldName(param.sourceName ?? varName)
548
551
  if (param.type) {
549
- const goType = typeInfoToGo(ctx, param.type, param.defaultValue)
552
+ const goType = typeInfoToGo(ctx, param.type, param.defaultValue, param.parsed)
550
553
  if (goType === 'interface{}') return `in.${fieldName}.(int) ${operator} ${operand}`
551
554
  }
552
555
  return `in.${fieldName} ${operator} ${operand}`
@@ -574,7 +577,7 @@ export function memoInitialFromParsedBody(
574
577
  // () => var — destructured prop, return the input field directly.
575
578
  if (body.kind === 'identifier') {
576
579
  const param = propsParams.find(p => p.name === body.name)
577
- if (param) return `in.${capitalizeFieldName(body.name)}`
580
+ if (param) return `in.${capitalizeFieldName(param.sourceName ?? body.name)}`
578
581
  }
579
582
 
580
583
  // () => <a> + <b> + … — a string-concatenation chain whose every leaf is a
@@ -607,7 +610,7 @@ function resolveStringConcatChainGo(
607
610
  ctx: GoEmitContext,
608
611
  expr: ParsedExpr,
609
612
  signals: { getter: string; initialValue: string; type?: TypeInfo }[],
610
- propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
613
+ propsParams: { name: string; sourceName?: string; type?: TypeInfo; defaultValue?: string }[],
611
614
  propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
612
615
  propRef: (propName: string) => string,
613
616
  ): string | null {
@@ -647,7 +650,7 @@ export function computeMemoInitialValueOrNull(
647
650
  ctx: GoEmitContext,
648
651
  memo: { name: string; computation: string; deps: string[]; parsed?: ParsedExpr; parsedBlock?: ParsedStatement[]; parsedBlockComplete?: boolean },
649
652
  signals: { getter: string; initialValue: string; type?: TypeInfo }[],
650
- propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
653
+ propsParams: { name: string; sourceName?: string; type?: TypeInfo; defaultValue?: string }[],
651
654
  propFallbackVars: ReadonlyMap<string, PropFallbackVar> = EMPTY_PROP_FALLBACK_VARS,
652
655
  /**
653
656
  * Memo names currently being resolved on this call stack — guards against
@@ -729,7 +732,7 @@ export function resolveGetterValueAsGo(
729
732
  ctx: GoEmitContext,
730
733
  name: string,
731
734
  signals: { getter: string; initialValue: string; type?: TypeInfo }[],
732
- propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
735
+ propsParams: { name: string; sourceName?: string; type?: TypeInfo; defaultValue?: string }[],
733
736
  propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
734
737
  resolving: ReadonlySet<string> = new Set(),
735
738
  ): string | null {
@@ -745,7 +748,8 @@ export function resolveGetterValueAsGo(
745
748
  const stripped = memo.computation.replace(/^\(\)\s*=>\s*/, '')
746
749
  const fb = ctx.extractPropFallback(stripped)
747
750
  if (fb && capitalizeFieldName(fb.propName) === capitalizeFieldName(memo.name)) {
748
- const field = `in.${capitalizeFieldName(fb.propName)}`
751
+ const fbParam = propsParams.find(p => p.name === fb.propName)
752
+ const field = `in.${capitalizeFieldName(fbParam?.sourceName ?? fb.propName)}`
749
753
  return `func() interface{} { v := interface{}(${field}); if v == nil || v == "" { return ${fb.goFallback} }; return v }()`
750
754
  }
751
755
  return computeMemoInitialValueOrNull(
@@ -755,7 +759,7 @@ export function resolveGetterValueAsGo(
755
759
  const param = propsParams.find(p => p.name === name)
756
760
  if (param) {
757
761
  const hoisted = propFallbackVars.get(name)
758
- return hoisted ? hoisted.varName : `in.${capitalizeFieldName(name)}`
762
+ return hoisted ? hoisted.varName : `in.${capitalizeFieldName(param.sourceName ?? name)}`
759
763
  }
760
764
  return null
761
765
  }
@@ -772,7 +776,7 @@ export function computeComparisonTernaryGo(
772
776
  ctx: GoEmitContext,
773
777
  parsed: ParsedExpr | undefined,
774
778
  signals: { getter: string; initialValue: string; type?: TypeInfo }[],
775
- propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
779
+ propsParams: { name: string; sourceName?: string; type?: TypeInfo; defaultValue?: string }[],
776
780
  propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
777
781
  resolving: ReadonlySet<string> = new Set(),
778
782
  ): string | null {
@@ -828,7 +832,7 @@ export function resolveComparisonOperandGo(
828
832
  ctx: GoEmitContext,
829
833
  node: ParsedExpr,
830
834
  signals: { getter: string; initialValue: string; type?: TypeInfo }[],
831
- propsParams: { name: string; type?: TypeInfo; defaultValue?: string }[],
835
+ propsParams: { name: string; sourceName?: string; type?: TypeInfo; defaultValue?: string }[],
832
836
  propFallbackVars: ReadonlyMap<string, PropFallbackVar>,
833
837
  resolving: ReadonlySet<string> = new Set(),
834
838
  ): string | null {
@@ -34,8 +34,8 @@ export function isListFilterMemo(memo: { parsed?: ParsedExpr }): boolean {
34
34
  export function isBooleanMemo(
35
35
  ctx: GoEmitContext,
36
36
  memo: { computation: string; deps: string[]; parsed?: ParsedExpr },
37
- signals: { getter: string; initialValue: string; type: TypeInfo }[],
38
- propsParamMap: Map<string, { name: string; type: TypeInfo; defaultValue?: string }>,
37
+ signals: { getter: string; initialValue: string; type: TypeInfo; parsed?: ParsedExpr }[],
38
+ propsParamMap: Map<string, { name: string; type: TypeInfo; defaultValue?: string; parsed?: ParsedExpr }>,
39
39
  ): boolean {
40
40
  const c = memo.computation
41
41
  // A LIST-valued `.filter(arrow)` memo (#2075) is never boolean, even though
@@ -58,13 +58,13 @@ export function isBooleanMemo(
58
58
  if (typeInfoToGo(ctx, sig.type) === 'bool') return true
59
59
  // Signal initialised from `props.X ?? false` / a boolean prop.
60
60
  if (/\?\?\s*(true|false)\b/.test(sig.initialValue)) return true
61
- const propName = ctx.extractPropNameFromInitialValue(sig.initialValue) ?? sig.initialValue
61
+ const propName = ctx.extractPropNameFromInitialValue(sig.initialValue, sig.parsed) ?? sig.initialValue
62
62
  const prop = propsParamMap.get(propName)
63
- if (prop && typeInfoToGo(ctx, prop.type, prop.defaultValue) === 'bool') return true
63
+ if (prop && typeInfoToGo(ctx, prop.type, prop.defaultValue, prop.parsed) === 'bool') return true
64
64
  return false
65
65
  }
66
66
  const prop = propsParamMap.get(name)
67
- return !!prop && typeInfoToGo(ctx, prop.type, prop.defaultValue) === 'bool'
67
+ return !!prop && typeInfoToGo(ctx, prop.type, prop.defaultValue, prop.parsed) === 'bool'
68
68
  }
69
69
  const ternary = c.match(/=>\s*\w+\(\)\s*\?\s*(\w+)\(\)\s*:\s*(\w+)\(\)/)
70
70
  if (ternary) {
@@ -27,9 +27,9 @@ export function buildPropTypeOverrides(ctx: GoEmitContext, ir: ComponentIR): Map
27
27
  for (const propName of propNames) {
28
28
  const param = ir.metadata.propsParams.find(p => p.name === propName)
29
29
  if (!param) continue
30
- const propGoType = typeInfoToGo(ctx, param.type, param.defaultValue)
30
+ const propGoType = typeInfoToGo(ctx, param.type, param.defaultValue, param.parsed)
31
31
  if (propGoType.includes('interface{}')) {
32
- const signalGoType = typeInfoToGo(ctx, signal.type, signal.initialValue)
32
+ const signalGoType = typeInfoToGo(ctx, signal.type, signal.initialValue, signal.parsed)
33
33
  if (!signalGoType.includes('interface{}')) {
34
34
  overrides.set(propName, signalGoType)
35
35
  }
@@ -50,7 +50,7 @@ export function buildPropTypeOverrides(ctx: GoEmitContext, ir: ComponentIR): Map
50
50
  for (const propName of collectToFixedPropNames(ir.root)) {
51
51
  const param = ir.metadata.propsParams.find(p => p.name === propName)
52
52
  if (!param) continue
53
- const resolved = overrides.get(propName) ?? typeInfoToGo(ctx, param.type, param.defaultValue)
53
+ const resolved = overrides.get(propName) ?? typeInfoToGo(ctx, param.type, param.defaultValue, param.parsed)
54
54
  if (resolved === 'int') {
55
55
  overrides.set(propName, 'float64')
56
56
  }
@@ -381,7 +381,8 @@ export function collectPresenceCheckedPropNames(ctx: GoEmitContext, ir: Componen
381
381
  /**
382
382
  * Resolve a prop param's Go struct-field type using the SAME logic
383
383
  * `generatePropsStruct` / `generateInputStruct` use: a `propTypeOverrides` entry
384
- * wins, otherwise `typeInfoToGo(param.type, param.defaultValue)`. Factored out so
384
+ * wins, otherwise `typeInfoToGo(param.type, param.defaultValue, param.parsed)`.
385
+ * Factored out so
385
386
  * the nillable-field set (`collectNillablePropNames`) can't drift from the
386
387
  * emitted field types.
387
388
  */
@@ -390,7 +391,7 @@ export function resolvePropGoType(
390
391
  param: IRMetadata['propsParams'][number],
391
392
  propTypeOverrides: Map<string, string>,
392
393
  ): string {
393
- const base = propTypeOverrides.get(param.name) ?? typeInfoToGo(ctx, param.type, param.defaultValue)
394
+ const base = propTypeOverrides.get(param.name) ?? typeInfoToGo(ctx, param.type, param.defaultValue, param.parsed)
394
395
  // An OPTIONAL prop typed as a named struct (`opts?: EmblaOptionsType`) lowers
395
396
  // to `map[string]interface{}`, not the value struct: a value struct is always
396
397
  // truthy in Go templates (so a `{{if .Opts}}`-guarded attribute could never be
@@ -225,7 +225,7 @@ export function buildSpreadInitializer(
225
225
  // `{...extras}` resolves to `in.Extras`.
226
226
  const param = ir.metadata.propsParams.find(p => p.name === trimmed)
227
227
  if (param) {
228
- return `in.${capitalizeFieldName(param.name)}`
228
+ return `in.${capitalizeFieldName(param.sourceName ?? param.name)}`
229
229
  }
230
230
  // 2. SolidJS-style props object: `function(props: P)` → spread
231
231
  // `{...props}` enumerates all propsParams into a `map[string]any`
@@ -235,8 +235,11 @@ export function buildSpreadInitializer(
235
235
  // `applyRestAttrs` hydrate path still applies them — worse than full
236
236
  // enumeration, better than BF101 blocking the build.
237
237
  if (ir.metadata.propsObjectName === trimmed) {
238
+ // Bag key stays the caller-facing prop name (mirrors the spread bag's
239
+ // external shape); Input field read is caller-facing too (#2525) —
240
+ // identity here since the `props`-object pattern has no aliasing.
238
241
  const entries = ir.metadata.propsParams.map(p =>
239
- `${JSON.stringify(p.name)}: in.${capitalizeFieldName(p.name)}`,
242
+ `${JSON.stringify(p.sourceName ?? p.name)}: in.${capitalizeFieldName(p.sourceName ?? p.name)}`,
240
243
  )
241
244
  return `map[string]any{${entries.join(', ')}}`
242
245
  }
@@ -344,7 +347,7 @@ function conditionToGoBool(
344
347
  if (node.kind !== 'identifier') return null
345
348
  const param = ir.metadata.propsParams.find(p => p.name === node.name)
346
349
  if (!param) return null
347
- const field = `in.${capitalizeFieldName(param.name)}`
350
+ const field = `in.${capitalizeFieldName(param.sourceName ?? param.name)}`
348
351
  const prim = param.type.kind === 'primitive' ? param.type.primitive : undefined
349
352
  let truthy: string
350
353
  if (prim === 'boolean') {
@@ -396,7 +399,7 @@ function objectLiteralToGoSpreadMap(
396
399
  } else if (val.kind === 'identifier') {
397
400
  const param = ir.metadata.propsParams.find(p => p.name === val.name)
398
401
  if (!param) return null
399
- goVal = `in.${capitalizeFieldName(param.name)}`
402
+ goVal = `in.${capitalizeFieldName(param.sourceName ?? param.name)}`
400
403
  } else {
401
404
  const indexed = recordIndexAccessToGoMap(ctx, val, ir)
402
405
  if (indexed === null) return null
@@ -453,6 +456,10 @@ function recordIndexAccessToGoMap(
453
456
  return `${JSON.stringify(e.key)}: ${mapVal}`
454
457
  })
455
458
  ctx.state.usesFmt = true
456
- const field = `in.${capitalizeFieldName(parsed.indexPropName)}`
459
+ // `parsed.indexPropName` is the LOCAL binding (`parseRecordIndexAccess` is
460
+ // shared with other adapters and stays name-agnostic); resolve the Input
461
+ // field caller-facing here (#2525) rather than in the shared parser.
462
+ const indexParam = ir.metadata.propsParams.find(p => p.name === parsed.indexPropName)
463
+ const field = `in.${capitalizeFieldName(indexParam?.sourceName ?? parsed.indexPropName)}`
457
464
  return `map[string]any{${entries.join(', ')}}[fmt.Sprint(${field})]`
458
465
  }
@@ -11,7 +11,7 @@
11
11
  * pure.
12
12
  */
13
13
 
14
- import type { TypeInfo } from '@barefootjs/jsx'
14
+ import type { ParsedExpr, TypeInfo } from '@barefootjs/jsx'
15
15
 
16
16
  import type { GoEmitContext } from '../emit-context.ts'
17
17
 
@@ -57,6 +57,48 @@ export function collapseLiteralUnion(typeInfo: TypeInfo): TypeInfo {
57
57
  return { kind: 'primitive', raw: typeInfo.raw, primitive: first }
58
58
  }
59
59
 
60
+ /**
61
+ * A literal number's numeric value, unwrapping a leading unary minus (`-7.6`
62
+ * parses as `{kind:'unary', op:'-', argument:{literalType:'number', value:7.6}}`
63
+ * — the same shape `parsedLiteralToGo`'s own unary-minus arm unwraps,
64
+ * `value-lowering.ts`/`parsed-literal-to-go.ts`). Returns `null` for anything
65
+ * that isn't (possibly negated) a number literal.
66
+ */
67
+ function literalNumberValue(expr: ParsedExpr): number | null {
68
+ if (expr.kind === 'literal' && expr.literalType === 'number' && typeof expr.value === 'number') {
69
+ return expr.value
70
+ }
71
+ if (
72
+ expr.kind === 'unary' &&
73
+ expr.op === '-' &&
74
+ expr.argument.kind === 'literal' &&
75
+ expr.argument.literalType === 'number' &&
76
+ typeof expr.argument.value === 'number'
77
+ ) {
78
+ return -expr.argument.value
79
+ }
80
+ return null
81
+ }
82
+
83
+ /**
84
+ * Structural counterpart to {@link inferTypeFromValue}: classify a parsed
85
+ * literal's Go type from its `ParsedExpr` shape instead of its source text.
86
+ * Returns `null` for anything not covered here (identifier/call/member,
87
+ * object-literal — same scope `inferTypeFromValue`'s text scan covers, so a
88
+ * caller falling back to the text path for those sees identical results).
89
+ */
90
+ function inferGoTypeFromParsed(expr: ParsedExpr): string | null {
91
+ const n = literalNumberValue(expr)
92
+ if (n !== null) return Number.isInteger(n) ? 'int' : 'float64'
93
+ if (expr.kind === 'literal') {
94
+ if (expr.literalType === 'boolean') return 'bool'
95
+ if (expr.literalType === 'string') return 'string'
96
+ return null
97
+ }
98
+ if (expr.kind === 'array-literal') return '[]interface{}'
99
+ return null
100
+ }
101
+
60
102
  /**
61
103
  * Convert a `TypeInfo` to a Go type string.
62
104
  *
@@ -65,12 +107,18 @@ export function collapseLiteralUnion(typeInfo: TypeInfo): TypeInfo {
65
107
  * `primitive`/`number` (#2168 math-methods/number-tofixed — a bare TS
66
108
  * `number` blindly mapped to Go `int`, so a fractional signal initial
67
109
  * value like `-7.6` silently truncated to the Go zero value)
110
+ * @param preParsed the SAME default/initial value as `defaultValue`, already
111
+ * parsed to structure (`SignalInfo.parsed` / `ParamInfo.parsed`) — preferred
112
+ * over regexing `defaultValue`'s text when present. Callers with no
113
+ * structural counterpart (a shape `tsNodeToParsedExpr` doesn't support) pass
114
+ * nothing and fall through to the text path.
68
115
  * @returns the Go type, falling back to `interface{}` when unresolvable
69
116
  */
70
117
  export function typeInfoToGo(
71
118
  ctx: GoEmitContext,
72
119
  _typeInfo: TypeInfo,
73
120
  defaultValue?: string,
121
+ preParsed?: ParsedExpr,
74
122
  ): string {
75
123
  const typeInfo = collapseLiteralUnion(_typeInfo)
76
124
  switch (typeInfo.kind) {
@@ -78,8 +126,11 @@ export function typeInfoToGo(
78
126
  switch (typeInfo.primitive) {
79
127
  case 'string':
80
128
  return 'string'
81
- case 'number':
129
+ case 'number': {
130
+ const n = preParsed ? literalNumberValue(preParsed) : null
131
+ if (n !== null) return Number.isInteger(n) ? 'int' : 'float64'
82
132
  return defaultValue !== undefined ? numberPrimitiveGoType(defaultValue) : 'int'
133
+ }
83
134
  case 'boolean':
84
135
  return 'bool'
85
136
  default:
@@ -108,58 +159,68 @@ export function typeInfoToGo(
108
159
  if (typeInfo.raw && (ctx.state.localStructFields.has(typeInfo.raw) || ctx.state.localTypeAliases.has(typeInfo.raw))) {
109
160
  return typeInfo.raw
110
161
  }
111
- // Resolve a raw type string pattern (e.g. `Array<Todo>`).
162
+ // A named type with no backing struct/alias — an external/unresolved
163
+ // reference. `typeNodeToTypeInfo` already normalises every ARRAY spelling
164
+ // (`T[]`, `Array<T>`, `ReadonlyArray<T>`) to `kind: 'array'` before a
165
+ // `TypeInfo` ever reaches here (#2480's structural literal-type pass did
166
+ // the same for literal unions), so `typeInfo.raw` at this point is never
167
+ // an array shape — `tsTypeStringToGo` is a plain lookup, not a parser.
112
168
  if (typeInfo.raw) {
113
169
  const resolved = tsTypeStringToGo(ctx, typeInfo.raw)
114
170
  if (resolved !== 'interface{}') return resolved
115
171
  }
116
172
  return 'interface{}'
117
- case 'unknown':
173
+ case 'unknown': {
174
+ const inferred = preParsed ? inferGoTypeFromParsed(preParsed) : null
175
+ if (inferred) return inferred
118
176
  if (defaultValue !== undefined) {
119
177
  return inferTypeFromValue(defaultValue)
120
178
  }
121
179
  return 'interface{}'
180
+ }
122
181
  default:
123
182
  return 'interface{}'
124
183
  }
125
184
  }
126
185
 
127
186
  /**
128
- * Convert a raw TypeScript type string to a Go type string. Handles primitives,
129
- * `T[]` / `Array<T>` arrays, and known local types; else `interface{}`.
187
+ * Look up a raw TypeScript type-reference name against the component's own
188
+ * Go-backed local types. NOT a parser: by the time a `TypeInfo` reaches here
189
+ * (`typeInfoToGo`'s `'interface'` case, its one caller) `typeNodeToTypeInfo`
190
+ * has already normalised every array spelling to `kind: 'array'` and every
191
+ * primitive keyword to `kind: 'primitive'` — `tsType` is always a bare named
192
+ * reference (a struct, a string-union alias, or an unbacked/external name),
193
+ * never `T[]` / `Array<T>` / `'number'` text to re-parse. (#2484: this used to
194
+ * carry `t.endsWith('[]')` / `Array<(.+)>` regex branches as a fallback for
195
+ * that dead case — unreachable given the analyzer's normalisation, deleted.)
130
196
  */
131
197
  export function tsTypeStringToGo(ctx: GoEmitContext, tsType: string): string {
132
198
  const t = tsType.trim()
133
- if (t === 'number') return 'int'
134
- if (t === 'string') return 'string'
135
- if (t === 'boolean' || t === 'bool') return 'bool'
136
- if (t.endsWith('[]')) {
137
- const elem = t.slice(0, -2)
138
- return `[]${tsTypeStringToGo(ctx, elem)}`
139
- }
140
- const arrayMatch = t.match(/^Array<(.+)>$/)
141
- if (arrayMatch) return `[]${tsTypeStringToGo(ctx, arrayMatch[1])}`
142
- // Same backing gate as `typeInfoToGo`'s 'interface' case above — an
143
- // unbacked local type name (a tuple alias with no struct fields) must not
144
- // be returned bare, or the generated code references an undeclared type.
145
199
  if (ctx.state.localStructFields.has(t) || ctx.state.localTypeAliases.has(t)) return t
146
200
  return 'interface{}'
147
201
  }
148
202
 
149
203
  /**
150
204
  * Distinguish Go `int` vs `float64` for a `number`-typed field from the
151
- * literal source text of its default/initial value. Falls back to `int`
152
- * when `value` isn't recognizably a bare numeric literal (e.g. a
153
- * destructured default that's itself an expression, `props.initial ?? 0`)
154
- * `int` remains the blind fallback for `kind: 'primitive'`; only a
155
- * literal fractional value (`-7.6`) is positive enough evidence to widen
156
- * to `float64`.
205
+ * literal source text of its default/initial value. TEXT FALLBACK: used only
206
+ * when `typeInfoToGo`'s caller has no structural `ParsedExpr` for the same
207
+ * value to pass as `preParsed` (see `inferGoTypeFromParsed`/`literalNumberValue`
208
+ * above, which this mirrors on parsed structure). Falls back to `int` when
209
+ * `value` isn't recognizably a bare numeric literal (e.g. a destructured
210
+ * default that's itself an expression, `props.initial ?? 0`) — `int` remains
211
+ * the blind fallback for `kind: 'primitive'`; only a literal fractional value
212
+ * (`-7.6`) is positive enough evidence to widen to `float64`.
157
213
  */
158
214
  function numberPrimitiveGoType(value: string): string {
159
215
  return /^-?\d+\.\d+$/.test(value) ? 'float64' : 'int'
160
216
  }
161
217
 
162
- /** Infer a Go type from a JS value literal; `interface{}` when unrecognized. */
218
+ /**
219
+ * Infer a Go type from a JS value literal's source TEXT; `interface{}` when
220
+ * unrecognized. TEXT FALLBACK: mirrors `inferGoTypeFromParsed` above on raw
221
+ * text, used only when `typeInfoToGo`'s caller has no structural `ParsedExpr`
222
+ * for the same value to pass as `preParsed`.
223
+ */
163
224
  export function inferTypeFromValue(value: string): string {
164
225
  if (value === 'true' || value === 'false') return 'bool'
165
226
  if (/^-?\d+$/.test(value)) return 'int'
@@ -74,13 +74,18 @@ function bakeInlineObjectAsGoMap(ctx: GoEmitContext, expr: ParsedExpr): string |
74
74
  return `map[string]interface{}{${entries.join(', ')}}`
75
75
  }
76
76
 
77
- export function parsedLiteralToGo(
78
- ctx: GoEmitContext,
79
- expr: ParsedExpr,
80
- typeInfo?: TypeInfo,
81
- ): string | null {
82
- // Leading unary minus on a numeric literal (`-1`), from the carried `raw`
83
- // token.
77
+ /**
78
+ * A literal number's exact Go source text, unwrapping a leading unary minus
79
+ * (`-1` parses as `{kind:'unary', op:'-', argument:{literalType:'number'}}`).
80
+ * Needs the exact source token — re-stringifying the parsed numeric VALUE
81
+ * could change spelling / lose precision — so this defers (`null`) rather
82
+ * than reconstructing one, same as every other non-literal shape here.
83
+ *
84
+ * Shared by `parsedLiteralToGo`'s own number/unary-minus cases below and
85
+ * `convertInitialValue`'s primitive-number branch (`value-lowering.ts`) —
86
+ * the SAME literal needs the SAME exact-token handling wherever it's baked.
87
+ */
88
+ export function numberLiteralRawGo(expr: ParsedExpr): string | null {
84
89
  if (
85
90
  expr.kind === 'unary' &&
86
91
  expr.op === '-' &&
@@ -89,6 +94,19 @@ export function parsedLiteralToGo(
89
94
  ) {
90
95
  return expr.argument.raw !== undefined ? `-${expr.argument.raw}` : null
91
96
  }
97
+ if (expr.kind === 'literal' && expr.literalType === 'number') {
98
+ return expr.raw ?? null
99
+ }
100
+ return null
101
+ }
102
+
103
+ export function parsedLiteralToGo(
104
+ ctx: GoEmitContext,
105
+ expr: ParsedExpr,
106
+ typeInfo?: TypeInfo,
107
+ ): string | null {
108
+ const numberGo = numberLiteralRawGo(expr)
109
+ if (numberGo !== null) return numberGo
92
110
 
93
111
  if (expr.kind === 'literal') {
94
112
  switch (expr.literalType) {
@@ -96,9 +114,9 @@ export function parsedLiteralToGo(
96
114
  // `value` is the unquoted text; `JSON.stringify` re-quotes it for Go.
97
115
  return JSON.stringify(expr.value)
98
116
  case 'number':
99
- // Need the exact source token; without it the value could change
100
- // spelling / lose precision, so defer.
101
- return expr.raw ?? null
117
+ // A number literal with no `raw` token (`numberLiteralRawGo` already
118
+ // returned null above) defer.
119
+ return null
102
120
  case 'boolean':
103
121
  return expr.value ? 'true' : 'false'
104
122
  case 'null':