@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/src/analyzer.ts CHANGED
@@ -1226,11 +1226,27 @@ function collectSignal(node: ts.VariableDeclaration, ctx: AnalyzerContext): void
1226
1226
  // the binding actually in scope rather than a hardcoded canonical name (#2057).
1227
1227
  const envFactory = envReader ? callExpr.expression.getText(ctx.sourceFile) : undefined
1228
1228
 
1229
+ // Destructured-arg components only (#2265): a signal's initial value
1230
+ // referencing a bare destructured prop (`createSignal(size ?? 1)` with
1231
+ // `{ size }: { size?: number }`) needs `_p.size` for the CSR
1232
+ // `template:` arrow's module-scope SSR fallback — that arrow isn't a
1233
+ // closure over `initXxx`'s `const size = _p.size` extraction. Mirrors
1234
+ // the local-constant rewrite a few hundred lines up (`propNames` built
1235
+ // the same way from `ctx.propsParams`).
1236
+ let templateInitialValue: string | undefined
1237
+ if (!ctx.propsObjectName && callExpr.arguments[0]) {
1238
+ const propNames = new Set(ctx.propsParams.map(p => p.name))
1239
+ if (propNames.size > 0) {
1240
+ templateInitialValue = rewriteBarePropRefs(initialValue, callExpr.arguments[0], propNames)
1241
+ }
1242
+ }
1243
+
1229
1244
  ctx.signals.push({
1230
1245
  getter,
1231
1246
  setter,
1232
1247
  initialValue,
1233
1248
  typedInitialValue: typedInitialValue !== initialValue ? typedInitialValue : undefined,
1249
+ templateInitialValue,
1234
1250
  type,
1235
1251
  loc: getSourceLocation(node, ctx.sourceFile, ctx.filePath),
1236
1252
  initialFreeIdentifiers: callExpr.arguments[0]
@@ -3052,6 +3068,8 @@ function extractProps(param: ts.ParameterDeclaration, ctx: AnalyzerContext): voi
3052
3068
  optional: !!member?.optional || !!element.initializer,
3053
3069
  defaultValue,
3054
3070
  defaultContainsArrow: defaultContainsArrow || undefined,
3071
+ // Only aliased bindings carry the source key — see ParamInfo.sourceName.
3072
+ ...(sourcePropName !== localName && { sourceName: sourcePropName }),
3055
3073
  })
3056
3074
  }
3057
3075
  }
package/src/compiler.ts CHANGED
@@ -24,6 +24,7 @@ import { applyCssLayerPrefix } from './css-layer-prefixer.ts'
24
24
  import { preprocessInlineJsxCallbacks } from './preprocess-inline-jsx-callbacks.ts'
25
25
  import { extractSsrDefaults } from './ssr-defaults.ts'
26
26
  import { computeSsrSeedPlan } from './ssr-seed-plan.ts'
27
+ import { checkRichTypeMethodCalls } from './rich-type-refusal.ts'
27
28
 
