@component-compass/reference-graph 0.1.23 → 0.1.24

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.
@@ -2,6 +2,7 @@ import type { Graph, FileGraph, InferredType } from "../index.js";
2
2
  import type { OccurrenceVia } from "@component-compass/plugin-core";
3
3
  import type { ArgumentMap } from "./argument-map.js";
4
4
  import { type CycleGuard } from "./cycle-detection.js";
5
+ import { type Evaluation, type Terminal } from "./denotation.js";
5
6
  /**
6
7
  * Identity hint for a folded terminal. Tells the engine which binding produced
7
8
  * this terminal so componentId derives from the folded-to-inner identity.
@@ -25,17 +26,6 @@ export type TerminalIdentity = {
25
26
  filePath: string;
26
27
  export: string;
27
28
  } | null;
28
- /**
29
- * Terminal returned by `walkWithFolding`. Each terminal carries an inner viaTrail
30
- * accumulated through any folding rules that fired along the walk, plus an identity
31
- * hint for componentId derivation. The engine's outer occurrence-builder prepends
32
- * the outer via (direct-import/local-component/etc.) to form the full viaChain.
33
- */
34
- export type FoldedTerminal = {
35
- type: InferredType;
36
- viaTrail: OccurrenceVia[];
37
- identity: TerminalIdentity;
38
- };
39
29
  /**
40
30
  * Provenance of ONE argument of a wrapper call: the wrapper's `callee` name
41
31
  * and the `{ specifier, import }` pair naming the argument's own binding —
@@ -64,7 +54,7 @@ export declare function argumentProvenance(type: Extract<InferredType, {
64
54
  */
65
55
  export declare function foldedArgument(graph: Graph, fileGraph: FileGraph, call: Extract<InferredType, {
66
56
  kind: "ReturnTypeOf";
67
- }>, jsx: FoldedTerminal[], argMap: ArgumentMap): {
57
+ }>, jsx: readonly Terminal[], argMap: ArgumentMap): {
68
58
  arg: InferredType;
69
59
  via: OccurrenceVia;
70
60
  } | null;
