@barefootjs/jsx 0.19.1 → 0.21.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/builtin-lowering-plugins.d.ts.map +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/date-lowering.d.ts +33 -0
- package/dist/date-lowering.d.ts.map +1 -0
- package/dist/index.js +663 -218
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/generate-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +14 -1
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/rich-type-evidence.d.ts +67 -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 +9 -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__/date-lowering.test.ts +232 -0
- package/src/__tests__/nested-loop-reactive-attrs.test.ts +56 -0
- package/src/__tests__/rich-type-method-refusal.test.ts +323 -0
- package/src/analyzer.ts +21 -2
- package/src/builtin-lowering-plugins.ts +2 -1
- package/src/compiler.ts +3 -0
- package/src/date-lowering.ts +117 -0
- package/src/ir-to-client-js/emit-reactive.ts +103 -2
- package/src/ir-to-client-js/generate-init.ts +11 -4
- package/src/ir-to-client-js/imports.ts +4 -0
- package/src/ir-to-client-js/index.ts +2 -0
- package/src/ir-to-client-js/reactivity.ts +13 -2
- package/src/ir-to-client-js/types.ts +15 -0
- package/src/jsx-to-ir.ts +128 -3
- package/src/rich-type-evidence.ts +159 -0
- package/src/rich-type-refusal.ts +311 -0
- package/src/types.ts +9 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-evidence resolution for the rich-type method-call refusal (#2273).
|
|
3
|
+
*
|
|
4
|
+
* `resolveReceiverType` answers one question: "what TypeScript type, if any,
|
|
5
|
+
* does this `ParsedExpr` evaluate to?" — using only the structured metadata
|
|
6
|
+
* already collected at IR-build time (`propsType` / `typeDefinitions`), never
|
|
7
|
+
* a fresh type-checker pass. It is deliberately conservative: any receiver
|
|
8
|
+
* shape it doesn't recognize (a call result, computed access, a local not in
|
|
9
|
+
* `bindings`, …) resolves to `null` ("no evidence"), which the caller must
|
|
10
|
+
* treat as "don't flag" rather than "flag as unknown". A false negative here
|
|
11
|
+
* only misses a refusal; a false positive would incorrectly block valid code.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { IRMetadata, PropertyInfo, TypeInfo } from './types.ts'
|
|
15
|
+
import type { ParsedExpr } from './expression-parser.ts'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Built-in JS/TS types whose instance methods have no catalogued lowering
|
|
19
|
+
* (spec/subset-conformance.md). A prop typed as one of these is opaque past
|
|
20
|
+
* this point — the adapters have no structural representation for `Date`,
|
|
21
|
+
* `Map`, etc., only for the primitives/arrays/plain-objects the IR already
|
|
22
|
+
* lowers. Names only (no generic args) — compare against `baseTypeName`.
|
|
23
|
+
*
|
|
24
|
+
* Two shapes deliberately escape this catalogue (conservative misses, not
|
|
25
|
+
* bugs — a miss only skips a refusal, never misdiagnoses):
|
|
26
|
+
* - keyword-typed `bigint` / `symbol` annotations lower to
|
|
27
|
+
* `{ kind: 'unknown' }` in `typeNodeToTypeInfo` (only the object-form
|
|
28
|
+
* `BigInt` / `Symbol` type references reach `kind: 'interface'` and
|
|
29
|
+
* match here);
|
|
30
|
+
* - a local alias of a host type (`type Timestamp = Date`) resolves to
|
|
31
|
+
* the alias NAME — `derefNamedType` only fills in `properties` from a
|
|
32
|
+
* declaration, it never rewrites `raw` to the alias target — so the
|
|
33
|
+
* catalogue lookup sees `Timestamp`, not `Date`.
|
|
34
|
+
*/
|
|
35
|
+
export const HOST_RICH_TYPE_NAMES: ReadonlySet<string> = new Set([
|
|
36
|
+
'Date',
|
|
37
|
+
'Map',
|
|
38
|
+
'Set',
|
|
39
|
+
'WeakMap',
|
|
40
|
+
'WeakSet',
|
|
41
|
+
'URL',
|
|
42
|
+
'URLSearchParams',
|
|
43
|
+
'RegExp',
|
|
44
|
+
'Promise',
|
|
45
|
+
'Error',
|
|
46
|
+
'Symbol',
|
|
47
|
+
'BigInt',
|
|
48
|
+
'Function',
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Strip generic type arguments from a `TypeInfo.raw` string (`Map<string,
|
|
53
|
+
* string>` → `Map`) so a parametrized host type still matches the bare-name
|
|
54
|
+
* catalogue above. `raw` is source-verbatim (`typeNodeToTypeInfo`), so this
|
|
55
|
+
* is a plain substring split on the first `<` — not a type-syntax parse.
|
|
56
|
+
*/
|
|
57
|
+
export function baseTypeName(raw: string): string {
|
|
58
|
+
const idx = raw.indexOf('<')
|
|
59
|
+
return (idx === -1 ? raw : raw.slice(0, idx)).trim()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type EvidenceMetadata = Pick<IRMetadata, 'propsType' | 'propsObjectName' | 'propsParams' | 'typeDefinitions'>
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Collapse a union to its single non-nullish arm (`Date | null` → `Date`),
|
|
66
|
+
* recursively, so an optional rich-typed prop still carries evidence. A
|
|
67
|
+
* union with more than one non-nullish arm has no single answer and is left
|
|
68
|
+
* as-is (its `kind` is `'union'`, which never matches the `'interface'`
|
|
69
|
+
* check callers gate on).
|
|
70
|
+
*/
|
|
71
|
+
function isNullishArm(t: TypeInfo): boolean {
|
|
72
|
+
if (t.kind === 'primitive' && (t.primitive === 'null' || t.primitive === 'undefined')) return true
|
|
73
|
+
// `null` as a type annotation is a `ts.LiteralTypeNode` (not the
|
|
74
|
+
// `NullKeyword` `typeNodeToTypeInfo`'s primitive switch checks for), so it
|
|
75
|
+
// falls through to `{ kind: 'unknown', raw: 'null' }` there — a pre-existing
|
|
76
|
+
// gap in that shared helper, out of scope to fix here. Match on `raw` too
|
|
77
|
+
// so `Date | null` still strips down to `Date`.
|
|
78
|
+
return t.kind === 'unknown' && (t.raw === 'null' || t.raw === 'undefined')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function stripUnion(type: TypeInfo | null): TypeInfo | null {
|
|
82
|
+
if (!type || type.kind !== 'union' || !type.unionTypes) return type
|
|
83
|
+
const nonNullish = type.unionTypes.filter((t) => !isNullishArm(t))
|
|
84
|
+
return nonNullish.length === 1 ? stripUnion(nonNullish[0]) : type
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve a named type (`{ kind: 'interface', raw: 'Props' }`) that carries
|
|
89
|
+
* no inline `properties` to its declaration's field list via
|
|
90
|
+
* `metadata.typeDefinitions`. A type already carrying properties (an inline
|
|
91
|
+
* object literal type, or a type resolved from `tsTypeToTypeInfo`) is
|
|
92
|
+
* returned unchanged — this only fills in the gap left by a *named*
|
|
93
|
+
* reference, which `typeNodeToTypeInfo` intentionally resolves to
|
|
94
|
+
* `{ kind: 'interface', raw }` with no member walk of its own.
|
|
95
|
+
*/
|
|
96
|
+
export function derefNamedType(type: TypeInfo, meta: EvidenceMetadata): TypeInfo {
|
|
97
|
+
if (type.kind !== 'interface') return type
|
|
98
|
+
if (type.properties && type.properties.length > 0) return type
|
|
99
|
+
const name = baseTypeName(type.raw)
|
|
100
|
+
const def = meta.typeDefinitions.find((d) => d.name === name)
|
|
101
|
+
if (!def?.properties) return type
|
|
102
|
+
return { ...type, properties: def.properties }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Resolve one property's type off an object-shaped receiver type, deref'ing
|
|
107
|
+
* a named type first (`Props.createdAt`) and stripping a nullable union off
|
|
108
|
+
* the result (`Date | null` field). Returns `null` when the receiver has no
|
|
109
|
+
* evidence, or the property isn't found on it.
|
|
110
|
+
*/
|
|
111
|
+
function lookupProperty(objType: TypeInfo | null, propName: string, meta: EvidenceMetadata): TypeInfo | null {
|
|
112
|
+
const stripped = stripUnion(objType)
|
|
113
|
+
if (!stripped) return null
|
|
114
|
+
const deref = derefNamedType(stripped, meta)
|
|
115
|
+
const prop = deref.properties?.find((p: PropertyInfo) => p.name === propName)
|
|
116
|
+
return prop ? stripUnion(prop.type) : null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Resolve the TypeInfo of a receiver expression, using only propsType /
|
|
121
|
+
* propsParams / typeDefinitions and the caller-supplied local bindings.
|
|
122
|
+
* `bindings` maps a name to its known type — or explicitly to `null` for a
|
|
123
|
+
* shadow the caller has proven carries no evidence (e.g. an arrow param, a
|
|
124
|
+
* loop item whose array type isn't known). A `bindings` hit always wins over
|
|
125
|
+
* the props fallback, matching JS lexical shadowing.
|
|
126
|
+
*
|
|
127
|
+
* Only two `ParsedExpr` shapes carry evidence: a bare identifier and a
|
|
128
|
+
* non-computed member access. Everything else (calls, computed/index
|
|
129
|
+
* access, literals, …) resolves to `null` — see the module doc.
|
|
130
|
+
*/
|
|
131
|
+
export function resolveReceiverType(
|
|
132
|
+
expr: ParsedExpr,
|
|
133
|
+
meta: EvidenceMetadata,
|
|
134
|
+
bindings: ReadonlyMap<string, TypeInfo | null>,
|
|
135
|
+
): TypeInfo | null {
|
|
136
|
+
if (expr.kind === 'identifier') {
|
|
137
|
+
if (bindings.has(expr.name)) return stripUnion(bindings.get(expr.name) ?? null)
|
|
138
|
+
if (meta.propsObjectName !== null) {
|
|
139
|
+
// Object-props mode: props are only reachable through the props object,
|
|
140
|
+
// so a bare identifier is never a prop — treating every name that
|
|
141
|
+
// happens to match a propsType field as one would misattribute module
|
|
142
|
+
// consts / imports that share a field's name.
|
|
143
|
+
return expr.name === meta.propsObjectName ? stripUnion(meta.propsType) : null
|
|
144
|
+
}
|
|
145
|
+
// Destructured mode: only a declared param binding is a prop (membership
|
|
146
|
+
// via propsParams, which carries LOCAL names — including rename targets).
|
|
147
|
+
// The TYPE must come from propsType.properties keyed by the SOURCE prop
|
|
148
|
+
// name: propsParams' own `type` degrades to `unknown` for non-primitive
|
|
149
|
+
// props (`collectMemberTypes`' primitives-only gate).
|
|
150
|
+
const param = meta.propsParams.find((p) => p.name === expr.name && !p.isRest)
|
|
151
|
+
if (!param) return null
|
|
152
|
+
return lookupProperty(meta.propsType, param.sourceName ?? param.name, meta)
|
|
153
|
+
}
|
|
154
|
+
if (expr.kind === 'member' && !expr.computed) {
|
|
155
|
+
const objType = resolveReceiverType(expr.object, meta, bindings)
|
|
156
|
+
return lookupProperty(objType, expr.property, meta)
|
|
157
|
+
}
|
|
158
|
+
return null
|
|
159
|
+
}
|
|
@@ -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
|
// =============================================================================
|