@component-compass/reference-graph 0.1.23 → 0.1.25

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.
@@ -1,12 +1,15 @@
1
+ import { findLocalDeclaration } from "./helper-callers.js";
1
2
  import { resolveType, resolveToFunctions, resolveDynamicImportTarget, DYNAMIC_MEMBER_KEY } from "./resolve-type.js";
2
- import { argumentRefKey } from "./argument-map.js";
3
+ import { argumentRefKey, boundArgument } from "./argument-map.js";
3
4
  import { createCycleGuard } from "./cycle-detection.js";
4
5
  import { fileGraphForRef, resolveReference } from "./resolve-reference.js";
5
6
  import { packageNameFromSpecifier } from "./specifier.js";
6
7
  import { followReExportChain } from "./re-export-chain.js";
7
8
  import { unparsedMemberDefinition, effectiveExportName, residualMemberChain, compoundExportName } from "./member-identity.js";
8
- import { evalKind, isImportBackedLeaf } from "./component-shape.js";
9
+ import { containsDynamicImport, evalKind, isImportBackedLeaf, nonComponentProduct } from "./component-shape.js";
10
+ import { HOOK_NAME } from "./host-element.js";
9
11
  import { assertNever } from "./assert-never.js";
12
+ import { creditedTerminals, isCallable, matchDenotation } from "./denotation.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,192 @@ 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
+ /** True when a value is a loader: a function whose body holds an `import()`,
23
+ * inline, or reached from a reference through a chain of aliases, or held by
24
+ * a static member of an object literal (by every member, for a dynamic one)
25
+ * that the value names directly or through such a chain. Each reference
26
+ * resolves in its own file. Anything else, a `lazy()` holder or a wrapper
27
+ * product included, is not. */
28
+ function isLoader(value, graph, fileGraph, guard) {
29
+ if (value.kind === "Function")
30
+ return containsDynamicImport(value, { graph, fileGraph });
31
+ if (value.kind !== "TypeOf" && value.kind !== "MemberOf")
32
+ return false;
33
+ if (guard.pushNode(value) === "cycle")
34
+ return false;
35
+ try {
36
+ if (value.kind === "TypeOf")
37
+ return isLoader(resolveReference(graph, fileGraph, value.ref, guard), graph, fileGraph, guard);
38
+ const holder = objectLiteralOf(value.obj, graph, fileGraph, guard);
39
+ if (holder === null)
40
+ return false;
41
+ if (value.member !== DYNAMIC_MEMBER_KEY) {
42
+ const held = holder.props[value.member];
43
+ return held !== undefined && isLoader(held, graph, fileGraph, guard);
44
+ }
45
+ const all = Object.values(holder.props);
46
+ return all.length > 0 && all.every((v) => isLoader(v, graph, fileGraph, guard));
47
+ }
48
+ finally {
49
+ guard.popNode(value);
50
+ }
51
+ }
52
+ /** The object literal a value is, directly or through a chain of aliases. */
53
+ function objectLiteralOf(value, graph, fileGraph, guard) {
54
+ if (value.kind === "Object")
55
+ return value;
56
+ if (value.kind !== "TypeOf" || guard.pushNode(value) === "cycle")
57
+ return null;
58
+ try {
59
+ return objectLiteralOf(resolveReference(graph, fileGraph, value.ref, guard), graph, fileGraph, guard);
60
+ }
61
+ finally {
62
+ guard.popNode(value);
63
+ }
64
+ }
65
+ /** The denotation of a branch the walk learned nothing about. */
66
+ const INDETERMINATE = { kind: "indeterminate" };
67
+ function withSemantics(t, semantics) {
68
+ return { denotation: t.denotation, viaTrail: t.viaTrail, identity: t.identity, semantics };
69
+ }
70
+ /** The product every branch of an evaluation is marked with; null when any
71
+ * branch is unmarked or marked differently. */
72
+ function markedProduct(terminals) {
73
+ const first = terminals[0]?.semantics;
74
+ if (first === undefined)
75
+ return null;
76
+ return terminals.every((t) => t.semantics === first) ? first : null;
77
+ }
78
+ /** THE in-walker replacement for `t.type.kind === "Unknown"`, stated once and
79
+ * read only through `matchDenotation`: "this branch came to nothing".
80
+ *
81
+ * `late-bound` answers `true`: the values it classifies reduce to `Unknown` in
82
+ * the algebra. `opaque` is in the record for exhaustiveness only — the walk
83
+ * mints it at the public boundary and never reads it back, so no internal
84
+ * path can observe that handler. */
85
+ const CAME_TO_NOTHING = {
86
+ element: () => false,
87
+ foreign: () => false,
88
+ "host-string": () => false,
89
+ opaque: () => true,
90
+ "late-bound": () => true,
91
+ data: () => false,
92
+ indeterminate: () => true,
93
+ callable: () => false,
94
+ };
95
+ function cameToNothing(t) {
96
+ return matchDenotation(t, CAME_TO_NOTHING);
97
+ }
98
+ const IS_LATE_BOUND = {
99
+ element: () => false,
100
+ foreign: () => false,
101
+ "host-string": () => false,
102
+ opaque: () => false,
103
+ "late-bound": () => true,
104
+ data: () => false,
105
+ indeterminate: () => false,
106
+ callable: () => false,
107
+ };
108
+ /**
109
+ * Rename a branch that came to nothing, keeping its identity and viaTrail
110
+ * exactly as the fold produced them, and dropping any product mark: the new
111
+ * denotation is what the branch is now. A branch that reduced to something the
112
+ * walk does know keeps its own denotation and its mark.
113
+ */
114
+ function relabel(denotation) {
115
+ return (t) => (cameToNothing(t) ? { denotation, viaTrail: t.viaTrail, identity: t.identity } : t);
116
+ }
117
+ function asLateBound(source) {
118
+ return relabel({ kind: "late-bound", source });
119
+ }
120
+ /** True when every branch of an evaluation is a value supplied elsewhere. */
121
+ function evaluatesLateBound(terminals) {
122
+ return terminals.length > 0 && terminals.every((t) => matchDenotation(t, IS_LATE_BOUND));
123
+ }
124
+ /**
125
+ * THE boundary between "what this value IS" and "what this function returns".
126
+ *
127
+ * A string becomes the data the function returns; a late-bound value becomes
128
+ * "nothing known"; a product mark is dropped, so a function that returns a
129
+ * context is not itself a context. `opaque` is in the record for
130
+ * exhaustiveness only — no internal path produces that denotation.
131
+ *
132
+ * Stated once and applied at exactly two places: the `Function` arm of
133
+ * `walkInner` and the exit of `walkReturnTypeOf`.
134
+ */
135
+ const AS_RETURNED_VALUE = {
136
+ element: (d) => d,
137
+ foreign: (d) => d,
138
+ "host-string": () => ({ kind: "data", shape: "string" }),
139
+ opaque: () => INDETERMINATE,
140
+ "late-bound": () => INDETERMINATE,
141
+ data: (d) => d,
142
+ indeterminate: (d) => d,
143
+ callable: (d) => d,
144
+ };
145
+ function returnedValue(t) {
146
+ const denotation = matchDenotation(t, AS_RETURNED_VALUE);
147
+ return denotation === t.denotation && t.semantics === undefined
148
+ ? t
149
+ : { denotation, viaTrail: t.viaTrail, identity: t.identity };
150
+ }
151
+ function indeterminateTerminal() {
152
+ return { denotation: INDETERMINATE, viaTrail: [], identity: null };
153
+ }
154
+ /** THE one place a product mark becomes a denotation. A marked branch the tag
155
+ * would credit keeps the denotation it earned; any other marked branch IS the
156
+ * product. */
157
+ function published(t) {
158
+ if (t.semantics === undefined)
159
+ return t;
160
+ const denotation = creditedTerminals([t]).length > 0 ? t.denotation : { kind: "opaque", semantics: t.semantics };
161
+ return { denotation, viaTrail: t.viaTrail, identity: t.identity };
162
+ }
163
+ /** The public boundary's guarantee, and only the public boundary's: an
164
+ * `Evaluation` handed to a caller is never empty, because a caller has
165
+ * nothing to read otherwise. Inside the walk `[]` stays `[]`. */
166
+ function atLeastOne(terminals) {
167
+ return terminals.length > 0 ? terminals : [indeterminateTerminal()];
168
+ }
169
+ /**
170
+ * What one `resolveType` terminal denotes. `synthesised` marks the unreducible
171
+ * import-backed leaf `foldTerminals` mints an identity for: the reference is
172
+ * real and its target unparseable, so the value is `foreign`. `indeterminate`
173
+ * is reserved for the terminal the algebra bottomed out at knowing nothing —
174
+ * a value the algebra did reduce, to a string or to data, is not that.
175
+ */
176
+ function denote(type, synthesised) {
177
+ if (synthesised)
178
+ return { kind: "foreign" };
179
+ switch (type.kind) {
180
+ case "JSX":
181
+ return { kind: "element" };
182
+ case "Str":
183
+ return { kind: "host-string", value: type.value };
184
+ case "Object":
185
+ return { kind: "data", shape: "object" };
186
+ case "Array":
187
+ return { kind: "data", shape: "array" };
188
+ case "TypeOf":
189
+ case "Function":
190
+ case "ReturnTypeOf":
191
+ case "ParameterOf":
192
+ case "MemberOf":
193
+ case "Union":
194
+ case "Unknown":
195
+ case "DynamicImport":
196
+ return INDETERMINATE;
197
+ default:
198
+ return assertNever(type);
199
+ }
200
+ }
19
201
  /**
20
202
  * THE single seam for unreducible-leaf identity synthesis.
21
203
  *
22
- * Maps `resolveType` results into FoldedTerminals. When the algebra preserved
204
+ * Maps `resolveType` results into Terminals. When the algebra preserved
23
205
  * an identity-bearing leaf — an import-backed TypeOf or a DynamicImport that
24
206
  * couldn't be reduced (see resolve-type.ts's TypeOf/DynamicImport cases) —
25
- * synthesize a JSX terminal carrying the identity derived FROM THE TERMINAL
207
+ * synthesize a `foreign` terminal carrying the identity derived FROM THE TERMINAL
26
208
  * ITSELF: the reference is real even though its target can't be parsed
27
209
  * (external package, CJS, unresolvable specifier). Every other terminal
28
210
  * derives identity from `source`, falling back to `fallback` only when the
@@ -36,7 +218,7 @@ function foldTerminals(graph, fileGraph, results, fallback) {
36
218
  if (t.kind === "TypeOf" || t.kind === "DynamicImport") {
37
219
  const identity = deriveIdentity(t, fileGraph, graph);
38
220
  if (identity !== null) {
39
- return { type: { kind: "JSX" }, viaTrail: [], identity };
221
+ return { denotation: denote(t, true), viaTrail: [], identity };
40
222
  }
41
223
  }
42
224
  // A fan-out branch (Union / dynamic-map) is stamped as `source`. Derive
@@ -46,7 +228,7 @@ function foldTerminals(graph, fileGraph, results, fallback) {
46
228
  // ternary/logical render body).
47
229
  const identity = (source && deriveIdentity(source, fileGraph, graph)) ?? deriveIdentity(fallback, fileGraph, graph);
48
230
  return {
49
- type: t,
231
+ denotation: denote(t, false),
50
232
  viaTrail: [],
51
233
  identity,
52
234
  };
@@ -58,22 +240,24 @@ function foldTerminals(graph, fileGraph, results, fallback) {
58
240
  * a seen-through wrapper and an opaque one attribute identically. Inner
59
241
  * terminals pass through with the wrapper hop prepended. When the inner walk
60
242
  * bottomed out at Unknown but carried an identity hint (imported, or
61
- * local-promoted through the re-export chain), synthesise a JSX terminal
243
+ * local-promoted through the re-export chain), synthesise a `foreign` terminal
62
244
  * from it — without this
63
245
  * rescue, HOC-wrapped occurrences drop silently (~128 in example-web's
64
246
  * connect/flow/withTranslators patterns). Truly
65
247
  * nothing → a single Unknown terminal.
66
248
  */
