@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.
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +17 -0
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +342 -11
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/generate-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/rich-type-evidence.d.ts +56 -0
- package/dist/rich-type-evidence.d.ts.map +1 -0
- package/dist/rich-type-refusal.d.ts +35 -0
- package/dist/rich-type-refusal.d.ts.map +1 -0
- package/dist/types.d.ts +22 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/client-js-generation.test.ts +59 -0
- package/src/__tests__/nested-loop-reactive-attrs.test.ts +104 -0
- package/src/__tests__/rich-type-method-refusal.test.ts +303 -0
- package/src/analyzer.ts +18 -0
- package/src/compiler.ts +3 -0
- package/src/expression-parser.ts +59 -1
- package/src/index.ts +1 -1
- package/src/ir-to-client-js/collect-elements.ts +11 -13
- package/src/ir-to-client-js/csr-substitute.ts +8 -1
- package/src/ir-to-client-js/generate-init.ts +11 -4
- package/src/ir-to-client-js/reactivity.ts +13 -2
- package/src/rich-type-evidence.ts +159 -0
- package/src/rich-type-refusal.ts +311 -0
- package/src/types.ts +22 -0
|
@@ -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
|
/**
|