@@ -78,7 +68,7 @@ export declare function foldedArgument(graph: Graph, fileGraph: FileGraph, call:
78
68
  * (pure algebra) and computes identity from the input type's leaf TypeOf
79
69
  * when one can be confidently identified.
80
70
  */
81
- export declare function walkWithFolding(graph: Graph, fileGraph: FileGraph, type: InferredType, argMap: ArgumentMap, guard?: CycleGuard, identityFallback?: InferredType): FoldedTerminal[];
71
+ export declare function walkWithFolding(graph: Graph, fileGraph: FileGraph, type: InferredType, argMap: ArgumentMap, guard?: CycleGuard, identityFallback?: InferredType): Evaluation;
82
72
  /**
83
73
  * The file a DynamicImport's specifier resolves against: the file that
84
74
  * contains the `import()`, whichever file the walk is standing in.
@@ -1,12 +1,15 @@
1
1
  import { resolveType, resolveToFunctions, resolveDynamicImportTarget, DYNAMIC_MEMBER_KEY } from "./resolve-type.js";
2
- import { argumentRefKey } from "./argument-map.js";
2
+ import { argumentRefKey, boundArgument } from "./argument-map.js";
3
3
  import { createCycleGuard } from "./cycle-detection.js";
4
4
  import { fileGraphForRef, resolveReference } from "./resolve-reference.js";
5
5
  import { packageNameFromSpecifier } from "./specifier.js";
6
6
  import { followReExportChain } from "./re-export-chain.js";
7
7
  import { unparsedMemberDefinition, effectiveExportName, residualMemberChain, compoundExportName } from "./member-identity.js";
8
8
  import { evalKind, isImportBackedLeaf } from "./component-shape.js";
9
+ import { HOOK_NAME } from "./host-element.js";
9
10
  import { assertNever } from "./assert-never.js";
11
+ import { creditedTerminals, matchDenotation } from "./denotation.js";
12
+ import { libraryExportFor } from "./library-stubs.js";
10
13
  /** True when a `ReturnTypeOf`'s callee is a data-method render
11
14
  * call (`items.map(render)`) rather than a wrapper taking a component as a
12
15
  * prop — the exact structural condition `component-shape.ts`'s rule (ii)
@@ -16,13 +19,146 @@ import { assertNever } from "./assert-never.js";
16
19
  function isDataMethodRenderCall(type, graph, fileGraph, guard) {
17
20
  return type.callee.kind === "MemberOf" && !isImportBackedLeaf(type.callee, graph, fileGraph, guard);
18
21
  }
22
+ /** The denotation of a branch the walk learned nothing about. */
23
+ const INDETERMINATE = { kind: "indeterminate" };
24
+ function withSemantics(t, semantics) {
25
+ return { denotation: t.denotation, viaTrail: t.viaTrail, identity: t.identity, semantics };
26
+ }
27
+ /** The product every branch of an evaluation is marked with; null when any
28
+ * branch is unmarked or marked differently. */
29
+ function markedProduct(terminals) {
30
+ const first = terminals[0]?.semantics;
31
+ if (first === undefined)
32
+ return null;
33
+ return terminals.every((t) => t.semantics === first) ? first : null;
34
+ }
35
+ /** THE in-walker replacement for `t.type.kind === "Unknown"`, stated once and
36
+ * read only through `matchDenotation`: "this branch came to nothing".
37
+ *
38
+ * `late-bound` answers `true`: the values it classifies reduce to `Unknown` in
39
+ * the algebra. `opaque` is in the record for exhaustiveness only — the walk
40
+ * mints it at the public boundary and never reads it back, so no internal
41
+ * path can observe that handler. */
42
+ const CAME_TO_NOTHING = {
43
+ element: () => false,
44
+ foreign: () => false,
45
+ "host-string": () => false,
46
+ opaque: () => true,
47
+ "late-bound": () => true,
48
+ data: () => false,
49
+ indeterminate: () => true,
50
+ };
51
+ function cameToNothing(t) {
52
+ return matchDenotation(t, CAME_TO_NOTHING);
53
+ }
54
+ const IS_LATE_BOUND = {
55
+ element: () => false,
56
+ foreign: () => false,
57
+ "host-string": () => false,
58
+ opaque: () => false,
59
+ "late-bound": () => true,
60
+ data: () => false,
61
+ indeterminate: () => false,
62
+ };
63
+ /**
64
+ * Rename a branch that came to nothing, keeping its identity and viaTrail
65
+ * exactly as the fold produced them, and dropping any product mark: the new
66
+ * denotation is what the branch is now. A branch that reduced to something the
67
+ * walk does know keeps its own denotation and its mark.
68
+ */
69
+ function relabel(denotation) {
70
+ return (t) => (cameToNothing(t) ? { denotation, viaTrail: t.viaTrail, identity: t.identity } : t);
71
+ }
72
+ function asLateBound(source) {
73
+ return relabel({ kind: "late-bound", source });
74
+ }
75
+ /** True when every branch of an evaluation is a value supplied elsewhere. */
76
+ function evaluatesLateBound(terminals) {
77
+ return terminals.length > 0 && terminals.every((t) => matchDenotation(t, IS_LATE_BOUND));
78
+ }
79
+ /**
80
+ * THE boundary between "what this value IS" and "what this function returns".
81
+ *
82
+ * A string becomes the data the function returns; a late-bound value becomes
83
+ * "nothing known"; a product mark is dropped, so a function that returns a
84
+ * context is not itself a context. `opaque` is in the record for
85
+ * exhaustiveness only — no internal path produces that denotation.
86
+ *
87
+ * Stated once and applied at exactly two places: the `Function` arm of
88
+ * `walkInner` and the exit of `walkReturnTypeOf`.
89
+ */
90
+ const AS_RETURNED_VALUE = {
91
+ element: (d) => d,
92
+ foreign: (d) => d,
93
+ "host-string": () => ({ kind: "data", shape: "string" }),
94
+ opaque: () => INDETERMINATE,
95
+ "late-bound": () => INDETERMINATE,
96
+ data: (d) => d,
97
+ indeterminate: (d) => d,
98
+ };
99
+ function returnedValue(t) {
100
+ const denotation = matchDenotation(t, AS_RETURNED_VALUE);
101
+ return denotation === t.denotation && t.semantics === undefined
102
+ ? t
103
+ : { denotation, viaTrail: t.viaTrail, identity: t.identity };
104
+ }
105
+ function indeterminateTerminal() {
106
+ return { denotation: INDETERMINATE, viaTrail: [], identity: null };
107
+ }
108
+ /** THE one place a product mark becomes a denotation. A marked branch the tag
109
+ * would credit keeps the denotation it earned; any other marked branch IS the
110
+ * product. */
111
+ function published(t) {
112
+ if (t.semantics === undefined)
113
+ return t;
114
+ const denotation = creditedTerminals([t]).length > 0 ? t.denotation : { kind: "opaque", semantics: t.semantics };
115
+ return { denotation, viaTrail: t.viaTrail, identity: t.identity };
116
+ }
117
+ /** The public boundary's guarantee, and only the public boundary's: an
118
+ * `Evaluation` handed to a caller is never empty, because a caller has
119
+ * nothing to read otherwise. Inside the walk `[]` stays `[]`. */
120
+ function atLeastOne(terminals) {
121
+ return terminals.length > 0 ? terminals : [indeterminateTerminal()];
122
+ }
123
+ /**
124
+ * What one `resolveType` terminal denotes. `synthesised` marks the unreducible
125
+ * import-backed leaf `foldTerminals` mints an identity for: the reference is
126
+ * real and its target unparseable, so the value is `foreign`. `indeterminate`
127
+ * is reserved for the terminal the algebra bottomed out at knowing nothing —
128
+ * a value the algebra did reduce, to a string or to data, is not that.
129
+ */
130
+ function denote(type, synthesised) {
131
+ if (synthesised)
132
+ return { kind: "foreign" };
133
+ switch (type.kind) {
134
+ case "JSX":
135
+ return { kind: "element" };
136
+ case "Str":
137
+ return { kind: "host-string", value: type.value };
138
+ case "Object":
139
+ return { kind: "data", shape: "object" };
140
+ case "Array":
141
+ return { kind: "data", shape: "array" };
142
+ case "TypeOf":
143
+ case "Function":
144
+ case "ReturnTypeOf":
145
+ case "ParameterOf":
146
+ case "MemberOf":
147
+ case "Union":
148
+ case "Unknown":
149
+ case "DynamicImport":
150
+ return INDETERMINATE;
151
+ default:
152
+ return assertNever(type);
153
+ }
154
+ }
19
155
  /**
20
156
  * THE single seam for unreducible-leaf identity synthesis.
21
157
  *
22
- * Maps `resolveType` results into FoldedTerminals. When the algebra preserved
158
+ * Maps `resolveType` results into Terminals. When the algebra preserved
23
159
  * an identity-bearing leaf — an import-backed TypeOf or a DynamicImport that
24
160
  * couldn't be reduced (see resolve-type.ts's TypeOf/DynamicImport cases) —
25
- * synthesize a JSX terminal carrying the identity derived FROM THE TERMINAL
161
+ * synthesize a `foreign` terminal carrying the identity derived FROM THE TERMINAL
26
162
  * ITSELF: the reference is real even though its target can't be parsed
27
163
  * (external package, CJS, unresolvable specifier). Every other terminal
28
164
  * derives identity from `source`, falling back to `fallback` only when the
@@ -36,7 +172,7 @@ function foldTerminals(graph, fileGraph, results, fallback) {
36
172
  if (t.kind === "TypeOf" || t.kind === "DynamicImport") {
37
173
  const identity = deriveIdentity(t, fileGraph, graph);
38
174
  if (identity !== null) {
39
- return { type: { kind: "JSX" }, viaTrail: [], identity };
175
+ return { denotation: denote(t, true), viaTrail: [], identity };
40
176
  }
41
177
  }
42
178
  // A fan-out branch (Union / dynamic-map) is stamped as `source`. Derive
@@ -46,7 +182,7 @@ function foldTerminals(graph, fileGraph, results, fallback) {
46
182
  // ternary/logical render body).
47
183
  const identity = (source && deriveIdentity(source, fileGraph, graph)) ?? deriveIdentity(fallback, fileGraph, graph);
48
184
  return {
49
- type: t,
185
+ denotation: denote(t, false),
50
186
  viaTrail: [],
51
187
  identity,
52
188
  };
@@ -58,22 +194,24 @@ function foldTerminals(graph, fileGraph, results, fallback) {
58
194
  * a seen-through wrapper and an opaque one attribute identically. Inner
59
195
  * terminals pass through with the wrapper hop prepended. When the inner walk
60
196
  * bottomed out at Unknown but carried an identity hint (imported, or
61
- * local-promoted through the re-export chain), synthesise a JSX terminal
197
+ * local-promoted through the re-export chain), synthesise a `foreign` terminal
62
198
  * from it — without this
63
199
  * rescue, HOC-wrapped occurrences drop silently (~128 in example-web's
64
200
  * connect/flow/withTranslators patterns). Truly
65
201
  * nothing → a single Unknown terminal.
66
202
  */
67
203
  function foldToWrappedArg(inner, via) {
68
- const innerHasNonUnknown = inner.some((t) => t.type.kind !== "Unknown");
204
+ // Drops the product mark: what a wrapper call returns is not the wrapped
205
+ // value, so `memo(Ctx)` is not a context.
206
+ const innerHasNonUnknown = inner.some((t) => !cameToNothing(t));
69
207
  if (innerHasNonUnknown) {
70
- return inner.map((t) => ({ type: t.type, viaTrail: [via, ...t.viaTrail], identity: t.identity }));
208
+ return inner.map((t) => ({ denotation: t.denotation, viaTrail: [via, ...t.viaTrail], identity: t.identity }));
71
209
  }
72
210
  const synthesisIdentity = inner.find((t) => t.identity !== null)?.identity ?? null;
73
211
  if (synthesisIdentity) {
74
- return [{ type: { kind: "JSX" }, viaTrail: [via], identity: synthesisIdentity }];
212
+ return [{ denotation: { kind: "foreign" }, viaTrail: [via], identity: synthesisIdentity }];
75
213
  }
76
- return [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
214
+ return [indeterminateTerminal()];
77
215
  }
78
216
  /**
79
217
  * Provenance of ONE argument of a wrapper call: the wrapper's `callee` name
@@ -150,8 +288,8 @@ function identityKey(id) {
150
288
  * returns a DIFFERENT component fails this and keeps its richer algebra result.
151
289
  */
152
290
  function isPassThroughWrapper(algResult, inner) {
153
- const innerKeys = new Set(inner.filter((t) => t.type.kind !== "Unknown" && t.identity !== null).map((t) => identityKey(t.identity)));
154
- const algKeys = algResult.filter((t) => t.type.kind !== "Unknown");
291
+ const innerKeys = new Set(inner.filter((t) => !cameToNothing(t) && t.identity !== null).map((t) => identityKey(t.identity)));
292
+ const algKeys = algResult.filter((t) => !cameToNothing(t));
155
293
  // A null identity on either side is "no information", never a match:
156
294
  // null-equals-null folds a factory call onto its rehydrate/config argument.
157
295
  return algKeys.length > 0 && algKeys.every((t) => t.identity !== null && innerKeys.has(identityKey(t.identity)));
@@ -166,7 +304,16 @@ function isPassThroughWrapper(algResult, inner) {
166
304
  * (pure algebra) and computes identity from the input type's leaf TypeOf
167
305
  * when one can be confidently identified.
168
306
  */
169
- export function walkWithFolding(graph, fileGraph, type, argMap, guard = createCycleGuard(),
307
+ export function walkWithFolding(graph, fileGraph, type, argMap, guard = createCycleGuard(), identityFallback) {
308
+ return atLeastOne(walkInner(graph, fileGraph, type, argMap, guard, identityFallback).map(published));
309
+ }
310
+ /**
311
+ * The walk itself. Returns exactly what each arm produced, empty included:
312
+ * inside the walk an empty result means "this branch contributed nothing",
313
+ * which `walkTypeOf`'s vacuous `every` and `foldToWrappedArg`'s rescue both
314
+ * read. Only the exported boundary above may substitute a terminal for it.
315
+ */
316
+ function walkInner(graph, fileGraph, type, argMap, guard,
170
317
  // The identity `foldTerminals` falls back to when a terminal
171
318
  // carries no identity of its own (no `source`, no unreducible TypeOf/
172
319
  // DynamicImport leaf). Defaults to `type` (the walked node itself). The
@@ -200,7 +347,9 @@ identityFallback) {
200
347
  // Function as the identity fallback for every return — a
201
348
  // Function is never TypeOf/DynamicImport, so `deriveIdentity` on it
202
349
  // is always null.
203
- return type.returns.flatMap((r) => walkWithFolding(graph, fileGraph, r, argMap, guard, type));
350
+ return type.returns
351
+ .flatMap((r) => walkInner(graph, fileGraph, r, argMap, guard, type))
352
+ .map(returnedValue);
204
353
  case "TypeOf":
205
354
  return walkTypeOf(graph, fileGraph, type, argMap, guard, fallback);
206
355
  case "Union":
@@ -208,8 +357,9 @@ identityFallback) {
208
357
  // a TypeOf branch reaches `walkTypeOf` and decides its own identity
209
358
  // (per-branch attribution) while a bare JSX branch — a lowered
210
359
  // ternary/logical render body — derives from the outer fallback.
211
- return type.types.flatMap((branch) => walkWithFolding(graph, fileGraph, branch, argMap, guard, fallback));
360
+ return type.types.flatMap((branch) => walkInner(graph, fileGraph, branch, argMap, guard, fallback));
212
361
  case "ParameterOf":
362
+ return walkParameter(graph, fileGraph, type, argMap, guard, fallback);
213
363
  case "DynamicImport":
214
364
  case "Object":
215
365
  case "Array":
@@ -221,12 +371,25 @@ identityFallback) {
221
371
  return assertNever(type);
222
372
  }
223
373
  }
374
+ /**
375
+ * A parameter. Bound by the call the walk is inside — `resolveType`
376
+ * substitutes the argument and the walk continues into it, exactly as before.
377
+ * Unbound, the value is supplied by whoever calls this function later, which
378
+ * is a different fact from "nothing is known": the algebra flattens both to
379
+ * `Unknown`, and only the render site acts on the difference.
380
+ */
381
+ function walkParameter(graph, fileGraph, type, argMap, guard, fallback) {
382
+ const leaf = foldLeaf(graph, fileGraph, type, argMap, guard, fallback);
383
+ if (boundArgument(argMap, type) !== undefined)
384
+ return leaf;
385
+ return leaf.map(asLateBound("parameter"));
386
+ }
224
387
  /**
225
388
  * The leaf fold: pure algebra, then identity synthesis through the ONE
226
389
  * unreducible-leaf seam (`foldTerminals`). This is the only place the walker
227
390
  * hands a subtree to `resolveType`; every structural kind (TypeOf, Union,
228
391
  * MemberOf, Function, ReturnTypeOf) has its own arm above and re-enters
229
- * `walkWithFolding` instead.
392
+ * `walkInner` instead.
230
393
  */
231
394
  function foldLeaf(graph, fileGraph, type, argMap, guard, fallback) {
232
395
  return foldTerminals(graph, fileGraph, resolveType(graph, fileGraph, type, argMap, guard), fallback);
@@ -282,7 +445,7 @@ export function dynamicImportIdentity(graph, fileGraph, di) {
282
445
  }
283
446
  /**
284
447
  * Structural re-entry for a reference. Resolves ONE
285
- * hop and walks the resolved value through `walkWithFolding`, so every walker
448
+ * hop and walks the resolved value through `walkInner`, so every walker
286
449
  * rule — wrapper hops, dynamic maps, `.map()` bodies — applies
287
450
  * through an alias exactly as at a direct render site. Without this arm the
288
451
  * default fold hands the whole subtree to `resolveType`, which never comes
@@ -309,7 +472,7 @@ export function dynamicImportIdentity(graph, fileGraph, di) {
309
472
  */
310
473
  function walkTypeOf(graph, fileGraph, type, argMap, guard, fallback) {
311
474
  if (guard.pushNode(type) === "cycle") {
312
- return [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
475
+ return [indeterminateTerminal()];
313
476
  }
314
477
  try {
315
478
  const resolved = resolveReference(graph, fileGraph, type.ref, guard);
@@ -323,14 +486,15 @@ function walkTypeOf(graph, fileGraph, type, argMap, guard, fallback) {
323
486
  return foldLeaf(graph, fileGraph, type, argMap, guard, fallback);
324
487
  }
325
488
  const innerGraph = resolved.kind === "Function" ? fileGraphForFunction(graph, fileGraph, resolved) : fileGraph;
326
- const inner = walkWithFolding(graph, innerGraph, resolved, argMap, guard, fallback);
489
+ const inner = walkInner(graph, innerGraph, resolved, argMap, guard, fallback);
327
490
  if (inner.every((t) => t.identity !== null))
328
491
  return inner;
329
492
  const self = resolved.kind !== "ParameterOf" && evalKind(resolved, graph, innerGraph, argMap) === "component"
330
493
  ? deriveIdentity(type, fileGraph, graph)
331
494
  : null;
332
495
  const stamp = self ?? deriveIdentity(fallback, fileGraph, graph);
333
- return inner.map((t) => (t.identity === null ? { type: t.type, viaTrail: t.viaTrail, identity: stamp } : t));
496
+ // Carries the product mark: a reference to a context IS that context.
497
+ return inner.map((t) => t.identity === null ? { denotation: t.denotation, viaTrail: t.viaTrail, identity: stamp, semantics: t.semantics } : t);
334
498
  }
335
499
  finally {
336
500
  guard.popNode(type);
@@ -339,7 +503,7 @@ function walkTypeOf(graph, fileGraph, type, argMap, guard, fallback) {
339
503
  /**
340
504
  * Static member access. Mirrors `walkDynamicMember` for one
341
505
  * named prop and without the `dynamic-map` hop: resolve the object, take the
342
- * member, walk the member's VALUE through `walkWithFolding` — so a wrapper
506
+ * member, walk the member's VALUE through `walkInner` — so a wrapper
343
507
  * product stored in a map (`MAP.memo` → `memo(Button)`) reaches
344
508
  * `walkReturnTypeOf` and keeps its `hoc-wrapper` hop. Anything that is not an
345
509
  * in-graph Object (an Array, an import-backed namespace the graph cannot see)
@@ -352,7 +516,7 @@ function walkTypeOf(graph, fileGraph, type, argMap, guard, fallback) {
352
516
  */
353
517
  function walkStaticMember(graph, fileGraph, type, argMap, guard, fallback) {
354
518
  if (guard.pushNode(type) === "cycle") {
355
- return [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
519
+ return [indeterminateTerminal()];
356
520
  }
357
521
  try {
358
522
  const objects = resolveType(graph, fileGraph, type.obj, argMap, guard)
@@ -365,22 +529,87 @@ function walkStaticMember(graph, fileGraph, type, argMap, guard, fallback) {
365
529
  // identity), not just on real cycles. The `finally` below still runs
366
530
  // (a no-op double pop) once this returns.
367
531
  guard.popNode(type);
368
- return foldLeaf(graph, fileGraph, type, argMap, guard, fallback);
532
+ const leaf = foldLeaf(graph, fileGraph, type, argMap, guard, fallback);
533
+ if (!(leaf.length > 0 && leaf.every(cameToNothing)))
534
+ return leaf;
535
+ // The object answers for the member: a prop read off a value supplied
536
+ // elsewhere (`props.Icon`, `useThing()[0]`) is supplied elsewhere too,
537
+ // and any member of a non-component product is that product. ONE walk
538
+ // answers both, and only once the member came to nothing, so a member
539
+ // that resolved is never re-walked.
540
+ const object = walkInner(graph, fileGraph, type.obj, argMap, guard);
541
+ const semantics = markedProduct(object);
542
+ if (semantics !== null)
543
+ return leaf.map((t) => withSemantics(t, semantics));
544
+ return evaluatesLateBound(object) ? leaf.map(asLateBound("parameter-member")) : leaf;
369
545
  }
370
546
  const out = [];
371
547
  for (const obj of objects) {
372
548
  const value = obj.props[type.member];
373
549
  if (value === undefined)
374
550
  continue;
375
- out.push(...walkWithFolding(graph, fileGraph, value, argMap, guard, fallback));
551
+ out.push(...walkInner(graph, fileGraph, value, argMap, guard, fallback));
376
552
  }
377
- return out.length > 0 ? out : [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
553
+ return out.length > 0 ? out : [indeterminateTerminal()];
378
554
  }
379
555
  finally {
380
556
  guard.popNode(type);
381
557
  }
382
558
  }
559
+ /**
560
+ * A call. Every folding rule runs first (`foldCall`); only when all of them
561
+ * declined — no argument credited, no algebra result — does the call's own
562
+ * shape get a name. A call of a hook the graph cannot see returns a value
563
+ * bound at render time by the hook's own implementation, so it is late-bound
564
+ * rather than unknown. The predicate is React's `HOOK_NAME` over a callee
565
+ * `isImportBackedLeaf` says the graph cannot resolve; it runs after the folds,
566
+ * so `const X = useStyled(Base)` keeps the credit its fold gave it.
567
+ *
568
+ * A call of an export the stub table names a non-component product carries
569
+ * that product's mark on the terminals the folds produced, and only when the
570
+ * folds credited nothing.
571
+ *
572
+ * ORDER IS LOAD-BEARING: `returnedValue` runs on the fold's results before
573
+ * `late-bound{hook-result}` is minted for the call itself.
574
+ */
383
575
  function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
576
+ const returns = foldCall(graph, fileGraph, type, argMap, guard).map(returnedValue);
577
+ if (creditedTerminals(returns).length === 0) {
578
+ const semantics = nonComponentProduct(graph, fileGraph, type.callee, guard);
579
+ if (semantics !== null)
580
+ return returns.map((t) => withSemantics(t, semantics));
581
+ }
582
+ if (!(returns.length > 0 && returns.every(cameToNothing)))
583
+ return returns;
584
+ const unseenHook = HOOK_NAME.test(deriveHocCallee(type.callee)) && isImportBackedLeaf(type.callee, graph, fileGraph, guard);
585
+ return unseenHook ? returns.map(asLateBound("hook-result")) : returns;
586
+ }
587
+ /**
588
+ * The semantics of the non-component value a call produces: the stub table's
589
+ * entry for the callee's export, when the callee is an import the graph cannot
590
+ * see (`isImportBackedLeaf`, the same predicate the unseen-hook rule uses).
591
+ * Null for every other call.
592
+ *
593
+ * The callee's spelling is read through `libraryExportFor`, the one table
594
+ * `libraryStubFor` reads: a reference (a named import, or a default /
595
+ * namespace import carrying the member on the ref) or a member access on one
596
+ * (`React.createContext`).
597
+ */
598
+ function nonComponentProduct(graph, fileGraph, callee, guard) {
599
+ const ref = callee.kind === "TypeOf"
600
+ ? callee.ref
601
+ : callee.kind === "MemberOf" && callee.obj.kind === "TypeOf"
602
+ ? { ...callee.obj.ref, memberChain: [callee.member] }
603
+ : null;
604
+ if (ref === null)
605
+ return null;
606
+ const imp = fileGraphForRef(graph, ref, fileGraph).importsByLocal.get(ref.symbol);
607
+ const entry = imp === undefined ? null : libraryExportFor(imp, ref);
608
+ if (entry === null || entry.kind !== "non-component-product")
609
+ return null;
610
+ return isImportBackedLeaf(callee, graph, fileGraph, guard) ? entry.semantics : null;
611
+ }
612
+ function foldCall(graph, fileGraph, type, argMap, guard) {
384
613
  // (1) lazy-import shape: any arg's subtree contains a DynamicImport
385
614
  // (with one-ref-deep peek into local-file bindings to catch the async-await form).
386
615
  const lazyShape = findLazyImportArg(type.args, fileGraph);
@@ -395,14 +624,14 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
395
624
  };
396
625
  const guardKey = `import()::${syntheticDI.specifier}::${syntheticDI.projection.join(".")}`;
397
626
  if (guard.push(syntheticDI.originFile, guardKey) !== "ok") {
398
- return [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
627
+ return [indeterminateTerminal()];
399
628
  }
400
629
  let inner;
401
630
  try {
402
631
  const target = resolveDynamicImportTarget(graph, fileGraph, syntheticDI, guard);
403
632
  inner = target
404
- ? walkWithFolding(graph, target.targetFileGraph, target.value, argMap, guard)
405
- : walkWithFolding(graph, fileGraph, syntheticDI, argMap, guard);
633
+ ? walkInner(graph, target.targetFileGraph, target.value, argMap, guard)
634
+ : walkInner(graph, fileGraph, syntheticDI, argMap, guard);
406
635
  }
407
636
  finally {
408
637
  guard.pop(syntheticDI.originFile, guardKey);
@@ -418,12 +647,12 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
418
647
  // A terminal keeps the identity its own fold produced; the import-derived
419
648
  // identity covers terminals that carry none.
420
649
  const identity = dynamicImportIdentity(graph, fileGraph, syntheticDI);
421
- const innerHasNonUnknown = inner.some((t) => t.type.kind !== "Unknown");
650
+ const innerHasNonUnknown = inner.some((t) => !cameToNothing(t));
422
651
  if (innerHasNonUnknown) {
423
652
  return inner
424
- .filter((t) => t.type.kind !== "Unknown")
653
+ .filter((t) => !cameToNothing(t))
425
654
  .map((t) => ({
426
- type: t.type,
655
+ denotation: t.denotation,
427
656
  viaTrail: [via, ...t.viaTrail],
428
657
  identity: t.identity ?? identity,
429
658
  }));
@@ -431,13 +660,13 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
431
660
  // External package: target not in graph. Also reached when the inner walk
432
661
  // was cut by the import cycle key above or by the depth budget, in which
433
662
  // case the import-derived identity is the answer. We still have a valid
434
- // specifier+projection from lazyShape — synthesize a JSX terminal rather
663
+ // specifier+projection from lazyShape — synthesize a `foreign` terminal rather
435
664
  // than falling through to Unknown.
436
- return [{ type: { kind: "JSX" }, viaTrail: [via], identity }];
665
+ return [{ denotation: { kind: "foreign" }, viaTrail: [via], identity }];
437
666
  }
438
667
  // (2) Dispatcher detection: if the callee resolves to a Function
439
668
  // whose body returns a dynamic-map access (`return MAP[k]`), walk that body
440
- // via walkWithFolding in the function's authoring fileGraph. This lets the
669
+ // via walkInner in the function's authoring fileGraph. This lets the
441
670
  // walkDynamicMember rule fire and surfaces the dynamic-map via in the chain
442
671
  // — without it, cross-module dispatcher patterns would lose the via even
443
672
  // though identity attribution worked.
@@ -457,7 +686,7 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
457
686
  if (passThroughIndex !== null) {
458
687
  const wrapped = type.args[passThroughIndex];
459
688
  if (wrapped !== undefined) {
460
- const inner = walkWithFolding(graph, fileGraph, wrapped, argMap, guard);
689
+ const inner = walkInner(graph, fileGraph, wrapped, argMap, guard);
461
690
  return foldToWrappedArg(inner, buildHocVia(type, wrapped, fileGraph));
462
691
  }
463
692
  }
@@ -466,10 +695,10 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
466
695
  // Union / Array fanouts attribute each branch to its own binding instead
467
696
  // of collapsing every leaf to the outer wrapper's identity.
468
697
  // foldTerminals also converts unreducible identity-bearing leaves into
469
- // synthesized JSX terminals, so a resolvable and an unresolvable imported
698
+ // synthesized `foreign` terminals, so a resolvable and an unresolvable imported
470
699
  // leaf take the same emission path.
471
700
  const algResult = foldTerminals(graph, fileGraph, resolveType(graph, fileGraph, type, argMap, guard), type);
472
- const algHasNonUnknown = algResult.some((r) => r.type.kind !== "Unknown");
701
+ const algHasNonUnknown = algResult.some((r) => !cameToNothing(r));
473
702
  if (algHasNonUnknown) {
474
703
  // (3b) Pass-through HOC recovery. When the algebra reduces this call to the
475
704
  // identity of its wrapped last arg (an identity-preserving `c => c` wrapper
@@ -482,12 +711,12 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
482
711
  // stands with no via.
483
712
  const lastArg = type.args.at(-1);
484
713
  if (lastArg !== undefined) {
485
- const inner = walkWithFolding(graph, fileGraph, lastArg, argMap, guard);
714
+ const inner = walkInner(graph, fileGraph, lastArg, argMap, guard);
486
715
  if (isPassThroughWrapper(algResult, inner)) {
487
716
  const via = buildHocVia(type, lastArg, fileGraph);
488
717
  return inner
489
- .filter((t) => t.type.kind !== "Unknown")
490
- .map((t) => ({ type: t.type, viaTrail: [via, ...t.viaTrail], identity: t.identity }));
718
+ .filter((t) => !cameToNothing(t))
719
+ .map((t) => ({ denotation: t.denotation, viaTrail: [via, ...t.viaTrail], identity: t.identity }));
491
720
  }
492
721
  }
493
722
  return algResult;
@@ -495,9 +724,9 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
495
724
  // (4) HOC fallback: algebra failed, last arg is the wrapper target.
496
725
  const lastArg = type.args.at(-1);
497
726
  if (lastArg === undefined) {
498
- return [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
727
+ return [indeterminateTerminal()];
499
728
  }
500
- const inner = walkWithFolding(graph, fileGraph, lastArg, argMap, guard);
729
+ const inner = walkInner(graph, fileGraph, lastArg, argMap, guard);
501
730
  // A data-method render call (`items.map(render)`) is not a
502
731
  // wrapper — its result IS the callback's rendered JSX, not a component
503
732
  // received as a prop. Return the callback's terminals AS-IS: no
@@ -506,11 +735,11 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
506
735
  // the callback itself happened to resolve to. Import-backed MemberOf
507
736
  // callees (`Sentry.withProfiler`) fall through to the fold+hop below.
508
737
  if (isDataMethodRenderCall(type, graph, fileGraph, guard)) {
509
- const nonUnknown = inner.filter((t) => t.type.kind !== "Unknown");
738
+ const nonUnknown = inner.filter((t) => !cameToNothing(t));
510
739
  if (nonUnknown.length > 0) {
511
- return nonUnknown.map((t) => ({ type: t.type, viaTrail: t.viaTrail, identity: null }));
740
+ return nonUnknown.map((t) => ({ denotation: t.denotation, viaTrail: t.viaTrail, identity: null }));
512
741
  }
513
- return [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
742
+ return [indeterminateTerminal()];
514
743
  }
515
744
  return foldToWrappedArg(inner, buildHocVia(type, lastArg, fileGraph));
516
745
  }
@@ -559,7 +788,7 @@ function passThroughParameterIndex(graph, fileGraph, type, argMap, guard) {
559
788
  * so no `dynamic-map` via would land in the chain.
560
789
  *
561
790
  * This helper detects the shape and walks each function's returns through
562
- * `walkWithFolding` in the function's authoring fileGraph (via
791
+ * `walkInner` in the function's authoring fileGraph (via
563
792
  * `enclosingBinding.file`). Returns null when no callee matches the shape so
564
793
  * the caller falls through to the pure algebra path.
565
794
  */
@@ -577,7 +806,7 @@ function walkDispatcherReturns(graph, fileGraph, type, argMap, guard) {
577
806
  argMap.bind(bindingRef, type.args);
578
807
  try {
579
808
  for (const ret of fn.returns) {
580
- folded.push(...walkWithFolding(graph, fnFileGraph, ret, argMap, guard));
809
+ folded.push(...walkInner(graph, fnFileGraph, ret, argMap, guard));
581
810
  }
582
811
  }
583
812
  finally {
@@ -585,10 +814,10 @@ function walkDispatcherReturns(graph, fileGraph, type, argMap, guard) {
585
814
  argMap.unbind(bindingRef);
586
815
  }
587
816
  }
588
- const hasNonUnknown = folded.some((t) => t.type.kind !== "Unknown");
817
+ const hasNonUnknown = folded.some((t) => !cameToNothing(t));
589
818
  if (!hasNonUnknown)
590
819
  return null;
591
- return folded.filter((t) => t.type.kind !== "Unknown");
820
+ return folded.filter((t) => !cameToNothing(t));
592
821
  }
593
822
  function isDynamicMemberReturn(t) {
594
823
  return t.kind === "MemberOf" && t.member === DYNAMIC_MEMBER_KEY;
@@ -659,7 +888,7 @@ function findFirstDynamicImport(t, fileGraph, depthBudget) {
659
888
  */
660
889
  function walkDynamicMember(graph, fileGraph, type, argMap, guard) {
661
890
  if (guard.pushNode(type) === "cycle") {
662
- return [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
891
+ return [indeterminateTerminal()];
663
892
  }
664
893
  try {
665
894
  const objResults = resolveType(graph, fileGraph, type.obj, argMap, guard);
@@ -675,15 +904,15 @@ function walkDynamicMember(graph, fileGraph, type, argMap, guard) {
675
904
  column: mapBinding?.loc.column ?? 0,
676
905
  };
677
906
  for (const propValue of Object.values(obj.props)) {
678
- // Recurse via walkWithFolding so identity is derived from each branch's
907
+ // Recurse via walkInner so identity is derived from each branch's
679
908
  // leaf TypeOf (Foo/Bar resolve to their respective bindings).
680
- const inner = walkWithFolding(graph, fileGraph, propValue, argMap, guard);
909
+ const inner = walkInner(graph, fileGraph, propValue, argMap, guard);
681
910
  const via = { kind: "dynamic-map", mapName, mapLoc };
682
911
  for (const t of inner) {
683
- if (t.type.kind === "Unknown")
912
+ if (cameToNothing(t))
684
913
  continue;
685
914
  out.push({
686
- type: t.type,
915
+ denotation: t.denotation,
687
916
  viaTrail: [via, ...t.viaTrail],
688
917
  identity: t.identity, // per-branch identity, distinct per map value
689
918
  });