67
249
  function foldToWrappedArg(inner, via) {
68
- const innerHasNonUnknown = inner.some((t) => t.type.kind !== "Unknown");
250
+ // Drops the product mark: what a wrapper call returns is not the wrapped
251
+ // value, so `memo(Ctx)` is not a context.
252
+ const innerHasNonUnknown = inner.some((t) => !cameToNothing(t));
69
253
  if (innerHasNonUnknown) {
70
- return inner.map((t) => ({ type: t.type, viaTrail: [via, ...t.viaTrail], identity: t.identity }));
254
+ return inner.map((t) => ({ denotation: t.denotation, viaTrail: [via, ...t.viaTrail], identity: t.identity }));
71
255
  }
72
256
  const synthesisIdentity = inner.find((t) => t.identity !== null)?.identity ?? null;
73
257
  if (synthesisIdentity) {
74
- return [{ type: { kind: "JSX" }, viaTrail: [via], identity: synthesisIdentity }];
258
+ return [{ denotation: { kind: "foreign" }, viaTrail: [via], identity: synthesisIdentity }];
75
259
  }
76
- return [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
260
+ return [indeterminateTerminal()];
77
261
  }
78
262
  /**
79
263
  * Provenance of ONE argument of a wrapper call: the wrapper's `callee` name
@@ -150,8 +334,8 @@ function identityKey(id) {
150
334
  * returns a DIFFERENT component fails this and keeps its richer algebra result.
151
335
  */
152
336
  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");
337
+ const innerKeys = new Set(inner.filter((t) => !cameToNothing(t) && t.identity !== null).map((t) => identityKey(t.identity)));
338
+ const algKeys = algResult.filter((t) => !cameToNothing(t));
155
339
  // A null identity on either side is "no information", never a match:
156
340
  // null-equals-null folds a factory call onto its rehydrate/config argument.
157
341
  return algKeys.length > 0 && algKeys.every((t) => t.identity !== null && innerKeys.has(identityKey(t.identity)));
@@ -166,7 +350,23 @@ function isPassThroughWrapper(algResult, inner) {
166
350
  * (pure algebra) and computes identity from the input type's leaf TypeOf
167
351
  * when one can be confidently identified.
168
352
  */
169
- export function walkWithFolding(graph, fileGraph, type, argMap, guard = createCycleGuard(),
353
+ export function walkWithFolding(graph, fileGraph, type, argMap, guard = createCycleGuard(), identityFallback, position = "value") {
354
+ return atLeastOne(walkInner(graph, fileGraph, type, argMap, guard, identityFallback, position).map(published));
355
+ }
356
+ /**
357
+ * The walk itself. Returns exactly what each arm produced, empty included:
358
+ * inside the walk an empty result means "this branch contributed nothing",
359
+ * which `walkTypeOf`'s vacuous `every` and `foldToWrappedArg`'s rescue both
360
+ * read. Only the exported boundary above may substitute a terminal for it.
361
+ *
362
+ * `position` says whether `type` is what a tag renders. At a `tag`, a
363
+ * function is one `callable` terminal and its body is not walked; the
364
+ * position carries through the value hops (a reference, a union branch, a
365
+ * static or dynamic member, a call's folded argument, a lazy target, a
366
+ * dispatcher's returns) and nowhere else. In `value` position a function is
367
+ * walked into its returns.
368
+ */
369
+ function walkInner(graph, fileGraph, type, argMap, guard,
170
370
  // The identity `foldTerminals` falls back to when a terminal
171
371
  // carries no identity of its own (no `source`, no unreducible TypeOf/
172
372
  // DynamicImport leaf). Defaults to `type` (the walked node itself). The
@@ -178,38 +378,46 @@ export function walkWithFolding(graph, fileGraph, type, argMap, guard = createCy
178
378
  // that happens to sit in `foldTerminals`' fallback slot. Per-branch
179
379
  // identity from a fanout `source` (Union([TypeOf A, TypeOf B])) is
180
380
  // untouched: `source` is checked BEFORE this fallback in `foldTerminals`.
181
- identityFallback) {
381
+ identityFallback, position) {
182
382
  const fallback = identityFallback ?? type;
183
383
  switch (type.kind) {
184
384
  case "ReturnTypeOf":
185
- return walkReturnTypeOf(graph, fileGraph, type, argMap, guard);
385
+ return walkReturnTypeOf(graph, fileGraph, type, argMap, guard, position);
186
386
  case "MemberOf":
187
387
  if (type.member === DYNAMIC_MEMBER_KEY) {
188
- return walkDynamicMember(graph, fileGraph, type, argMap, guard);
388
+ return walkDynamicMember(graph, fileGraph, type, argMap, guard, position);
189
389
  }
190
- return walkStaticMember(graph, fileGraph, type, argMap, guard, fallback);
390
+ return walkStaticMember(graph, fileGraph, type, argMap, guard, fallback, position);
191
391
  case "Function":
192
- // A component's own returns can contain a nested
392
+ // At a tag the function is what JSX renders: one `callable` terminal
393
+ // with no identity of its own, and the body is not walked.
394
+ //
395
+ // In value position a component's own returns can contain a nested
193
396
  // ReturnTypeOf/MemberOf (`List = ({items}) => items.map(render)`) that
194
397
  // needs wrapper-folding's steps (the opaque-callee / dynamic-map
195
398
  // handling below), not `resolveType`'s plain algebra — `resolveType`'s
196
399
  // own "Function" case recurses via plain `resolveType`, which never
197
- // re-enters this walk, so a JSX usage resolving to such a Function
198
- // (e.g. `<List/>`) would silently produce zero occurrences.
400
+ // re-enters this walk, so a value resolving to such a Function would
401
+ // silently produce zero terminals.
199
402
  // Mirrors `resolveType`'s "Function" case, one level up. Pass THIS
200
403
  // Function as the identity fallback for every return — a
201
404
  // Function is never TypeOf/DynamicImport, so `deriveIdentity` on it
202
405
  // is always null.
203
- return type.returns.flatMap((r) => walkWithFolding(graph, fileGraph, r, argMap, guard, type));
406
+ if (position === "tag")
407
+ return [{ denotation: { kind: "callable" }, identity: null, viaTrail: [] }];
408
+ return type.returns
409
+ .flatMap((r) => walkInner(graph, fileGraph, r, argMap, guard, type, "value"))
410
+ .map(returnedValue);
204
411
  case "TypeOf":
205
- return walkTypeOf(graph, fileGraph, type, argMap, guard, fallback);
412
+ return walkTypeOf(graph, fileGraph, type, argMap, guard, fallback, position);
206
413
  case "Union":
207
414
  // Structural re-entry: each branch walks through the walker so
208
415
  // a TypeOf branch reaches `walkTypeOf` and decides its own identity
209
416
  // (per-branch attribution) while a bare JSX branch — a lowered
210
417
  // ternary/logical render body — derives from the outer fallback.
211
- return type.types.flatMap((branch) => walkWithFolding(graph, fileGraph, branch, argMap, guard, fallback));
418
+ return type.types.flatMap((branch) => walkInner(graph, fileGraph, branch, argMap, guard, fallback, position));
212
419
  case "ParameterOf":
420
+ return walkParameter(graph, fileGraph, type, argMap, guard, fallback);
213
421
  case "DynamicImport":
214
422
  case "Object":
215
423
  case "Array":
@@ -221,12 +429,25 @@ identityFallback) {
221
429
  return assertNever(type);
222
430
  }
223
431
  }
432
+ /**
433
+ * A parameter. Bound by the call the walk is inside — `resolveType`
434
+ * substitutes the argument and the walk continues into it, exactly as before.
435
+ * Unbound, the value is supplied by whoever calls this function later, which
436
+ * is a different fact from "nothing is known": the algebra flattens both to
437
+ * `Unknown`, and only the render site acts on the difference.
438
+ */
439
+ function walkParameter(graph, fileGraph, type, argMap, guard, fallback) {
440
+ const leaf = foldLeaf(graph, fileGraph, type, argMap, guard, fallback);
441
+ if (boundArgument(argMap, type) !== undefined)
442
+ return leaf;
443
+ return leaf.map(asLateBound("parameter"));
444
+ }
224
445
  /**
225
446
  * The leaf fold: pure algebra, then identity synthesis through the ONE
226
447
  * unreducible-leaf seam (`foldTerminals`). This is the only place the walker
227
448
  * hands a subtree to `resolveType`; every structural kind (TypeOf, Union,
228
449
  * MemberOf, Function, ReturnTypeOf) has its own arm above and re-enters
229
- * `walkWithFolding` instead.
450
+ * `walkInner` instead.
230
451
  */
231
452
  function foldLeaf(graph, fileGraph, type, argMap, guard, fallback) {
232
453
  return foldTerminals(graph, fileGraph, resolveType(graph, fileGraph, type, argMap, guard), fallback);
@@ -282,7 +503,7 @@ export function dynamicImportIdentity(graph, fileGraph, di) {
282
503
  }
283
504
  /**
284
505
  * Structural re-entry for a reference. Resolves ONE
285
- * hop and walks the resolved value through `walkWithFolding`, so every walker
506
+ * hop and walks the resolved value through `walkInner`, so every walker
286
507
  * rule — wrapper hops, dynamic maps, `.map()` bodies — applies
287
508
  * through an alias exactly as at a direct render site. Without this arm the
288
509
  * default fold hands the whole subtree to `resolveType`, which never comes
@@ -291,9 +512,14 @@ export function dynamicImportIdentity(graph, fileGraph, di) {
291
512
  * Identity: a terminal that comes back with no identity of its own is
292
513
  * stamped with THIS reference's identity only when the reference names a
293
514
  * component-shaped value — `evalKind === "component"`, the one component-
294
- * shape seam, called not re-derived. A reference to a JSX value
295
- * (`return priceElement`) or to a parameter binding stays null so the outer
296
- * fallback wins, by construction rather than by special case.
515
+ * shape seam, called not re-derived — or, at a tag, when the walk came back
516
+ * with an identity-less `callable`: a reference to a function, or to a
517
+ * wrapper call over an anonymous one, names what the tag renders, whatever
518
+ * it returns. A reference to a JSX value (`return priceElement`) or to a
519
+ * parameter binding stays null so the outer fallback wins, by construction
520
+ * rather than by special case. At a tag, a self-stamp of a same-file reference
521
+ * also carries the declaration that reference resolves to
522
+ * (`withOwnDeclaration`).
297
523
  *
298
524
  * An import the graph cannot see (`resolved` is Unknown) is NOT re-entered:
299
525
  * it is the identity-bearing leaf `foldLeaf` synthesises from, and
@@ -307,9 +533,9 @@ export function dynamicImportIdentity(graph, fileGraph, di) {
307
533
  * Cycle guard: mirrors `resolveType`'s node guard. `const A = B; const B = A`
308
534
  * is legal JS and would otherwise re-enter forever.
309
535
  */
310
- function walkTypeOf(graph, fileGraph, type, argMap, guard, fallback) {
536
+ function walkTypeOf(graph, fileGraph, type, argMap, guard, fallback, position) {
311
537
  if (guard.pushNode(type) === "cycle") {
312
- return [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
538
+ return [indeterminateTerminal()];
313
539
  }
314
540
  try {
315
541
  const resolved = resolveReference(graph, fileGraph, type.ref, guard);
@@ -323,23 +549,40 @@ function walkTypeOf(graph, fileGraph, type, argMap, guard, fallback) {
323
549
  return foldLeaf(graph, fileGraph, type, argMap, guard, fallback);
324
550
  }
325
551
  const innerGraph = resolved.kind === "Function" ? fileGraphForFunction(graph, fileGraph, resolved) : fileGraph;
326
- const inner = walkWithFolding(graph, innerGraph, resolved, argMap, guard, fallback);
552
+ const inner = walkInner(graph, innerGraph, resolved, argMap, guard, fallback, position);
327
553
  if (inner.every((t) => t.identity !== null))
328
554
  return inner;
329
- const self = resolved.kind !== "ParameterOf" && evalKind(resolved, graph, innerGraph, argMap) === "component"
555
+ const derived = (position === "tag" && inner.some((t) => t.identity === null && isCallable(t))) ||
556
+ (resolved.kind !== "ParameterOf" && evalKind(resolved, graph, innerGraph, argMap) === "component")
330
557
  ? deriveIdentity(type, fileGraph, graph)
331
558
  : null;
559
+ const self = position === "tag" ? withOwnDeclaration(derived, graph, fileGraph, type.ref) : derived;
332
560
  const stamp = self ?? deriveIdentity(fallback, fileGraph, graph);
333
- return inner.map((t) => (t.identity === null ? { type: t.type, viaTrail: t.viaTrail, identity: stamp } : t));
561
+ // Carries the product mark: a reference to a context IS that context.
562
+ return inner.map((t) => t.identity === null ? { denotation: t.denotation, viaTrail: t.viaTrail, identity: stamp, semantics: t.semantics } : t);
334
563
  }
335
564
  finally {
336
565
  guard.popNode(type);
337
566
  }
338
567
  }
568
+ /**
569
+ * A local identity stamped from a same-file reference with no member chain,
570
+ * given the declaration that reference resolves to in its own scope
571
+ * (`findLocalDeclaration`). Any other identity is returned unchanged.
572
+ */
573
+ function withOwnDeclaration(identity, graph, fileGraph, ref) {
574
+ if (identity === null || identity.kind !== "local" || ref.memberChain.length > 0)
575
+ return identity;
576
+ const lookup = fileGraphForRef(graph, ref, fileGraph);
577
+ if (identity.filePath !== lookup.filePath || lookup.importsByLocal.has(ref.symbol))
578
+ return identity;
579
+ const declaration = findLocalDeclaration(lookup, ref);
580
+ return declaration === null ? identity : { ...identity, declaration };
581
+ }
339
582
  /**
340
583
  * Static member access. Mirrors `walkDynamicMember` for one
341
584
  * 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
585
+ * member, walk the member's VALUE through `walkInner` — so a wrapper
343
586
  * product stored in a map (`MAP.memo` → `memo(Button)`) reaches
344
587
  * `walkReturnTypeOf` and keeps its `hoc-wrapper` hop. Anything that is not an
345
588
  * in-graph Object (an Array, an import-backed namespace the graph cannot see)
@@ -350,9 +593,9 @@ function walkTypeOf(graph, fileGraph, type, argMap, guard, fallback) {
350
593
  * re-enters this arm forever without it — `resolveType` pops every node it
351
594
  * pushes, so nothing on such a cycle stays on the guard's path otherwise.
352
595
  */
353
- function walkStaticMember(graph, fileGraph, type, argMap, guard, fallback) {
596
+ function walkStaticMember(graph, fileGraph, type, argMap, guard, fallback, position) {
354
597
  if (guard.pushNode(type) === "cycle") {
355
- return [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
598
+ return [indeterminateTerminal()];
356
599
  }
357
600
  try {
358
601
  const objects = resolveType(graph, fileGraph, type.obj, argMap, guard)
@@ -365,22 +608,61 @@ function walkStaticMember(graph, fileGraph, type, argMap, guard, fallback) {
365
608
  // identity), not just on real cycles. The `finally` below still runs
366
609
  // (a no-op double pop) once this returns.
367
610
  guard.popNode(type);
368
- return foldLeaf(graph, fileGraph, type, argMap, guard, fallback);
611
+ const leaf = foldLeaf(graph, fileGraph, type, argMap, guard, fallback);
612
+ if (!(leaf.length > 0 && leaf.every(cameToNothing)))
613
+ return leaf;
614
+ // The object answers for the member: a prop read off a value supplied
615
+ // elsewhere (`props.Icon`, `useThing()[0]`) is supplied elsewhere too,
616
+ // and any member of a non-component product is that product. ONE walk
617
+ // answers both, and only once the member came to nothing, so a member
618
+ // that resolved is never re-walked.
619
+ const object = walkInner(graph, fileGraph, type.obj, argMap, guard, undefined, "value");
620
+ const semantics = markedProduct(object);
621
+ if (semantics !== null)
622
+ return leaf.map((t) => withSemantics(t, semantics));
623
+ return evaluatesLateBound(object) ? leaf.map(asLateBound("parameter-member")) : leaf;
369
624
  }
370
625
  const out = [];
371
626
  for (const obj of objects) {
372
627
  const value = obj.props[type.member];
373
628
  if (value === undefined)
374
629
  continue;
375
- out.push(...walkWithFolding(graph, fileGraph, value, argMap, guard, fallback));
630
+ out.push(...walkInner(graph, fileGraph, value, argMap, guard, fallback, position));
376
631
  }
377
- return out.length > 0 ? out : [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
632
+ return out.length > 0 ? out : [indeterminateTerminal()];
378
633
  }
379
634
  finally {
380
635
  guard.popNode(type);
381
636
  }
382
637
  }
383
- function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
638
+ /**
639
+ * A call. A call of an export the stub table names a non-component product
640
+ * IS that product, before any fold runs: its arguments are values the product
641
+ * holds (a context's default), never a wrapped component, so none of them is
642
+ * walked or credited.
643
+ *
644
+ * Every other call runs the folding rules first (`foldCall`). The call's own
645
+ * shape gets a name only when all of them declined: no argument credited and no
646
+ * algebra result. A call of a hook the graph cannot see returns a value
647
+ * bound at render time by the hook's own implementation, so it is late-bound
648
+ * rather than unknown. The predicate is React's `HOOK_NAME` over a callee
649
+ * `isImportBackedLeaf` says the graph cannot resolve; it runs after the folds,
650
+ * so `const X = useStyled(Base)` keeps the credit its fold gave it.
651
+ *
652
+ * ORDER IS LOAD-BEARING: `returnedValue` runs on the fold's results before
653
+ * `late-bound{hook-result}` is minted for the call itself.
654
+ */
655
+ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard, position) {
656
+ const semantics = nonComponentProduct(graph, fileGraph, type.callee, guard);
657
+ if (semantics !== null)
658
+ return [withSemantics(indeterminateTerminal(), semantics)];
659
+ const returns = foldCall(graph, fileGraph, type, argMap, guard, position).map(returnedValue);
660
+ if (!(returns.length > 0 && returns.every(cameToNothing)))
661
+ return returns;
662
+ const unseenHook = HOOK_NAME.test(deriveHocCallee(type.callee)) && isImportBackedLeaf(type.callee, graph, fileGraph, guard);
663
+ return unseenHook ? returns.map(asLateBound("hook-result")) : returns;
664
+ }
665
+ function foldCall(graph, fileGraph, type, argMap, guard, position) {
384
666
  // (1) lazy-import shape: any arg's subtree contains a DynamicImport
385
667
  // (with one-ref-deep peek into local-file bindings to catch the async-await form).
386
668
  const lazyShape = findLazyImportArg(type.args, fileGraph);
@@ -395,14 +677,14 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
395
677
  };
396
678
  const guardKey = `import()::${syntheticDI.specifier}::${syntheticDI.projection.join(".")}`;
397
679
  if (guard.push(syntheticDI.originFile, guardKey) !== "ok") {
398
- return [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
680
+ return [indeterminateTerminal()];
399
681
  }
400
682
  let inner;
401
683
  try {
402
684
  const target = resolveDynamicImportTarget(graph, fileGraph, syntheticDI, guard);
403
685
  inner = target
404
- ? walkWithFolding(graph, target.targetFileGraph, target.value, argMap, guard)
405
- : walkWithFolding(graph, fileGraph, syntheticDI, argMap, guard);
686
+ ? walkInner(graph, target.targetFileGraph, target.value, argMap, guard, undefined, position)
687
+ : walkInner(graph, fileGraph, syntheticDI, argMap, guard, undefined, position);
406
688
  }
407
689
  finally {
408
690
  guard.pop(syntheticDI.originFile, guardKey);
@@ -418,12 +700,12 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
418
700
  // A terminal keeps the identity its own fold produced; the import-derived
419
701
  // identity covers terminals that carry none.
420
702
  const identity = dynamicImportIdentity(graph, fileGraph, syntheticDI);
421
- const innerHasNonUnknown = inner.some((t) => t.type.kind !== "Unknown");
703
+ const innerHasNonUnknown = inner.some((t) => !cameToNothing(t));
422
704
  if (innerHasNonUnknown) {
423
705
  return inner
424
- .filter((t) => t.type.kind !== "Unknown")
706
+ .filter((t) => !cameToNothing(t))
425
707
  .map((t) => ({
426
- type: t.type,
708
+ denotation: t.denotation,
427
709
  viaTrail: [via, ...t.viaTrail],
428
710
  identity: t.identity ?? identity,
429
711
  }));
@@ -431,17 +713,17 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
431
713
  // External package: target not in graph. Also reached when the inner walk
432
714
  // was cut by the import cycle key above or by the depth budget, in which
433
715
  // case the import-derived identity is the answer. We still have a valid
434
- // specifier+projection from lazyShape — synthesize a JSX terminal rather
716
+ // specifier+projection from lazyShape — synthesize a `foreign` terminal rather
435
717
  // than falling through to Unknown.
436
- return [{ type: { kind: "JSX" }, viaTrail: [via], identity }];
718
+ return [{ denotation: { kind: "foreign" }, viaTrail: [via], identity }];
437
719
  }
438
720
  // (2) Dispatcher detection: if the callee resolves to a Function
439
721
  // 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
722
+ // via walkInner in the function's authoring fileGraph. This lets the
441
723
  // walkDynamicMember rule fire and surfaces the dynamic-map via in the chain
442
724
  // — without it, cross-module dispatcher patterns would lose the via even
443
725
  // though identity attribution worked.
444
- const dispatcherResult = walkDispatcherReturns(graph, fileGraph, type, argMap, guard);
726
+ const dispatcherResult = walkDispatcherReturns(graph, fileGraph, type, argMap, guard, position);
445
727
  if (dispatcherResult)
446
728
  return dispatcherResult;
447
729
  // (2b) Structural pass-through. When every Function
@@ -457,7 +739,7 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
457
739
  if (passThroughIndex !== null) {
458
740
  const wrapped = type.args[passThroughIndex];
459
741
  if (wrapped !== undefined) {
460
- const inner = walkWithFolding(graph, fileGraph, wrapped, argMap, guard);
742
+ const inner = walkInner(graph, fileGraph, wrapped, argMap, guard, undefined, position);
461
743
  return foldToWrappedArg(inner, buildHocVia(type, wrapped, fileGraph));
462
744
  }
463
745
  }
@@ -466,10 +748,10 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
466
748
  // Union / Array fanouts attribute each branch to its own binding instead
467
749
  // of collapsing every leaf to the outer wrapper's identity.
468
750
  // foldTerminals also converts unreducible identity-bearing leaves into
469
- // synthesized JSX terminals, so a resolvable and an unresolvable imported
751
+ // synthesized `foreign` terminals, so a resolvable and an unresolvable imported
470
752
  // leaf take the same emission path.
471
753
  const algResult = foldTerminals(graph, fileGraph, resolveType(graph, fileGraph, type, argMap, guard), type);
472
- const algHasNonUnknown = algResult.some((r) => r.type.kind !== "Unknown");
754
+ const algHasNonUnknown = algResult.some((r) => !cameToNothing(r));
473
755
  if (algHasNonUnknown) {
474
756
  // (3b) Pass-through HOC recovery. When the algebra reduces this call to the
475
757
  // identity of its wrapped last arg (an identity-preserving `c => c` wrapper
@@ -482,12 +764,12 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
482
764
  // stands with no via.
483
765
  const lastArg = type.args.at(-1);
484
766
  if (lastArg !== undefined) {
485
- const inner = walkWithFolding(graph, fileGraph, lastArg, argMap, guard);
767
+ const inner = walkInner(graph, fileGraph, lastArg, argMap, guard, undefined, position);
486
768
  if (isPassThroughWrapper(algResult, inner)) {
487
769
  const via = buildHocVia(type, lastArg, fileGraph);
488
770
  return inner
489
- .filter((t) => t.type.kind !== "Unknown")
490
- .map((t) => ({ type: t.type, viaTrail: [via, ...t.viaTrail], identity: t.identity }));
771
+ .filter((t) => !cameToNothing(t))
772
+ .map((t) => ({ denotation: t.denotation, viaTrail: [via, ...t.viaTrail], identity: t.identity }));
491
773
  }
492
774
  }
493
775
  return algResult;
@@ -495,9 +777,13 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
495
777
  // (4) HOC fallback: algebra failed, last arg is the wrapper target.
496
778
  const lastArg = type.args.at(-1);
497
779
  if (lastArg === undefined) {
498
- return [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
780
+ return [indeterminateTerminal()];
499
781
  }
500
- const inner = walkWithFolding(graph, fileGraph, lastArg, argMap, guard);
782
+ // A data-method callback and a loader holding an `import()` the lazy arm
783
+ // declined are called, not rendered, so they walk in value position.
784
+ const dataMethod = isDataMethodRenderCall(type, graph, fileGraph, guard);
785
+ const called = dataMethod || isLoader(lastArg, graph, fileGraph, guard);
786
+ const inner = walkInner(graph, fileGraph, lastArg, argMap, guard, undefined, called ? "value" : position);
501
787
  // A data-method render call (`items.map(render)`) is not a
502
788
  // wrapper — its result IS the callback's rendered JSX, not a component
503
789
  // received as a prop. Return the callback's terminals AS-IS: no
@@ -505,12 +791,12 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
505
791
  // (the declaration being walked, e.g. `List`) wins rather than whatever
506
792
  // the callback itself happened to resolve to. Import-backed MemberOf
507
793
  // callees (`Sentry.withProfiler`) fall through to the fold+hop below.
508
- if (isDataMethodRenderCall(type, graph, fileGraph, guard)) {
509
- const nonUnknown = inner.filter((t) => t.type.kind !== "Unknown");
794
+ if (dataMethod) {
795
+ const nonUnknown = inner.filter((t) => !cameToNothing(t));
510
796
  if (nonUnknown.length > 0) {
511
- return nonUnknown.map((t) => ({ type: t.type, viaTrail: t.viaTrail, identity: null }));
797
+ return nonUnknown.map((t) => ({ denotation: t.denotation, viaTrail: t.viaTrail, identity: null }));
512
798
  }
513
- return [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
799
+ return [indeterminateTerminal()];
514
800
  }
515
801
  return foldToWrappedArg(inner, buildHocVia(type, lastArg, fileGraph));
516
802
  }
@@ -559,11 +845,11 @@ function passThroughParameterIndex(graph, fileGraph, type, argMap, guard) {
559
845
  * so no `dynamic-map` via would land in the chain.
560
846
  *
561
847
  * This helper detects the shape and walks each function's returns through
562
- * `walkWithFolding` in the function's authoring fileGraph (via
848
+ * `walkInner` in the function's authoring fileGraph (via
563
849
  * `enclosingBinding.file`). Returns null when no callee matches the shape so
564
850
  * the caller falls through to the pure algebra path.
565
851
  */
566
- function walkDispatcherReturns(graph, fileGraph, type, argMap, guard) {
852
+ function walkDispatcherReturns(graph, fileGraph, type, argMap, guard, position) {
567
853
  const calleeFns = resolveToFunctions(graph, fileGraph, type.callee, argMap, guard);
568
854
  const hasDispatcher = calleeFns.some(({ fn }) => fn.kind === "Function" && fn.returns.some(isDynamicMemberReturn));
569
855
  if (!hasDispatcher)
@@ -577,7 +863,7 @@ function walkDispatcherReturns(graph, fileGraph, type, argMap, guard) {
577
863
  argMap.bind(bindingRef, type.args);
578
864
  try {
579
865
  for (const ret of fn.returns) {
580
- folded.push(...walkWithFolding(graph, fnFileGraph, ret, argMap, guard));
866
+ folded.push(...walkInner(graph, fnFileGraph, ret, argMap, guard, undefined, position));
581
867
  }
582
868
  }
583
869
  finally {
@@ -585,10 +871,10 @@ function walkDispatcherReturns(graph, fileGraph, type, argMap, guard) {
585
871
  argMap.unbind(bindingRef);
586
872
  }
587
873
  }
588
- const hasNonUnknown = folded.some((t) => t.type.kind !== "Unknown");
874
+ const hasNonUnknown = folded.some((t) => !cameToNothing(t));
589
875
  if (!hasNonUnknown)
590
876
  return null;
591
- return folded.filter((t) => t.type.kind !== "Unknown");
877
+ return folded.filter((t) => !cameToNothing(t));
592
878
  }
593
879
  function isDynamicMemberReturn(t) {
594
880
  return t.kind === "MemberOf" && t.member === DYNAMIC_MEMBER_KEY;
@@ -657,9 +943,9 @@ function findFirstDynamicImport(t, fileGraph, depthBudget) {
657
943
  * reached through `MAP[k]` re-enters this arm forever without it. No
658
944
  * `foldLeaf` path here, so no early pop is needed.
659
945
  */
660
- function walkDynamicMember(graph, fileGraph, type, argMap, guard) {
946
+ function walkDynamicMember(graph, fileGraph, type, argMap, guard, position) {
661
947
  if (guard.pushNode(type) === "cycle") {
662
- return [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
948
+ return [indeterminateTerminal()];
663
949
  }
664
950
  try {
665
951
  const objResults = resolveType(graph, fileGraph, type.obj, argMap, guard);
@@ -675,15 +961,15 @@ function walkDynamicMember(graph, fileGraph, type, argMap, guard) {
675
961
  column: mapBinding?.loc.column ?? 0,
676
962
  };
677
963
  for (const propValue of Object.values(obj.props)) {
678
- // Recurse via walkWithFolding so identity is derived from each branch's
964
+ // Recurse via walkInner so identity is derived from each branch's
679
965
  // leaf TypeOf (Foo/Bar resolve to their respective bindings).
680
- const inner = walkWithFolding(graph, fileGraph, propValue, argMap, guard);
966
+ const inner = walkInner(graph, fileGraph, propValue, argMap, guard, undefined, position);
681
967
  const via = { kind: "dynamic-map", mapName, mapLoc };
682
968
  for (const t of inner) {
683
- if (t.type.kind === "Unknown")
969
+ if (cameToNothing(t))
684
970
  continue;
685
971
  out.push({
686
- type: t.type,
972
+ denotation: t.denotation,
687
973
  viaTrail: [via, ...t.viaTrail],
688
974
  identity: t.identity, // per-branch identity, distinct per map value
689
975
  });
@@ -969,7 +1255,7 @@ export function deriveIdentity(type, fileGraph, graph) {
969
1255
  }
970
1256
  // Check local declarations. A same-file symbol is never a namespace
971
1257
  // import, so the whole member chain is residual (mirror of
972
- // `pushLocalOccurrence`'s fallback) — an empty chain is a no-op.
1258
+ // `evaluateJsxUsage`'s same-file fallback) — an empty chain is a no-op.
973
1259
  for (const [, decl] of lookup.declarations) {
974
1260
  if (decl && decl.symbol === symbol) {
975
1261
  return withResidual({