28
29
  /**
29
30
  * Extended compile options with required adapter
@@ -137,6 +138,7 @@ function compileMultipleComponents(
137
138
  }
138
139
 
139
140
  componentIR.metadata.clientAnalysis = analyzeClientNeeds(componentIR)
141
+ checkRichTypeMethodCalls(componentIR.root, componentIR.metadata, errors)
140
142
 
141
143
  if (options.cssLayerPrefix) {
142
144
  applyCssLayerPrefix(componentIR, options.cssLayerPrefix)
@@ -615,6 +617,7 @@ export function compileJSX(
615
617
 
616
618
  // Pre-compute client JS analysis for adapter optimization
617
619
  componentIR.metadata.clientAnalysis = analyzeClientNeeds(componentIR)
620
+ checkRichTypeMethodCalls(componentIR.root, componentIR.metadata, errors)
618
621
 
619
622
  // Cross-file @client signal sources: identify which import sources
620
623
  // need `.client.js` path rewriting in the client bundle.
@@ -591,6 +591,58 @@ export function cssKebabCase(name: string): string {
591
591
  return name.replace(/[A-Z]/g, m => '-' + m.toLowerCase()).replace(/^ms-/, '-ms-')
592
592
  }
593
593
 
594
+ /**
595
+ * Whether a dynamic CSS style VALUE is "unsafe" — the CSS-injection guard
596
+ * Hono's real JSX runtime (`hono/jsx/utils.ts`'s `hasUnsafeStyleValue`,
597
+ * the ORACLE this compiler's adapters must match, #2261) applies before
598
+ * emitting a `style={{...}}` object-literal property. A value flagged
599
+ * unsafe is DROPPED — the whole `key:value` pair is omitted from the CSS
600
+ * string — rather than escaped; this is a hand-rolled structural scan for
601
+ * characters that could break out of a CSS declaration (unbalanced
602
+ * quotes/brackets, `{`/`}`, a bare `;`, an unterminated CSS comment
603
+ * or backslash-escape), NOT real CSSOM property validation. Ported
604
+ * character-for-character (character-CODE comparisons, not codepoints —
605
+ * every tested character is ASCII, so scanning consistently by UTF-16
606
+ * code unit, byte, or codepoint all agree) so every adapter's own port
607
+ * (Go/Ruby/Python/Rust/PHP/Perl) stays byte-isomorphic with this
608
+ * reference and with Hono.
609
+ */
610
+ export function hasUnsafeStyleValue(value: string): boolean {
611
+ let quote = 0
612
+ const blockStack: number[] = []
613
+ for (let i = 0, len = value.length; i < len; i++) {
614
+ const c = value.charCodeAt(i)
615
+ if (c === 92) {
616
+ // backslash — an escape sequence; an escape at the very end (nothing
617
+ // to escape) is unsafe, else skip the escaped character.
618
+ if (i === len - 1) return true
619
+ i++
620
+ } else if (quote !== 0) {
621
+ if (c === 10 || c === 12 || c === 13) return true // \n \f \r inside a quoted string
622
+ if (c === quote) quote = 0
623
+ } else if (c === 47 && value.charCodeAt(i + 1) === 42) {
624
+ // "/*" comment start — must close before the value ends.
625
+ const end = value.indexOf('*/', i + 2)
626
+ if (end === -1) return true
627
+ i = end + 1
628
+ } else if (c === 34 || c === 39) {
629
+ quote = c // " or '
630
+ } else if (c === 40) {
631
+ blockStack.push(41) // ( expects a matching )
632
+ } else if (c === 91) {
633
+ blockStack.push(93) // [ expects a matching ]
634
+ } else if (c === 123 || c === 125) {
635
+ return true // { or } — always unsafe
636
+ } else if (c === 41 || c === 93) {
637
+ if (blockStack[blockStack.length - 1] !== c) return true // mismatched close
638
+ blockStack.pop()
639
+ } else if (c === 59 && blockStack.length === 0) {
640
+ return true // ; outside any bracket
641
+ }
642
+ }
643
+ return quote !== 0 || blockStack.length !== 0 // unterminated quote/bracket
644
+ }
645
+
594
646
  /**
595
647
  * Parse a JSX `style={{ … }}` object-literal source into CSS entries, or
596
648
  * `null` when the shape isn't a plain object of static-keyed properties
@@ -3538,7 +3590,13 @@ export function freeIdentifiers(expr: ParsedExpr): Set<string> | null {
3538
3590
  case 'regex':
3539
3591
  return true
3540
3592
  case 'identifier':
3541
- if (!bound.has(e.name)) free.add(e.name)
3593
+ // `undefined` parses as an `identifier` node (unlike `null`, which
3594
+ // is a `literal`) but is never a scope reference — every adapter
3595
+ // emitter already lowers it to `nil`/`none`/etc. Reporting it as
3596
+ // free wrongly opaques an otherwise-derivable expression (#2260:
3597
+ // `props.pressed !== undefined` in `computeSsrSeedPlan`'s
3598
+ // `classify()`, whose `baseScope` has no `undefined` entry).
3599
+ if (e.name !== 'undefined' && !bound.has(e.name)) free.add(e.name)
3542
3600
  return true
3543
3601
  case 'call': {
3544
3602
  const isBuiltinCallee = evalBuiltinCalleeName(e.callee) !== null
package/src/index.ts CHANGED
@@ -320,7 +320,7 @@ export {
320
320
  export { ErrorCodes, createError, formatError, generateCodeFrame } from './errors.ts'
321
321
 
322
322
  // Expression Parser
323
- export { parseExpression, tsNodeToParsedExpr, asCallbackMethodCall, CALLBACK_METHODS, sortComparatorFromArrow, serializeParsedExpr, freeVarsInBody, freeIdentifiers, materializeGetterCalls, isSupported, exprToString, stringifyParsedExpr, identifierPath, parseBlockBody, parseBlockBodyTolerant, foldBlockToExpr, predicateTernaryToLogical, containsHigherOrder, extractArrowBodyExpression, parseStyleObjectEntries, parseProviderObjectLiteral, type ProviderObjectMember, type FoldBlockOptions } from './expression-parser.ts'
323
+ export { parseExpression, tsNodeToParsedExpr, asCallbackMethodCall, CALLBACK_METHODS, sortComparatorFromArrow, serializeParsedExpr, freeVarsInBody, freeIdentifiers, materializeGetterCalls, isSupported, exprToString, stringifyParsedExpr, identifierPath, parseBlockBody, parseBlockBodyTolerant, foldBlockToExpr, predicateTernaryToLogical, containsHigherOrder, extractArrowBodyExpression, parseStyleObjectEntries, hasUnsafeStyleValue, parseProviderObjectLiteral, type ProviderObjectMember, type FoldBlockOptions } from './expression-parser.ts'
324
324
  export type { StyleObjectEntry } from './expression-parser.ts'
325
325
  export { PARSED_EXPR_KINDS } from './expression-parser.ts'
326
326
  export type { ParsedExpr, ObjectLiteralProperty, ParsedStatement, SortComparator, SortKey, FlatDepth, SupportLevel, SupportResult, TemplatePart } from './expression-parser.ts'
@@ -313,26 +313,24 @@ export function collectInnerLoops(
313
313
  const refsOuter = outerLoopParam
314
314
  ? new RegExp(`\\b${outerLoopParam}\\b`).test(n.array)
315
315
  : false
316
- // Per-item bindings for inner loop body. Mirror the gating the
317
- // pre-#1244 separate-field shape used:
318
- // - reactiveTexts: only when array references outer loop param
319
- // (otherwise non-outer-param reads stay statically templated).
320
- // - reactiveAttrs / refs: always when ctx is available; the
321
- // attribute SSR template renders the initial value and a
322
- // missing per-item createEffect would freeze it; refs need to
323
- // fire on every renderItem invocation (#1244).
316
+ // Per-item bindings for inner loop body, collected uniformly when
317
+ // ctx is available: reactiveTexts / reactiveAttrs / refs are each
318
+ // classified against the loop's OWN param via `classifyReactivity`,
319
+ // which already filters out non-reactive reads a `refsOuter` gate
320
+ // on texts alone (removed, #2264) wrongly used the FIXED top-level
321
+ // loop param at every nesting depth, so an innermost loop whose
322
+ // array only referenced its immediate parent (not the outermost
323
+ // param) silently dropped its text-child update effect while the
324
+ // sibling attribute effect (ungated) still fired. Refs need to fire
325
+ // on every renderItem invocation (#1244).
324
326
  // - events / conditionals: only in `collectBindings` (branch)
325
327
  // mode; the legacy non-branch path didn't wire them on
326
328
  // `NestedLoop` because event delegation handles them through
327
329
  // the parent's bindings instead.
328
330
  const bindings: LoopChildBindings = emptyLoopChildBindings()
329
- if (refsOuter && ctx) {
330
- for (const child of n.children) {
331
- bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, n.param, n.paramBindings))
332
- }
333
- }
334
331
  if (ctx) {
335
332
  for (const child of n.children) {
333
+ bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, n.param, n.paramBindings))
336
334
  bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, n.param, n.paramBindings))
337
335
  bindings.refs.push(...collectLoopChildRefs(child))
338
336
  }
@@ -474,6 +474,13 @@ function normalizeSignalInitial(signal: SignalInfo, propsObjectName: string | nu
474
474
  if (initialValue.startsWith(propsPrefix) && !initialValue.includes('??')) {
475
475
  return `${initialValue} ?? ${inferDefaultValue(signal.type)}`
476
476
  }
477
- return initialValue
477
+ // Destructured mode (#2265): `templateInitialValue` (when present) has
478
+ // bare destructured prop refs already rewritten to `_p.X` — the
479
+ // module-scope CSR `template:` arrow can't see the bare name (it isn't
480
+ // a closure over `initXxx`'s `const size = _p.size` extraction), which
481
+ // otherwise throws `ReferenceError` at template-eval time. The `??`
482
+ // fallback branch above only applies to object-props mode (a raw
483
+ // `props.X` prefix match), so it's checked against the ORIGINAL value.
484
+ return signal.templateInitialValue ?? initialValue
478
485
  }
479
486
 
@@ -99,16 +99,23 @@ export function generateInitFunction(
99
99
  let generatedCode = rewritePropsObjectRef(lines.join('\n'), ctx.propsObjectName)
100
100
  generatedCode += '\n' + hydrateLine
101
101
 
102
- const allImportLines = resolveFinalImports(generatedCode, ir, localImportPrefixes)
102
+ // Substitute module-level declarations BEFORE import detection: a
103
+ // module-level helper's body (e.g. `buildSheetVMs` calling
104
+ // `computeSheetGeometry`) only exists in `moduleConstantsCode`, so
105
+ // scanning `generatedCode` first would miss any import referenced
106
+ // only from that body and silently drop it (#2283).
103
107
  const moduleConstantsCode = emitModuleLevelDeclarations(
104
108
  classification.moduleLevelConstants,
105
109
  classification.moduleLevelFunctions,
106
110
  classification.moduleLevelSignals,
107
111
  classification.moduleLevelMemos,
108
112
  )
113
+ // Replacer-function form: a plain replacement string would let literal
114
+ // `$&`/`$1`/`$$` sequences in user helper bodies or import paths be
115
+ // reinterpreted by `String.replace`'s special-pattern handling.
116
+ const codeWithModuleConstants = generatedCode.replace(MODULE_CONSTANTS_PLACEHOLDER, () => moduleConstantsCode)
117
+ const allImportLines = resolveFinalImports(codeWithModuleConstants, ir, localImportPrefixes)
109
118
 
110
- return generatedCode
111
- .replace(IMPORT_PLACEHOLDER, allImportLines)
112
- .replace(MODULE_CONSTANTS_PLACEHOLDER, moduleConstantsCode)
119
+ return codeWithModuleConstants.replace(IMPORT_PLACEHOLDER, () => allImportLines)
113
120
  }
114
121
 
@@ -529,8 +529,19 @@ export function collectLoopChildReactiveTexts(
529
529
  const originFreeIds = freeIdsFromRefs(n.origin?.freeRefs)
530
530
  const expanded = expandConstantForReactivity(n.expr, ctx, originFreeIds)
531
531
  // Include if expression reads signals OR references the loop parameter
532
- // (loop param becomes a signal accessor via per-item signals).
533
- if (classifyReactivity(expanded.expr, ctx, loopParam, loopParamBindings, expanded.freeIds).kind === 'none') return
532
+ // (loop param becomes a signal accessor via per-item signals). Falls
533
+ // back to the Solid-style AST-flag wrap decision — mirroring
534
+ // `collectLoopChildReactiveAttrs`'s `callsReactiveGetters` /
535
+ // `hasFunctionCalls` fallback (#1673) and the top-level text path's
536
+ // `decideWrapFromAstFlags` gate (`collectElements`'s `expression`
537
+ // handler) — so a loop-item text read through an opaque helper
538
+ // (`textAt(i)` where `const textAt = (i) => rows()[i]`, which
539
+ // `classifyReactivity` can't see through) still gets an update
540
+ // effect instead of silently freezing at its SSR value (#2282).
541
+ const reactive =
542
+ classifyReactivity(expanded.expr, ctx, loopParam, loopParamBindings, expanded.freeIds).kind !== 'none'
543
+ || decideWrapFromAstFlags(n).wrap
544
+ if (!reactive) return
534
545
  texts.push({
535
546
  slotId: n.slotId,
536
547
  expression: expanded.expr,
@@ -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
+ 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
+ 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
+ }