@barefootjs/jsx 0.19.0 → 0.20.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.
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Rich-type method-call refusal (#2273).
3
+ *
4
+ * A method call on a prop typed as a built-in "host rich type" (`Date`,
5
+ * `Map`, …) has no catalogued lowering — no adapter can emit `.toISOString()`
6
+ * into a template. Left unchecked, such a call transliterates into the
7
+ * target template's own dot-call syntax and dies at request time (a Go
8
+ * template `.CreatedAt.ToISOString` panic, a Jinja `AttributeError`, …),
9
+ * once per adapter, only once someone renders the page. This module makes
10
+ * that gap loud at compile time instead: `checkRichTypeMethodCalls` walks
11
+ * every expression position the compiler already treats as template-lowered
12
+ * and pushes BF021 for any call this build has no evidence a lowering plugin
13
+ * (or `/* @client *\/`) will handle.
14
+ *
15
+ * Deliberately conservative in both directions:
16
+ * - `resolveReceiverType` (rich-type-evidence.ts) returns `null` — no
17
+ * opinion — for any receiver shape it can't prove a type for, so an
18
+ * untyped / generic / call-result receiver is silently allowed through
19
+ * rather than misdiagnosed.
20
+ * - A `/* @client *\/` node is skipped entirely (its expression already
21
+ * opts out of SSR lowering), and a call a registered lowering plugin
22
+ * claims (`prepareLoweringMatchers`) is exempt — the seam #2274 and
23
+ * later plugins use to catalogue a rich-type API without touching this
24
+ * module.
25
+ */
26
+
27
+ import type {
28
+ IRNode,
29
+ IRMetadata,
30
+ CompilerError,
31
+ SourceLocation,
32
+ TypeInfo,
33
+ AttrValue,
34
+ IRTemplatePart,
35
+ } from './types.ts'
36
+ import type { ParsedExpr } from './expression-parser.ts'
37
+ import { parseExpression } from './expression-parser.ts'
38
+ import { prepareLoweringMatchers, type LoweringMatcher } from './lowering-registry.ts'
39
+ import { ErrorCodes } from './errors.ts'
40
+ import { resolveReceiverType, baseTypeName, HOST_RICH_TYPE_NAMES } from './rich-type-evidence.ts'
41
+
42
+ type Bindings = ReadonlyMap<string, TypeInfo | null>
43
+ const EMPTY_BINDINGS: Bindings = new Map()
44
+
45
+ /**
46
+ * Walk `root` looking for a method call on a host rich-typed receiver with
47
+ * no catalogued lowering, pushing BF021 into `errors` for each one found.
48
+ * Mirrors `attachParsedExpressions`' tree coverage (jsx-to-ir.ts) so every
49
+ * position that reaches a template gets checked, but reads `.parsed` rather
50
+ * than attaching it, and additionally skips anything under `/* @client *\/`.
51
+ */
52
+ export function checkRichTypeMethodCalls(root: IRNode, metadata: IRMetadata, errors: CompilerError[]): void {
53
+ // Every evidence chain roots at propsType (bare prop, props.x, loop item of
54
+ // a prop array) — with no props type there is nothing to prove, so skip the
55
+ // walk (and the matcher preparation / on-demand template-part parses).
56
+ if (!metadata.propsType) return
57
+ const matchers = prepareLoweringMatchers(metadata)
58
+ const seen = new Set<string>()
59
+ walkNode(root, metadata, EMPTY_BINDINGS, matchers, errors, seen)
60
+ }
61
+
62
+ function isLoweringClaimed(matchers: readonly LoweringMatcher[], callee: ParsedExpr, args: readonly ParsedExpr[]): boolean {
63
+ return matchers.some((m) => m(callee, args) !== null)
64
+ }
65
+
66
+ /** Best-effort dotted-path text for the diagnostic message's receiver description. */
67
+ function describeReceiverPath(expr: ParsedExpr): string {
68
+ if (expr.kind === 'identifier') return expr.name
69
+ if (expr.kind === 'member' && !expr.computed) return `${describeReceiverPath(expr.object)}.${expr.property}`
70
+ return '<expression>'
71
+ }
72
+
73
+ /**
74
+ * Whether the receiver path roots at a prop (bare destructured prop or a
75
+ * `props.x` chain) rather than a locally-bound name (loop item, arrow param).
76
+ * Decides whether the diagnostic may call the receiver a "prop" — a loop
77
+ * item's `i.at` is prop-DERIVED but not itself a prop, and naming it one
78
+ * would misdirect the fix toward the props type.
79
+ */
80
+ function receiverRootIsProp(expr: ParsedExpr, bindings: Bindings): boolean {
81
+ let root = expr
82
+ while (root.kind === 'member' && !root.computed) root = root.object
83
+ return root.kind === 'identifier' && !bindings.has(root.name)
84
+ }
85
+
86
+ function pushDiagnostic(
87
+ errors: CompilerError[],
88
+ seen: Set<string>,
89
+ loc: SourceLocation,
90
+ method: string,
91
+ receiverPath: string,
92
+ isProp: boolean,
93
+ typeName: string,
94
+ ): void {
95
+ const key = `${loc.start.line}:${loc.start.column}:${receiverPath}.${method}`
96
+ if (seen.has(key)) return
97
+ seen.add(key)
98
+ const receiver = isProp ? `prop '${receiverPath}'` : `'${receiverPath}'`
99
+ errors.push({
100
+ code: ErrorCodes.UNSUPPORTED_JSX_PATTERN,
101
+ severity: 'error',
102
+ message: `Expression cannot be compiled to marked template: method '.${method}()' on ${receiver} of host type '${typeName}' has no catalogued lowering.`,
103
+ loc,
104
+ suggestion: {
105
+ message: 'Add /* @client */ to evaluate this expression on the client only, or pre-compute the value server-side.',
106
+ },
107
+ })
108
+ }
109
+
110
+ /**
111
+ * Recurse a parsed expression tree, checking every `call` node along the way
112
+ * and descending into every sub-expression (so a rich-type call nested
113
+ * inside a larger expression — a template literal interpolation, an object
114
+ * literal value, an arrow body, …) is still found. `bindings` carries local
115
+ * type evidence (loop item / arrow param shadows) down into the recursion.
116
+ */
117
+ function checkExpr(
118
+ expr: ParsedExpr,
119
+ loc: SourceLocation,
120
+ meta: IRMetadata,
121
+ bindings: Bindings,
122
+ matchers: readonly LoweringMatcher[],
123
+ errors: CompilerError[],
124
+ seen: Set<string>,
125
+ ): void {
126
+ const recurse = (e: ParsedExpr, b: Bindings = bindings) => checkExpr(e, loc, meta, b, matchers, errors, seen)
127
+
128
+ switch (expr.kind) {
129
+ case 'call': {
130
+ if (expr.callee.kind === 'member') {
131
+ const receiverType = resolveReceiverType(expr.callee.object, meta, bindings)
132
+ if (receiverType && receiverType.kind === 'interface') {
133
+ const typeName = baseTypeName(receiverType.raw)
134
+ const inFileShadow = meta.typeDefinitions.some((d) => d.name === typeName)
135
+ if (HOST_RICH_TYPE_NAMES.has(typeName) && !inFileShadow && !isLoweringClaimed(matchers, expr.callee, expr.args)) {
136
+ pushDiagnostic(
137
+ errors,
138
+ seen,
139
+ loc,
140
+ expr.callee.property,
141
+ describeReceiverPath(expr.callee.object),
142
+ receiverRootIsProp(expr.callee.object, bindings),
143
+ typeName,
144
+ )
145
+ }
146
+ }
147
+ }
148
+ recurse(expr.callee)
149
+ for (const arg of expr.args) recurse(arg)
150
+ break
151
+ }
152
+ case 'member':
153
+ recurse(expr.object)
154
+ break
155
+ case 'index-access':
156
+ recurse(expr.object)
157
+ recurse(expr.index)
158
+ break
159
+ case 'binary':
160
+ recurse(expr.left)
161
+ recurse(expr.right)
162
+ break
163
+ case 'unary':
164
+ recurse(expr.argument)
165
+ break
166
+ case 'conditional':
167
+ recurse(expr.test)
168
+ recurse(expr.consequent)
169
+ recurse(expr.alternate)
170
+ break
171
+ case 'logical':
172
+ recurse(expr.left)
173
+ recurse(expr.right)
174
+ break
175
+ case 'template-literal':
176
+ for (const part of expr.parts) if (part.type === 'expression') recurse(part.expr)
177
+ break
178
+ case 'arrow': {
179
+ const shadowed = new Map(bindings)
180
+ for (const param of expr.params) shadowed.set(param, null)
181
+ recurse(expr.body, shadowed)
182
+ break
183
+ }
184
+ case 'array-literal':
185
+ for (const el of expr.elements) recurse(el)
186
+ break
187
+ case 'object-literal':
188
+ for (const prop of expr.properties) recurse(prop.value)
189
+ break
190
+ case 'array-method':
191
+ recurse(expr.object)
192
+ for (const arg of expr.args) recurse(arg)
193
+ break
194
+ case 'identifier':
195
+ case 'literal':
196
+ case 'regex':
197
+ case 'unsupported':
198
+ break
199
+ }
200
+ }
201
+
202
+ /** `IRTemplatePart.ternary.condition` / `.lookup.key` carry no attached parse (unlike every other position here), so parse on demand. */
203
+ function walkTemplateParts(
204
+ parts: readonly IRTemplatePart[],
205
+ loc: SourceLocation,
206
+ meta: IRMetadata,
207
+ bindings: Bindings,
208
+ matchers: readonly LoweringMatcher[],
209
+ errors: CompilerError[],
210
+ seen: Set<string>,
211
+ ): void {
212
+ for (const part of parts) {
213
+ if (part.type === 'ternary') {
214
+ const trimmed = part.condition.trim()
215
+ if (trimmed) checkExpr(parseExpression(trimmed), loc, meta, bindings, matchers, errors, seen)
216
+ } else if (part.type === 'lookup') {
217
+ const trimmed = part.key.trim()
218
+ if (trimmed) checkExpr(parseExpression(trimmed), loc, meta, bindings, matchers, errors, seen)
219
+ }
220
+ }
221
+ }
222
+
223
+ function walkAttrValue(
224
+ value: AttrValue,
225
+ clientOnly: boolean | undefined,
226
+ loc: SourceLocation,
227
+ meta: IRMetadata,
228
+ bindings: Bindings,
229
+ matchers: readonly LoweringMatcher[],
230
+ errors: CompilerError[],
231
+ seen: Set<string>,
232
+ ): void {
233
+ if (clientOnly) return
234
+ if (value.kind === 'expression') {
235
+ if (value.parsed) checkExpr(value.parsed, loc, meta, bindings, matchers, errors, seen)
236
+ if (value.parts) walkTemplateParts(value.parts, loc, meta, bindings, matchers, errors, seen)
237
+ } else if (value.kind === 'spread') {
238
+ if (value.parsed) checkExpr(value.parsed, loc, meta, bindings, matchers, errors, seen)
239
+ } else if (value.kind === 'template') {
240
+ walkTemplateParts(value.parts, loc, meta, bindings, matchers, errors, seen)
241
+ }
242
+ }
243
+
244
+ function walkNode(
245
+ node: IRNode,
246
+ meta: IRMetadata,
247
+ bindings: Bindings,
248
+ matchers: readonly LoweringMatcher[],
249
+ errors: CompilerError[],
250
+ seen: Set<string>,
251
+ ): void {
252
+ if (node.type === 'expression') {
253
+ if (!node.clientOnly && node.parsed) checkExpr(node.parsed, node.loc, meta, bindings, matchers, errors, seen)
254
+ } else if (node.type === 'conditional') {
255
+ if (!node.clientOnly && node.parsedCondition) checkExpr(node.parsedCondition, node.loc, meta, bindings, matchers, errors, seen)
256
+ } else if (node.type === 'if-statement') {
257
+ if (node.parsedCondition) checkExpr(node.parsedCondition, node.loc, meta, bindings, matchers, errors, seen)
258
+ }
259
+
260
+ if (node.type === 'element') {
261
+ for (const attr of node.attrs) walkAttrValue(attr.value, attr.clientOnly, attr.loc, meta, bindings, matchers, errors, seen)
262
+ } else if (node.type === 'component') {
263
+ for (const prop of node.props) walkAttrValue(prop.value, prop.clientOnly, prop.loc, meta, bindings, matchers, errors, seen)
264
+ } else if (node.type === 'provider') {
265
+ walkAttrValue(node.valueProp.value, node.valueProp.clientOnly, node.valueProp.loc, meta, bindings, matchers, errors, seen)
266
+ }
267
+
268
+ switch (node.type) {
269
+ case 'element':
270
+ case 'component':
271
+ case 'fragment':
272
+ case 'provider':
273
+ for (const child of node.children) walkNode(child, meta, bindings, matchers, errors, seen)
274
+ break
275
+ case 'async':
276
+ walkNode(node.fallback, meta, bindings, matchers, errors, seen)
277
+ for (const child of node.children) walkNode(child, meta, bindings, matchers, errors, seen)
278
+ break
279
+ case 'loop': {
280
+ if (node.clientOnly) break
281
+ if (node.arrayParsed) checkExpr(node.arrayParsed, node.loc, meta, bindings, matchers, errors, seen)
282
+ const loopBindings = new Map(bindings)
283
+ const arrayType = node.arrayParsed ? resolveReceiverType(node.arrayParsed, meta, bindings) : null
284
+ loopBindings.set(node.param, arrayType?.kind === 'array' ? arrayType.elementType ?? null : null)
285
+ if (node.index) loopBindings.set(node.index, null)
286
+ for (const child of node.children) walkNode(child, meta, loopBindings, matchers, errors, seen)
287
+ if (node.childComponent) {
288
+ for (const child of node.childComponent.children) walkNode(child, meta, loopBindings, matchers, errors, seen)
289
+ }
290
+ for (const nested of node.nestedComponents ?? []) {
291
+ for (const child of nested.children) walkNode(child, meta, loopBindings, matchers, errors, seen)
292
+ }
293
+ for (const frag of node.flatMapCallback?.fragments ?? []) {
294
+ walkNode(frag.ir, meta, loopBindings, matchers, errors, seen)
295
+ }
296
+ break
297
+ }
298
+ case 'conditional':
299
+ // A clientOnly conditional's branches never reach any template (the
300
+ // whole expression defers to hydrate), so walking them would flag calls
301
+ // whose own suggested remediation — /* @client */ — is already applied.
302
+ if (node.clientOnly) break
303
+ walkNode(node.whenTrue, meta, bindings, matchers, errors, seen)
304
+ walkNode(node.whenFalse, meta, bindings, matchers, errors, seen)
305
+ break
306
+ case 'if-statement':
307
+ walkNode(node.consequent, meta, bindings, matchers, errors, seen)
308
+ if (node.alternate) walkNode(node.alternate, meta, bindings, matchers, errors, seen)
309
+ break
310
+ }
311
+ }
package/src/types.ts CHANGED
@@ -90,6 +90,15 @@ export interface ParamInfo {
90
90
  defaultContainsArrow?: boolean
91
91
  /** When true, the parameter is a rest spread (`...args`) — emit must prepend `...`. */
92
92
  isRest?: boolean
93
+ /**
94
+ * Source property name for an aliased destructured prop (`{ createdAt: c }`
95
+ * → name: 'c', sourceName: 'createdAt'). Set ONLY when the binding renames —
96
+ * an un-aliased binding leaves it unset so existing IR shapes/snapshots are
97
+ * untouched. Consumers keying into `propsType.properties` must use
98
+ * `sourceName ?? name` (the param's own `type` degrades to `unknown` for
99
+ * non-primitive props — `collectMemberTypes`' primitives-only gate).
100
+ */
101
+ sourceName?: string
93
102
  }
94
103
 
95
104
  // =============================================================================
@@ -1230,6 +1239,19 @@ export interface SignalInfo {
1230
1239
  parsed?: ParsedExpr
1231
1240
  /** Initial value with TypeScript type annotations preserved, for .tsx output */
1232
1241
  typedInitialValue?: string
1242
+ /**
1243
+ * `initialValue` with bare destructured prop references rewritten to
1244
+ * `_p.<name>` (#2265), for the CSR `template:` arrow's module-scope
1245
+ * SSR-string fallback — mirrors the `templateExpr`/`templateArray`/
1246
+ * `templateCondition` fields on other IR positions. Destructured mode
1247
+ * only (`propsObjectName === null`); undefined when no rewrite was
1248
+ * needed (no destructured prop referenced) or the component is in
1249
+ * object-props mode (there, `rewritePropsObjectRef`/`applyPropsRewrite`
1250
+ * handle `props.x` → `_p.x` on the final joined/substituted string
1251
+ * instead, since a bare `props.x` textual match works without an
1252
+ * AST-level pre-rewrite).
1253
+ */
1254
+ templateInitialValue?: string
1233
1255
  type: TypeInfo
1234
1256
  loc: SourceLocation
1235
1257
  /**