@component-compass/parser-react 0.0.4 → 0.1.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/emit.js ADDED
@@ -0,0 +1,1009 @@
1
+ import { findEnclosingComponentBinding, positionAt } from "@component-compass/ast-utils";
2
+ import { MODULE_SCOPE } from "@component-compass/reference-graph";
3
+ import { walk } from "oxc-walker";
4
+ import { inferValue, extractFunctionReturns, extractClassRenderReturns } from "./infer-value.js";
5
+ import { readJsxAttrs } from "./jsx-attrs.js";
6
+ export function emitReact(opts) {
7
+ const captureValues = opts.captureValues ?? false;
8
+ const emitState = {
9
+ fileBuilder: opts.fileBuilder,
10
+ pendingPropForwardBindings: [],
11
+ ast: opts.ast,
12
+ source: opts.source,
13
+ };
14
+ for (const node of opts.ast.body) {
15
+ if (node.type === "ImportDeclaration") {
16
+ emitImport(node, opts.fileBuilder, opts.source);
17
+ }
18
+ else if (node.type === "VariableDeclaration") {
19
+ emitVariableDeclaration(node, opts.fileBuilder, opts.source, captureValues, emitState);
20
+ }
21
+ else if (node.type === "FunctionDeclaration" ||
22
+ node.type === "TSDeclareFunction") {
23
+ emitFunctionDeclaration(node, opts.fileBuilder, opts.source, captureValues);
24
+ }
25
+ else if (node.type === "ClassDeclaration" ||
26
+ node.type === "ClassExpression") {
27
+ emitClassDeclaration(node, opts.fileBuilder, opts.source, captureValues);
28
+ }
29
+ else if (node.type === "ExportNamedDeclaration") {
30
+ emitExportNamed(node, opts.fileBuilder, opts.source, captureValues, emitState);
31
+ }
32
+ else if (node.type === "ExportDefaultDeclaration") {
33
+ emitExportDefault(node, opts.fileBuilder, opts.source, captureValues);
34
+ }
35
+ else if (node.type === "ExportAllDeclaration") {
36
+ emitExportAll(node, opts.fileBuilder);
37
+ }
38
+ }
39
+ // Second pass: prop-forward finalize (#151).
40
+ finalizePropForwardBindings(emitState);
41
+ }
42
+ // ---------------------------------------------------------------------------
43
+ // Import emission
44
+ // ---------------------------------------------------------------------------
45
+ function emitImport(node, fb, source) {
46
+ const specifier = node.source.value;
47
+ const pos = positionAt(source, node.start);
48
+ const loc = { line: pos.line, column: pos.column + 1 };
49
+ if (!node.specifiers || node.specifiers.length === 0) {
50
+ // Side-effect import — skip (no binding emitted).
51
+ return;
52
+ }
53
+ for (const spec of node.specifiers) {
54
+ if (spec.type === "ImportSpecifier") {
55
+ // `imported` is ModuleExportName: IdentifierName | IdentifierReference | StringLiteral
56
+ // IdentifierName / IdentifierReference have `.name`; StringLiteral has `.value`
57
+ const imported = spec.imported.type === "Identifier"
58
+ ? spec.imported.name
59
+ : String(spec.imported.value);
60
+ fb.addImport({ specifier, imported, local: spec.local.name, loc });
61
+ }
62
+ else if (spec.type === "ImportDefaultSpecifier") {
63
+ fb.addImport({ specifier, imported: "default", local: spec.local.name, loc });
64
+ }
65
+ else if (spec.type === "ImportNamespaceSpecifier") {
66
+ fb.addImport({ specifier, imported: "*", local: spec.local.name, loc });
67
+ }
68
+ }
69
+ }
70
+ // ---------------------------------------------------------------------------
71
+ // JSX walking helpers
72
+ // ---------------------------------------------------------------------------
73
+ /**
74
+ * Decompose a JSX element name into symbol + memberChain.
75
+ * `<Foo />` → { symbol: "Foo", memberChain: [] }
76
+ * `<Foo.Bar.Baz />` → { symbol: "Foo", memberChain: ["Bar", "Baz"] }
77
+ * Returns null for JSXNamespacedName (skip those).
78
+ */
79
+ function decomposeJsxName(node) {
80
+ if (node.type === "JSXIdentifier") {
81
+ return { symbol: node.name, memberChain: [] };
82
+ }
83
+ if (node.type === "JSXMemberExpression") {
84
+ const parts = [];
85
+ let current = node;
86
+ while (current.type === "JSXMemberExpression") {
87
+ const mem = current;
88
+ parts.unshift(mem.property.name);
89
+ current = mem.object;
90
+ }
91
+ if (current.type !== "JSXIdentifier")
92
+ return null;
93
+ return { symbol: current.name, memberChain: parts };
94
+ }
95
+ // JSXNamespacedName — skip
96
+ return null;
97
+ }
98
+ /**
99
+ * Walk an arbitrary AST subtree looking for JSXOpeningElement nodes.
100
+ * For each found, emit a JsxUsage via fb.addJsxUsage and, if currentOwner is
101
+ * non-null, wire it via fb.setOwner.
102
+ */
103
+ function walkJsxIn(node, fb, source, currentOwner, captureValues) {
104
+ if (node == null)
105
+ return;
106
+ if (node.type === "JSXOpeningElement") {
107
+ const opening = node;
108
+ const decomposed = decomposeJsxName(opening.name);
109
+ if (decomposed) {
110
+ const pos = positionAt(source, opening.start);
111
+ const loc = { line: pos.line, column: pos.column };
112
+ fb.addJsxUsage({
113
+ ref: {
114
+ symbol: decomposed.symbol,
115
+ memberChain: decomposed.memberChain,
116
+ scope: fb.currentScope(),
117
+ loc,
118
+ originFile: fb.filePath,
119
+ },
120
+ loc,
121
+ props: readJsxAttrs(opening, captureValues),
122
+ });
123
+ if (currentOwner !== null) {
124
+ fb.setOwner(currentOwner);
125
+ }
126
+ }
127
+ }
128
+ if (node.type === "VariableDeclaration") {
129
+ const varDecl = node;
130
+ for (const declr of varDecl.declarations) {
131
+ if (declr.id.type !== "Identifier") {
132
+ tryEmitDestructureFromCall(declr, fb, source);
133
+ continue;
134
+ }
135
+ const id = declr.id;
136
+ emitDeclarationFromDeclarator(declr, fb, source, { symbol: id.name, file: fb.filePath });
137
+ }
138
+ // Fall through to the generic recursion below so we still walk into initializers
139
+ // for JSX usages, nested function bodies, and further VariableDeclarations.
140
+ }
141
+ // Nested function / arrow / fn-expr — push scope + emit params, then walk body.
142
+ // Without this:
143
+ // - render-prop arrows like `<DL>{(Item) => <Item.X />}</DL>` never register Item as a binding;
144
+ // - nested function decls inside another function body leak params (or drop them).
145
+ // Top-level FunctionDeclarations are handled by `emitFunctionDeclaration` directly (which
146
+ // already pushes a scope + calls emitParameterBindings); this branch only fires for nested
147
+ // instances reached via this generic walker.
148
+ if (node.type === "ArrowFunctionExpression" ||
149
+ node.type === "FunctionExpression" ||
150
+ node.type === "FunctionDeclaration") {
151
+ const fnNode = node;
152
+ const refSymbol = fnNode.id?.name ?? `<arrow@${fnNode.start}>`;
153
+ const fnRef = {
154
+ symbol: refSymbol,
155
+ scope: fb.currentScope(),
156
+ memberChain: [],
157
+ loc: positionAt(source, fnNode.start),
158
+ originFile: fb.filePath,
159
+ };
160
+ fb.pushScope();
161
+ try {
162
+ emitParameterBindings(fnNode.params, fnRef, fb, source);
163
+ walkJsxIn(fnNode.body, fb, source, currentOwner, captureValues);
164
+ }
165
+ finally {
166
+ fb.popScope();
167
+ }
168
+ return; // do NOT fall through to generic recursion — we walked the body already.
169
+ }
170
+ // Recurse into all child nodes
171
+ for (const val of Object.values(node)) {
172
+ if (Array.isArray(val)) {
173
+ for (const child of val) {
174
+ if (child !== null && typeof child === "object" && typeof child.type === "string") {
175
+ walkJsxIn(child, fb, source, currentOwner, captureValues);
176
+ }
177
+ }
178
+ }
179
+ else if (val !== null && typeof val === "object" && typeof val.type === "string") {
180
+ walkJsxIn(val, fb, source, currentOwner, captureValues);
181
+ }
182
+ }
183
+ }
184
+ /**
185
+ * Walk the body of a top-level function looking for identifier-callee
186
+ * CallExpressions and emit one `fb.addBodyCall` entry per call found.
187
+ * Does NOT descend into nested ArrowFunctionExpression / FunctionExpression /
188
+ * FunctionDeclaration bodies — only the lexical body of the owning top-level
189
+ * function is walked.
190
+ *
191
+ * Only Identifier callees are recorded (member-expression calls like
192
+ * `this.foo()` or `a.b()` are skipped). This matches the design intent:
193
+ * we only need direct identifier calls for the helper-caller index.
194
+ */
195
+ function walkBodyCalls(node, fb, source, ownerSymbol) {
196
+ if (node == null)
197
+ return;
198
+ // Stop descent at nested function boundaries — only the top-level body is in scope.
199
+ if (node.type === "ArrowFunctionExpression" ||
200
+ node.type === "FunctionExpression" ||
201
+ node.type === "FunctionDeclaration") {
202
+ return;
203
+ }
204
+ // Emit a body-call record for every CallExpression with an Identifier callee.
205
+ if (node.type === "CallExpression") {
206
+ const call = node;
207
+ if (call.callee.type === "Identifier") {
208
+ const id = call.callee;
209
+ const pos = positionAt(source, id.start);
210
+ // `scope: MODULE_SCOPE` is intentionally hardcoded even though the call
211
+ // site is lexically inside a function body, not at module scope. The
212
+ // sole consumer (`buildHelperCallers` → `resolveRefToDecl`) ignores
213
+ // scope and resolves by symbol + originFile only. Setting a literal
214
+ // value here keeps the code mechanical without threading the file
215
+ // builder's scope stack through `walkBodyCalls`.
216
+ fb.addBodyCall({
217
+ ownerSymbol,
218
+ callee: {
219
+ symbol: id.name,
220
+ memberChain: [],
221
+ loc: { line: pos.line, column: pos.column },
222
+ originFile: fb.filePath,
223
+ scope: MODULE_SCOPE,
224
+ },
225
+ });
226
+ }
227
+ // Still recurse into arguments and callee (e.g. IIFE or chained calls)
228
+ // but the boundary check above prevents descent into nested function bodies.
229
+ }
230
+ // Generic recursion into all child nodes.
231
+ for (const val of Object.values(node)) {
232
+ if (Array.isArray(val)) {
233
+ for (const child of val) {
234
+ if (child !== null && typeof child === "object" && typeof child.type === "string") {
235
+ walkBodyCalls(child, fb, source, ownerSymbol);
236
+ }
237
+ }
238
+ }
239
+ else if (val !== null && typeof val === "object" && typeof val.type === "string") {
240
+ walkBodyCalls(val, fb, source, ownerSymbol);
241
+ }
242
+ }
243
+ }
244
+ /**
245
+ * Walk a function-parameter or destructure pattern AST node, yielding one
246
+ * PatternLeaf per leaf Identifier. The `source` InferredType describes the
247
+ * value being destructured (e.g. `ParameterOf(fnRef, i)` or the call-return
248
+ * type for `const { X } = useFoo()`). Leaves carry MemberOf chains over
249
+ * `source` keyed by destructure path. Mirrors Omlet's
250
+ * extract_symbols_from_pattern.
251
+ *
252
+ * `sourceText` is needed to compute `positionAt` for each leaf identifier.
253
+ */
254
+ export function extractBindingsFromPattern(pattern, source, sourceText) {
255
+ const out = [];
256
+ const recurse = (node, src) => {
257
+ switch (node.type) {
258
+ case "Identifier": {
259
+ const id = node;
260
+ const pos = positionAt(sourceText, id.start);
261
+ out.push({ symbol: id.name, value: src, loc: { line: pos.line, column: pos.column } });
262
+ return;
263
+ }
264
+ case "ObjectPattern": {
265
+ const op = node;
266
+ for (const prop of op.properties) {
267
+ // OXC emits "Property" for binding patterns (BindingProperty), never "ObjectProperty"
268
+ // which only appears in expression-context object literals.
269
+ if (prop.type === "Property") {
270
+ const p = prop;
271
+ if (p.computed)
272
+ continue; // computed keys carry no static member name
273
+ const keyName = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
274
+ recurse(p.value, { kind: "MemberOf", obj: src, member: keyName });
275
+ }
276
+ else if (prop.type === "RestElement") {
277
+ const r = prop;
278
+ recurse(r.argument, src);
279
+ }
280
+ }
281
+ return;
282
+ }
283
+ case "ArrayPattern": {
284
+ const ap = node;
285
+ for (let i = 0; i < ap.elements.length; i++) {
286
+ const el = ap.elements[i];
287
+ if (el == null)
288
+ continue; // sparse holes
289
+ if (el.type === "RestElement") {
290
+ const r = el;
291
+ recurse(r.argument, src);
292
+ }
293
+ else {
294
+ // Index labels are decorative — MemberOf-over-Array in resolve-type.ts
295
+ // fans out to all elements at resolve time regardless of `member`.
296
+ recurse(el, { kind: "MemberOf", obj: src, member: String(i) });
297
+ }
298
+ }
299
+ return;
300
+ }
301
+ case "AssignmentPattern": {
302
+ const ap = node;
303
+ recurse(ap.left, src);
304
+ return;
305
+ }
306
+ case "RestElement": {
307
+ const r = node;
308
+ recurse(r.argument, src);
309
+ return;
310
+ }
311
+ // Other pattern shapes (TSParameterProperty, etc) are not in scope for #31.
312
+ }
313
+ };
314
+ recurse(pattern, source);
315
+ return out;
316
+ }
317
+ /**
318
+ * Emit one BindingDecl per leaf in each function parameter, valued as a
319
+ * MemberOf chain over `ParameterOf(fnRef, paramIndex)`. Bindings are stamped
320
+ * `dynamicSource: "parameter"` so the engine emits a dynamic-binding
321
+ * occurrence when the resolution chain bottoms out at Unknown.
322
+ *
323
+ * Call this AFTER `fb.pushScope()` so the bindings land in the function's
324
+ * body scope (not the caller's scope).
325
+ */
326
+ function emitParameterBindings(params, fnRef, fb, sourceText) {
327
+ for (let i = 0; i < params.length; i++) {
328
+ const param = params[i];
329
+ if (!param)
330
+ continue;
331
+ const paramSource = { kind: "ParameterOf", fn: fnRef, index: i };
332
+ const leaves = extractBindingsFromPattern(param, paramSource, sourceText);
333
+ for (const leaf of leaves) {
334
+ fb.addDeclaration({
335
+ symbol: leaf.symbol,
336
+ value: leaf.value,
337
+ loc: leaf.loc,
338
+ isExported: false,
339
+ dynamicSource: "parameter",
340
+ });
341
+ }
342
+ }
343
+ }
344
+ /**
345
+ * If a VariableDeclarator has a destructure pattern id AND a CallExpression
346
+ * initializer, emit one BindingDecl per leaf with `dynamicSource: "hook-return"`
347
+ * and a `MemberOf(initType, key)` value over the call's InferredType.
348
+ *
349
+ * Returns true if any leaf was emitted; false if the shape didn't match
350
+ * (caller can fall through to other handling).
351
+ *
352
+ * The `"hook-return"` label fires for ANY CallExpression-init destructure,
353
+ * not just identifiers matching /^use[A-Z]/. Keeps detection cheap and avoids
354
+ * a brittle name heuristic.
355
+ */
356
+ function tryEmitDestructureFromCall(declr, fb, source) {
357
+ if (declr.id.type === "Identifier")
358
+ return false;
359
+ if (declr.id.type !== "ObjectPattern" && declr.id.type !== "ArrayPattern")
360
+ return false;
361
+ if (declr.init == null)
362
+ return false;
363
+ const init = declr.init;
364
+ if (init.type !== "CallExpression")
365
+ return false;
366
+ const initType = inferValue(declr.init, fb.currentScope(), fb.filePath, undefined);
367
+ const leaves = extractBindingsFromPattern(declr.id, initType, source);
368
+ for (const leaf of leaves) {
369
+ fb.addDeclaration({
370
+ symbol: leaf.symbol,
371
+ value: leaf.value,
372
+ loc: leaf.loc,
373
+ isExported: false,
374
+ dynamicSource: "hook-return",
375
+ });
376
+ }
377
+ return leaves.length > 0;
378
+ }
379
+ // ---------------------------------------------------------------------------
380
+ // Prop-forward helpers (#151)
381
+ // ---------------------------------------------------------------------------
382
+ /**
383
+ * True if the given node type is a JSX expression that produces a React
384
+ * element value. Used to detect module-scope `const X = <JSX/>` bindings
385
+ * whose JSX construction site needs prop-forward owner attribution.
386
+ */
387
+ export function isJsxTypeInit(nodeType) {
388
+ return nodeType === "JSXElement" || nodeType === "JSXFragment";
389
+ }
390
+ /**
391
+ * Extract the root identifier from a JSX init expression.
392
+ * Returns the root binding name (`<Foo.Bar />` → `"Foo"`) or null for
393
+ * JSXFragment / namespaced names (no static root identifier).
394
+ */
395
+ export function rootSymbolOfJsxInit(init) {
396
+ if (init.type !== "JSXElement")
397
+ return null;
398
+ const opening = init.openingElement;
399
+ const decomposed = decomposeJsxName(opening.name);
400
+ return decomposed?.symbol ?? null;
401
+ }
402
+ /**
403
+ * Scan the file AST for Identifier reads that resolve to a pending prop-forward
404
+ * binding. Returns one ReadSite per matching read, recording the enclosing
405
+ * component as the owner attribution target.
406
+ */
407
+ function findPropForwardReadSites(emitState) {
408
+ const sites = [];
409
+ const pending = emitState.pendingPropForwardBindings;
410
+ if (pending.length === 0)
411
+ return sites;
412
+ const pendingNames = new Set(pending.map((p) => p.symbol));
413
+ walk(emitState.ast, {
414
+ enter(node, parent) {
415
+ if (node.type !== "Identifier")
416
+ return;
417
+ const idName = node.name;
418
+ if (!pendingNames.has(idName))
419
+ return;
420
+ // Skip non-read positions.
421
+ if (isWritePosition(node, parent))
422
+ return;
423
+ // Use ast-utils helper to find the enclosing component.
424
+ const enclosing = findEnclosingComponentBinding(node, emitState.ast);
425
+ if (!enclosing)
426
+ return;
427
+ // Shadowing check: skip if a closer-enclosing scope re-binds the name.
428
+ if (isShadowedByInnerScope(emitState.ast, node, idName))
429
+ return;
430
+ const pendingIdx = pending.findIndex((p) => p.symbol === idName);
431
+ if (pendingIdx === -1)
432
+ return;
433
+ const ownerRef = {
434
+ symbol: enclosing.name,
435
+ scope: MODULE_SCOPE,
436
+ memberChain: [],
437
+ loc: enclosing.loc ?? { line: 0, column: 0 },
438
+ originFile: emitState.fileBuilder.filePath,
439
+ };
440
+ sites.push({ pendingIdx, ownerRef });
441
+ },
442
+ });
443
+ return sites;
444
+ }
445
+ /**
446
+ * Returns true if `outerName` is shadowed by a closer-enclosing declaration
447
+ * between the read site (`identifierNode`) and module scope. Used by the
448
+ * prop-forward second-pass to skip reads that bind to an inner declaration
449
+ * rather than the outer module-scope binding.
450
+ *
451
+ * v1 conservative implementation: scan only the immediately enclosing
452
+ * function's parameters + top-level VariableDeclarations within the function
453
+ * body. Misses block-scoped `let` shadowing inside if/for/etc. Acceptable
454
+ * for the consumer-web patterns we're targeting; tighten if real-world data
455
+ * shows misses.
456
+ */
457
+ function isShadowedByInnerScope(ast, identifierNode, outerName) {
458
+ let stopped = false;
459
+ let shadowed = false;
460
+ const fnStack = [];
461
+ walk(ast, {
462
+ enter(node) {
463
+ if (stopped)
464
+ return;
465
+ if (node.type === "FunctionDeclaration" ||
466
+ node.type === "FunctionExpression" ||
467
+ node.type === "ArrowFunctionExpression") {
468
+ fnStack.push(node);
469
+ }
470
+ if (node === identifierNode) {
471
+ stopped = true;
472
+ const innermostFn = fnStack[fnStack.length - 1];
473
+ if (!innermostFn)
474
+ return;
475
+ // Check params for a binding named outerName.
476
+ const params = innermostFn.params ?? [];
477
+ for (const param of params) {
478
+ if (containsBindingName(param, outerName)) {
479
+ shadowed = true;
480
+ return;
481
+ }
482
+ }
483
+ // Check top-level VariableDeclarations within the function body.
484
+ const body = innermostFn.body;
485
+ if (body && body.type === "BlockStatement" && Array.isArray(body.body)) {
486
+ for (const stmt of body.body) {
487
+ if (stmt.type === "VariableDeclaration") {
488
+ const decls = stmt.declarations;
489
+ for (const d of decls) {
490
+ if (containsBindingName(d.id, outerName)) {
491
+ shadowed = true;
492
+ return;
493
+ }
494
+ }
495
+ }
496
+ }
497
+ }
498
+ }
499
+ },
500
+ leave(node) {
501
+ if (stopped)
502
+ return;
503
+ if (node.type === "FunctionDeclaration" ||
504
+ node.type === "FunctionExpression" ||
505
+ node.type === "ArrowFunctionExpression") {
506
+ fnStack.pop();
507
+ }
508
+ },
509
+ });
510
+ return shadowed;
511
+ }
512
+ /** True if the given pattern binds `name` at its top level (simple identifier
513
+ * patterns + destructure leaves; no nested-destructure recursion since v1
514
+ * doesn't need that depth). */
515
+ function containsBindingName(pattern, name) {
516
+ if (pattern.type === "Identifier") {
517
+ return pattern.name === name;
518
+ }
519
+ if (pattern.type === "ObjectPattern") {
520
+ const props = pattern.properties;
521
+ for (const p of props) {
522
+ if (p.value && containsBindingName(p.value, name))
523
+ return true;
524
+ if (p.argument && containsBindingName(p.argument, name))
525
+ return true;
526
+ }
527
+ }
528
+ if (pattern.type === "ArrayPattern") {
529
+ const elements = pattern.elements;
530
+ for (const el of elements) {
531
+ if (el && containsBindingName(el, name))
532
+ return true;
533
+ }
534
+ }
535
+ return false;
536
+ }
537
+ /**
538
+ * True when the Identifier is in a "write" position (declaration, assignment
539
+ * target, static property key, static member access property, import name)
540
+ * rather than a "read" position. Used to filter out non-read appearances of
541
+ * the binding name during the prop-forward second-pass scan.
542
+ */
543
+ function isWritePosition(node, parent) {
544
+ if (!parent)
545
+ return false;
546
+ switch (parent.type) {
547
+ case "VariableDeclarator":
548
+ return parent.id === node;
549
+ case "FunctionDeclaration":
550
+ case "FunctionExpression":
551
+ case "ArrowFunctionExpression":
552
+ case "ClassDeclaration":
553
+ case "ClassExpression":
554
+ return parent.id === node;
555
+ case "ImportSpecifier":
556
+ case "ImportDefaultSpecifier":
557
+ case "ImportNamespaceSpecifier":
558
+ return true;
559
+ case "Property": {
560
+ const p = parent;
561
+ return p.key === node && p.computed !== true;
562
+ }
563
+ case "MemberExpression": {
564
+ const p = parent;
565
+ return p.property === node && p.computed !== true;
566
+ }
567
+ case "AssignmentExpression":
568
+ return parent.left === node;
569
+ default:
570
+ return false;
571
+ }
572
+ }
573
+ /**
574
+ * Finalize prop-forward attribution. For each pending binding that has at
575
+ * least one read site, re-attribute every JsxUsage in the binding's
576
+ * construction range `[usageIdxStart, usageIdxEnd)` to the read site's
577
+ * enclosing component.
578
+ *
579
+ * Range-clone semantics: when a binding is read in N components, every usage
580
+ * in the range is cloned (N − 1) additional times so nested children
581
+ * (e.g. `<Provider><Notification/></Provider>` emits two usages — both get
582
+ * fanned out per read site). The engine emits one occurrence per
583
+ * (jsxUsage, ownership) pair, so cloning produces accurate per-component
584
+ * occurrence counts even when the binding wraps the subject in a tracked
585
+ * structural component.
586
+ *
587
+ * Pending bindings with no read sites are left alone (orphan signal
588
+ * preserved).
589
+ */
590
+ function finalizePropForwardBindings(emitState) {
591
+ const sites = findPropForwardReadSites(emitState);
592
+ if (sites.length === 0)
593
+ return;
594
+ const byBinding = new Map();
595
+ for (const site of sites) {
596
+ const list = byBinding.get(site.pendingIdx) ?? [];
597
+ list.push(site);
598
+ byBinding.set(site.pendingIdx, list);
599
+ }
600
+ const fb = emitState.fileBuilder;
601
+ for (const [pendingIdx, readSites] of byBinding) {
602
+ const pending = emitState.pendingPropForwardBindings[pendingIdx];
603
+ if (!pending)
604
+ continue;
605
+ const via = {
606
+ kind: "prop-forward",
607
+ bindingName: pending.symbol,
608
+ constructionSite: {
609
+ file: fb.filePath,
610
+ line: pending.constructionLoc.line,
611
+ column: pending.constructionLoc.column,
612
+ },
613
+ };
614
+ // First read site: re-attribute every ownership entry in the range
615
+ // [usageIdxStart, usageIdxEnd) in place via setOwnerAt. Covers both the
616
+ // root JSX and any nested children.
617
+ const firstSite = readSites[0];
618
+ if (!firstSite)
619
+ continue;
620
+ for (let i = pending.usageIdxStart; i < pending.usageIdxEnd; i++) {
621
+ fb.setOwnerAt(i, firstSite.ownerRef, via);
622
+ }
623
+ // Additional read sites: clone the entire range. Each clone gets its
624
+ // own ownership entry attributed to the additional read site's enclosing
625
+ // component, so nested usages fan out correctly across multiple consumers.
626
+ for (let s = 1; s < readSites.length; s++) {
627
+ const site = readSites[s];
628
+ if (!site)
629
+ continue;
630
+ for (let i = pending.usageIdxStart; i < pending.usageIdxEnd; i++) {
631
+ const snapshot = fb.getJsxUsage(i);
632
+ if (!snapshot)
633
+ continue;
634
+ fb.addJsxUsage(snapshot);
635
+ fb.setOwner(site.ownerRef, via);
636
+ }
637
+ }
638
+ }
639
+ }
640
+ // ---------------------------------------------------------------------------
641
+ // Declaration emission
642
+ // ---------------------------------------------------------------------------
643
+ function emitDeclarationFromDeclarator(declr, fb, source, enclosingBinding, isExported = false) {
644
+ if (declr.id.type !== "Identifier")
645
+ return;
646
+ const id = declr.id;
647
+ const loc = positionAt(source, declr.start);
648
+ fb.addDeclaration({
649
+ symbol: id.name,
650
+ value: inferValue(declr.init ?? null, fb.currentScope(), fb.filePath, enclosingBinding),
651
+ loc,
652
+ isExported,
653
+ });
654
+ }
655
+ function emitVariableDeclaration(node, fb, source, captureValues, emitState) {
656
+ for (const declr of node.declarations) {
657
+ if (declr.id.type !== "Identifier") {
658
+ // Non-Identifier id: try destructure-from-call (#31). Falls through silently
659
+ // if the init isn't a CallExpression — other destructure shapes are out of
660
+ // scope for this issue.
661
+ tryEmitDestructureFromCall(declr, fb, source);
662
+ continue;
663
+ }
664
+ const id = declr.id;
665
+ const loc = positionAt(source, declr.start);
666
+ emitDeclarationFromDeclarator(declr, fb, source, { symbol: id.name, file: fb.filePath });
667
+ if (declr.init != null) {
668
+ // Unwrap `(...)`-wrapped expressions — oxc preserves ParenthesizedExpression
669
+ // (Babel strips it). Multi-line JSX inits in real codebases are typically
670
+ // parens-wrapped; without this, `const X = (<Y/>)` falls through to the
671
+ // catch-all walk path and bypasses the prop-forward / function-component /
672
+ // HOC detection below. Loop in case of multiple wraps (`((<Y/>))`).
673
+ let init = declr.init;
674
+ while (init.type === "ParenthesizedExpression") {
675
+ init = init.expression;
676
+ }
677
+ const initType = init.type;
678
+ const ownerRef = { symbol: id.name, scope: MODULE_SCOPE, memberChain: [], loc, originFile: fb.filePath };
679
+ if (initType === "ArrowFunctionExpression" || initType === "FunctionExpression") {
680
+ // Variable holding a function component — walk its body with this symbol as owner
681
+ const fnLike = init;
682
+ const body = fnLike.body;
683
+ const fnRef = { symbol: id.name, scope: MODULE_SCOPE, memberChain: [], loc, originFile: fb.filePath };
684
+ fb.pushScope();
685
+ try {
686
+ emitParameterBindings(fnLike.params, fnRef, fb, source);
687
+ walkJsxIn(body, fb, source, ownerRef, captureValues);
688
+ walkBodyCalls(body, fb, source, id.name);
689
+ }
690
+ finally {
691
+ fb.popScope();
692
+ }
693
+ }
694
+ else if (initType === "CallExpression") {
695
+ // HOC pattern: const Foo = memo(() => ...) / forwardRef((ref, props) => ...) etc.
696
+ // Walk the first argument (inner function) body with this symbol as owner,
697
+ // so JSX inside the HOC wrapper is attributed to the variable name.
698
+ const call = init;
699
+ const firstArg = call.arguments[0];
700
+ if (firstArg && (firstArg.type === "ArrowFunctionExpression" || firstArg.type === "FunctionExpression")) {
701
+ const fnLike = firstArg;
702
+ const body = fnLike.body;
703
+ const fnRef = { symbol: id.name, scope: MODULE_SCOPE, memberChain: [], loc, originFile: fb.filePath };
704
+ fb.pushScope();
705
+ try {
706
+ emitParameterBindings(fnLike.params, fnRef, fb, source);
707
+ walkJsxIn(body, fb, source, ownerRef, captureValues);
708
+ walkBodyCalls(body, fb, source, id.name);
709
+ }
710
+ finally {
711
+ fb.popScope();
712
+ }
713
+ }
714
+ else {
715
+ // Non-HOC call expression — walk with null owner
716
+ walkJsxIn(init, fb, source, null, captureValues);
717
+ }
718
+ }
719
+ else if (isJsxTypeInit(initType) && fb.currentScope() === MODULE_SCOPE) {
720
+ // Module-scope `const X = <JSX/>` — register pending prop-forward
721
+ // binding before walking so we can re-attribute the owner at file
722
+ // finalize (#151). Capture clone data at emit time to keep
723
+ // FileBuilder write-only (apart from the one jsxUsageCount accessor —
724
+ // documented spec §11 deviation).
725
+ const rootSymbol = rootSymbolOfJsxInit(init);
726
+ if (rootSymbol !== null) {
727
+ const opening = init.openingElement;
728
+ const decomposed = decomposeJsxName(opening.name);
729
+ if (decomposed) {
730
+ const openingPos = positionAt(source, opening.start);
731
+ // Capture the JsxUsage range emitted by walking the init.
732
+ // walkJsxIn descends depth-first; the FIRST emitted usage is the
733
+ // root JSX. Subsequent usages are nested children (e.g.
734
+ // `<Provider><Notification/></Provider>` emits Provider then
735
+ // Notification). The full range is re-attributed at finalize.
736
+ const usageIdxStart = fb.jsxUsageCount();
737
+ walkJsxIn(init, fb, source, null, captureValues);
738
+ const usageIdxEnd = fb.jsxUsageCount();
739
+ if (usageIdxEnd > usageIdxStart) {
740
+ emitState.pendingPropForwardBindings.push({
741
+ symbol: id.name,
742
+ scope: MODULE_SCOPE,
743
+ constructionLoc: loc,
744
+ jsxRootLoc: { line: openingPos.line, column: openingPos.column },
745
+ usageIdxStart,
746
+ usageIdxEnd,
747
+ });
748
+ }
749
+ }
750
+ else {
751
+ // Couldn't decompose JSX name (namespaced name etc.) — fall back to
752
+ // ordinary walk without registration.
753
+ walkJsxIn(init, fb, source, null, captureValues);
754
+ }
755
+ }
756
+ else {
757
+ // JSXFragment or other non-JSXElement init — no root symbol to track.
758
+ walkJsxIn(init, fb, source, null, captureValues);
759
+ }
760
+ }
761
+ else {
762
+ // Top-level non-JSX init or non-module-scope — walk with null owner (existing behavior).
763
+ walkJsxIn(init, fb, source, null, captureValues);
764
+ }
765
+ }
766
+ }
767
+ }
768
+ function emitFunctionDeclaration(node, fb, source, captureValues) {
769
+ if (!node.id)
770
+ return;
771
+ const loc = positionAt(source, node.start);
772
+ const enclosingBinding = { symbol: node.id.name, file: fb.filePath };
773
+ fb.addDeclaration({
774
+ symbol: node.id.name,
775
+ value: {
776
+ kind: "Function",
777
+ returns: extractFunctionReturns(node.body, fb.currentScope(), fb.filePath, enclosingBinding),
778
+ enclosingBinding,
779
+ },
780
+ loc,
781
+ isExported: false,
782
+ });
783
+ if (node.body) {
784
+ const ownerRef = { symbol: node.id.name, scope: MODULE_SCOPE, memberChain: [], loc, originFile: fb.filePath };
785
+ const fnRef = { symbol: node.id.name, scope: MODULE_SCOPE, memberChain: [], loc, originFile: fb.filePath };
786
+ fb.pushScope();
787
+ try {
788
+ emitParameterBindings(node.params, fnRef, fb, source);
789
+ walkJsxIn(node.body, fb, source, ownerRef, captureValues);
790
+ walkBodyCalls(node.body, fb, source, node.id.name);
791
+ }
792
+ finally {
793
+ fb.popScope();
794
+ }
795
+ }
796
+ }
797
+ function emitClassDeclaration(node, fb, source, captureValues) {
798
+ if (!node.id)
799
+ return;
800
+ const loc = positionAt(source, node.start);
801
+ const renderReturns = extractClassRenderReturns(node, fb.currentScope(), fb.filePath);
802
+ fb.addDeclaration({
803
+ symbol: node.id.name,
804
+ value: {
805
+ kind: "Function",
806
+ returns: renderReturns ?? [{ kind: "JSX" }],
807
+ },
808
+ loc,
809
+ isExported: false,
810
+ });
811
+ const ownerRef = { symbol: node.id.name, scope: MODULE_SCOPE, memberChain: [], loc, originFile: fb.filePath };
812
+ fb.pushScope();
813
+ try {
814
+ walkJsxIn(node, fb, source, ownerRef, captureValues);
815
+ }
816
+ finally {
817
+ fb.popScope();
818
+ }
819
+ }
820
+ // ---------------------------------------------------------------------------
821
+ // Export emission
822
+ // ---------------------------------------------------------------------------
823
+ function emitExportNamed(node, fb, source, captureValues, emitState) {
824
+ const decl = node.declaration;
825
+ if (decl) {
826
+ // `export const Foo = ...` / `export function Foo() {}` / `export class Foo {}`
827
+ let symbolName = null;
828
+ if (decl.type === "VariableDeclaration") {
829
+ emitVariableDeclaration(decl, fb, source, captureValues, emitState);
830
+ // Collect symbol names from declarators to mark as exported
831
+ for (const d of decl.declarations) {
832
+ if (d.id.type === "Identifier") {
833
+ const name = d.id.name;
834
+ // Re-emit with isExported: true (addDeclaration is upsert by key)
835
+ emitDeclarationFromDeclarator(d, fb, source, { symbol: name, file: fb.filePath }, true);
836
+ fb.addExport({ kind: "named", exportedAs: name, local: name });
837
+ }
838
+ }
839
+ return;
840
+ }
841
+ if (decl.type === "FunctionDeclaration" ||
842
+ decl.type === "TSDeclareFunction") {
843
+ const fnNode = decl;
844
+ emitFunctionDeclaration(fnNode, fb, source, captureValues);
845
+ symbolName = fnNode.id?.name ?? null;
846
+ }
847
+ else if (decl.type === "ClassDeclaration" ||
848
+ decl.type === "ClassExpression") {
849
+ const classNode = decl;
850
+ emitClassDeclaration(classNode, fb, source, captureValues);
851
+ symbolName = classNode.id?.name ?? null;
852
+ }
853
+ if (symbolName) {
854
+ // Re-emit declaration with isExported: true
855
+ const loc = positionAt(source, decl.start);
856
+ if (decl.type === "FunctionDeclaration" || decl.type === "TSDeclareFunction") {
857
+ const fnNode = decl;
858
+ const enclosingBinding = { symbol: symbolName, file: fb.filePath };
859
+ fb.addDeclaration({
860
+ symbol: symbolName,
861
+ value: {
862
+ kind: "Function",
863
+ returns: extractFunctionReturns(fnNode.body, fb.currentScope(), fb.filePath, enclosingBinding),
864
+ enclosingBinding,
865
+ },
866
+ loc,
867
+ isExported: true,
868
+ });
869
+ }
870
+ else if (decl.type === "ClassDeclaration" || decl.type === "ClassExpression") {
871
+ const classNode = decl;
872
+ const renderReturns = extractClassRenderReturns(classNode, fb.currentScope(), fb.filePath);
873
+ fb.addDeclaration({
874
+ symbol: symbolName,
875
+ value: {
876
+ kind: "Function",
877
+ returns: renderReturns ?? [{ kind: "JSX" }],
878
+ },
879
+ loc,
880
+ isExported: true,
881
+ });
882
+ }
883
+ fb.addExport({ kind: "named", exportedAs: symbolName, local: symbolName });
884
+ }
885
+ return;
886
+ }
887
+ // `export { Foo }` or `export { Foo as Bar }` or `export { Foo as Bar } from "./x"`
888
+ const from = node.source?.value ?? null;
889
+ for (const spec of node.specifiers) {
890
+ const localName = spec.local.type === "Identifier"
891
+ ? spec.local.name
892
+ : String(spec.local.value);
893
+ const exportedName = spec.exported.type === "Identifier"
894
+ ? spec.exported.name
895
+ : String(spec.exported.value);
896
+ if (from) {
897
+ fb.addExport({ kind: "named", exportedAs: exportedName, from, fromImported: localName });
898
+ }
899
+ else {
900
+ fb.addExport({ kind: "named", exportedAs: exportedName, local: localName });
901
+ }
902
+ }
903
+ }
904
+ function emitExportDefault(node, fb, source, captureValues) {
905
+ const decl = node.declaration;
906
+ if (decl.type === "Identifier") {
907
+ // `export default Foo` — re-export of a local binding.
908
+ const local = decl.name;
909
+ fb.addExport({ kind: "default", local });
910
+ }
911
+ else if (decl.type === "FunctionDeclaration" ||
912
+ decl.type === "TSDeclareFunction") {
913
+ // `export default function App() { ... }` — named or anonymous.
914
+ const fnNode = decl;
915
+ if (fnNode.id) {
916
+ // Named: treat as a regular function declaration and record the default export.
917
+ emitFunctionDeclaration(fnNode, fb, source, captureValues);
918
+ fb.addExport({ kind: "default", local: fnNode.id.name });
919
+ }
920
+ else {
921
+ // Anonymous default export function — attribute JSX to the synthetic "default" symbol.
922
+ const loc = positionAt(source, node.start);
923
+ const enclosingBinding = { symbol: "default", file: fb.filePath };
924
+ fb.addDeclaration({
925
+ symbol: "default",
926
+ value: {
927
+ kind: "Function",
928
+ returns: extractFunctionReturns(fnNode.body, fb.currentScope(), fb.filePath, enclosingBinding),
929
+ enclosingBinding,
930
+ },
931
+ loc,
932
+ isExported: true,
933
+ });
934
+ fb.addExport({ kind: "default", local: "default" });
935
+ const ownerRef = { symbol: "default", scope: MODULE_SCOPE, memberChain: [], loc, originFile: fb.filePath };
936
+ if (fnNode.body) {
937
+ const fnRef = { symbol: "default", scope: MODULE_SCOPE, memberChain: [], loc, originFile: fb.filePath };
938
+ fb.pushScope();
939
+ try {
940
+ emitParameterBindings(fnNode.params, fnRef, fb, source);
941
+ walkJsxIn(fnNode.body, fb, source, ownerRef, captureValues);
942
+ walkBodyCalls(fnNode.body, fb, source, "default");
943
+ }
944
+ finally {
945
+ fb.popScope();
946
+ }
947
+ }
948
+ }
949
+ }
950
+ else if (decl.type === "ClassDeclaration" ||
951
+ decl.type === "ClassExpression") {
952
+ // `export default class App { ... }` — named or anonymous.
953
+ const classNode = decl;
954
+ if (classNode.id) {
955
+ emitClassDeclaration(classNode, fb, source, captureValues);
956
+ fb.addExport({ kind: "default", local: classNode.id.name });
957
+ }
958
+ else {
959
+ fb.pushScope();
960
+ try {
961
+ walkJsxIn(classNode, fb, source, null, captureValues);
962
+ }
963
+ finally {
964
+ fb.popScope();
965
+ }
966
+ }
967
+ }
968
+ else if (decl.type === "CallExpression") {
969
+ // `export default withFallback(InnerComponent)` — HOC applied at the
970
+ // default export site. Mirror the inline form (`const X = withFallback(Y)`
971
+ // in emitVariableDeclaration above): synthesise a "default" declaration
972
+ // whose value carries the ReturnTypeOf shape so the engine's wrapper-
973
+ // folding collapses to the inner-arg identity.
974
+ const loc = positionAt(source, decl.start);
975
+ fb.addDeclaration({
976
+ symbol: "default",
977
+ value: inferValue(decl, fb.currentScope(), fb.filePath, { symbol: "default", file: fb.filePath }),
978
+ loc,
979
+ isExported: true,
980
+ });
981
+ fb.addExport({ kind: "default", local: "default" });
982
+ // Walk JSX inside an anonymous inner arg (e.g. `withFallback(() => <Foo />)`)
983
+ // with "default" as the owner so nested JSX is attributed correctly.
984
+ const call = decl;
985
+ const firstArg = call.arguments[0];
986
+ if (firstArg &&
987
+ (firstArg.type === "ArrowFunctionExpression" || firstArg.type === "FunctionExpression")) {
988
+ const fnLike = firstArg;
989
+ const body = fnLike.body;
990
+ const ownerRef = { symbol: "default", scope: MODULE_SCOPE, memberChain: [], loc, originFile: fb.filePath };
991
+ const fnRef = { symbol: "default", scope: MODULE_SCOPE, memberChain: [], loc, originFile: fb.filePath };
992
+ fb.pushScope();
993
+ try {
994
+ emitParameterBindings(fnLike.params, fnRef, fb, source);
995
+ walkJsxIn(body, fb, source, ownerRef, captureValues);
996
+ walkBodyCalls(body, fb, source, "default");
997
+ }
998
+ finally {
999
+ fb.popScope();
1000
+ }
1001
+ }
1002
+ }
1003
+ // Other default export shapes (object literals, arrow functions at top level,
1004
+ // etc.) are skipped — no stable name to key ownership on.
1005
+ }
1006
+ function emitExportAll(node, fb) {
1007
+ fb.addExport({ kind: "star", from: node.source.value });
1008
+ }
1009
+ //# sourceMappingURL=emit.js.map