@component-compass/reference-graph 0.1.15 → 0.1.18
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/engine/argument-map.d.ts +4 -0
- package/dist/engine/argument-map.js +7 -4
- package/dist/engine/argument-map.js.map +1 -1
- package/dist/engine/assert-never.d.ts +7 -0
- package/dist/engine/assert-never.js +10 -0
- package/dist/engine/assert-never.js.map +1 -0
- package/dist/engine/component-shape.d.ts +38 -0
- package/dist/engine/component-shape.js +177 -0
- package/dist/engine/component-shape.js.map +1 -0
- package/dist/engine/helper-callers.d.ts +31 -29
- package/dist/engine/helper-callers.js +98 -78
- package/dist/engine/helper-callers.js.map +1 -1
- package/dist/engine/index.d.ts +4 -0
- package/dist/engine/index.js +239 -99
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/library-stubs.d.ts +2 -0
- package/dist/engine/library-stubs.js +42 -0
- package/dist/engine/library-stubs.js.map +1 -0
- package/dist/engine/member-identity.d.ts +24 -4
- package/dist/engine/member-identity.js +28 -4
- package/dist/engine/member-identity.js.map +1 -1
- package/dist/engine/registry.d.ts +74 -0
- package/dist/engine/registry.js +226 -0
- package/dist/engine/registry.js.map +1 -0
- package/dist/engine/resolve-reference.js +7 -0
- package/dist/engine/resolve-reference.js.map +1 -1
- package/dist/engine/resolve-type.js +54 -1
- package/dist/engine/resolve-type.js.map +1 -1
- package/dist/engine/wrapper-folding.d.ts +21 -3
- package/dist/engine/wrapper-folding.js +386 -91
- package/dist/engine/wrapper-folding.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/types/file-graph.d.ts +0 -10
- package/package.json +2 -2
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import { resolveType, resolveToFunctions, DYNAMIC_MEMBER_KEY } from "./resolve-type.js";
|
|
2
|
+
import { argumentRefKey } from "./argument-map.js";
|
|
2
3
|
import { createCycleGuard } from "./cycle-detection.js";
|
|
3
|
-
import { fileGraphForRef } from "./resolve-reference.js";
|
|
4
|
+
import { fileGraphForRef, resolveReference } from "./resolve-reference.js";
|
|
4
5
|
import { packageNameFromSpecifier } from "./specifier.js";
|
|
5
6
|
import { followReExportChain } from "./re-export-chain.js";
|
|
6
|
-
import { unparsedMemberDefinition, effectiveExportName } from "./member-identity.js";
|
|
7
|
+
import { unparsedMemberDefinition, effectiveExportName, residualMemberChain, compoundExportName } from "./member-identity.js";
|
|
8
|
+
import { evalKind, isImportBackedLeaf } from "./component-shape.js";
|
|
9
|
+
import { assertNever } from "./assert-never.js";
|
|
10
|
+
/** Ruling 19: true when a `ReturnTypeOf`'s callee is a data-method render
|
|
11
|
+
* call (`items.map(render)`) rather than a wrapper taking a component as a
|
|
12
|
+
* prop — the exact structural condition `component-shape.ts`'s rule (ii)
|
|
13
|
+
* uses to answer `"jsx"` instead of `"component"`. Shared, not re-derived:
|
|
14
|
+
* a `MemberOf` callee that is NOT an import-backed leaf (`Sentry.x` stays a
|
|
15
|
+
* wrapper; `items.map`/a hook result/a parameter does not). */
|
|
16
|
+
function isDataMethodRenderCall(type, graph, fileGraph, guard) {
|
|
17
|
+
return type.callee.kind === "MemberOf" && !isImportBackedLeaf(type.callee, graph, fileGraph, guard);
|
|
18
|
+
}
|
|
7
19
|
/**
|
|
8
20
|
* THE single seam for unreducible-leaf identity synthesis (#135 / #274).
|
|
9
21
|
*
|
|
@@ -13,7 +25,8 @@ import { unparsedMemberDefinition, effectiveExportName } from "./member-identity
|
|
|
13
25
|
* synthesize a JSX terminal carrying the identity derived FROM THE TERMINAL
|
|
14
26
|
* ITSELF: the reference is real even though its target can't be parsed
|
|
15
27
|
* (external package, CJS, unresolvable specifier). Every other terminal
|
|
16
|
-
* derives identity from `source
|
|
28
|
+
* derives identity from `source`, falling back to `fallback` only when the
|
|
29
|
+
* branch itself carries none.
|
|
17
30
|
*
|
|
18
31
|
* Do NOT re-derive this salvage logic at individual fold sites — call this
|
|
19
32
|
* seam. Named in docs/CONTEXT.md ("unreducible-leaf identity synthesis").
|
|
@@ -26,27 +39,69 @@ function foldTerminals(graph, fileGraph, results, fallback) {
|
|
|
26
39
|
return { type: { kind: "JSX" }, viaTrail: [], identity };
|
|
27
40
|
}
|
|
28
41
|
}
|
|
42
|
+
// A fan-out branch (Union / dynamic-map) is stamped as `source`. Derive
|
|
43
|
+
// from it FIRST — a branch that has its own binding must win (#131) —
|
|
44
|
+
// and only fall back to the resolved binding when the branch carries no
|
|
45
|
+
// identity of its own (e.g. a bare `{kind:"JSX"}` branch from a lowered
|
|
46
|
+
// ternary/logical render body). Committing to `source ?? fallback` before
|
|
47
|
+
// deriving discarded the fallback entirely once any fanout occurred, even
|
|
48
|
+
// when the branch itself resolved to nothing.
|
|
49
|
+
const identity = (source && deriveIdentity(source, fileGraph, graph)) ?? deriveIdentity(fallback, fileGraph, graph);
|
|
29
50
|
return {
|
|
30
51
|
type: t,
|
|
31
52
|
viaTrail: [],
|
|
32
|
-
identity
|
|
53
|
+
identity,
|
|
33
54
|
};
|
|
34
55
|
});
|
|
35
56
|
}
|
|
36
57
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* pass
|
|
41
|
-
*
|
|
58
|
+
* Fold a wrapper call to its wrapped argument's terminals. Shared by the
|
|
59
|
+
* structural pass-through step (2b) and the opaque-HOC fallback (step 4) so
|
|
60
|
+
* a seen-through wrapper and an opaque one attribute identically. Inner
|
|
61
|
+
* terminals pass through with the wrapper hop prepended. When the inner walk
|
|
62
|
+
* bottomed out at Unknown but carried an identity hint (imported, or
|
|
63
|
+
* local-promoted via #101), synthesise a JSX terminal from it — without this
|
|
64
|
+
* rescue, HOC-wrapped occurrences drop silently (~128 in example-web's
|
|
65
|
+
* connect/flow/withTranslators patterns, diagnosed on PR #141). Truly
|
|
66
|
+
* nothing → a single Unknown terminal, as before.
|
|
42
67
|
*/
|
|
43
|
-
function
|
|
44
|
-
const
|
|
45
|
-
|
|
68
|
+
function foldToWrappedArg(inner, via) {
|
|
69
|
+
const innerHasNonUnknown = inner.some((t) => t.type.kind !== "Unknown");
|
|
70
|
+
if (innerHasNonUnknown) {
|
|
71
|
+
return inner.map((t) => ({ type: t.type, viaTrail: [via, ...t.viaTrail], identity: t.identity }));
|
|
72
|
+
}
|
|
73
|
+
const synthesisIdentity = inner.find((t) => t.identity !== null)?.identity ?? null;
|
|
74
|
+
if (synthesisIdentity) {
|
|
75
|
+
return [{ type: { kind: "JSX" }, viaTrail: [via], identity: synthesisIdentity }];
|
|
76
|
+
}
|
|
77
|
+
return [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Provenance of ONE argument of a wrapper call: the wrapper's `callee` name
|
|
81
|
+
* and the `{ specifier, import }` pair naming the argument's own binding —
|
|
82
|
+
* its import record when it is import-backed, else the anonymous-argument
|
|
83
|
+
* resolution. THE single helper for both via kinds that describe a wrapped
|
|
84
|
+
* argument (`hoc-wrapper` when the wrapper folds to the argument,
|
|
85
|
+
* `passed-as-argument` when the holder is its own identity and the argument
|
|
86
|
+
* is seeded at its site — spec §4), so the two name a binding identically.
|
|
87
|
+
* Never re-derive `{ specifier, import }` for an argument elsewhere.
|
|
88
|
+
*/
|
|
89
|
+
export function argumentProvenance(type, arg, fileGraph) {
|
|
90
|
+
const importRecord = findImportForLastArg(arg, fileGraph);
|
|
46
91
|
const resolved = importRecord
|
|
47
92
|
? { specifier: importRecord.specifier, import: importRecord.imported }
|
|
48
|
-
: resolveAnonymousLastArg(
|
|
49
|
-
return {
|
|
93
|
+
: resolveAnonymousLastArg(arg, fileGraph);
|
|
94
|
+
return { callee: deriveHocCallee(type.callee), specifier: resolved.specifier, import: resolved.import };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Build the `hoc-wrapper` via for a ReturnTypeOf wrapper call. Shared by the
|
|
98
|
+
* structural pass-through (step 2b), the pass-through recovery (step 3b) and
|
|
99
|
+
* the opaque HOC fallback (step 4) so all three paths attribute composition
|
|
100
|
+
* identically; the binding is named through `argumentProvenance`.
|
|
101
|
+
*/
|
|
102
|
+
function buildHocVia(type, lastArg, fileGraph) {
|
|
103
|
+
const { callee, specifier, import: imported } = argumentProvenance(type, lastArg, fileGraph);
|
|
104
|
+
return { kind: "hoc-wrapper", hocCallee: callee, specifier, import: imported };
|
|
50
105
|
}
|
|
51
106
|
/**
|
|
52
107
|
* Stable comparison key for a TerminalIdentity. Mirrors the collapse rule in
|
|
@@ -71,9 +126,13 @@ function identityKey(id) {
|
|
|
71
126
|
* returns a DIFFERENT component fails this and keeps its richer algebra result.
|
|
72
127
|
*/
|
|
73
128
|
function isPassThroughWrapper(algResult, inner) {
|
|
74
|
-
const innerKeys = new Set(inner.filter((t) => t.type.kind !== "Unknown").map((t) => identityKey(t.identity)));
|
|
129
|
+
const innerKeys = new Set(inner.filter((t) => t.type.kind !== "Unknown" && t.identity !== null).map((t) => identityKey(t.identity)));
|
|
75
130
|
const algKeys = algResult.filter((t) => t.type.kind !== "Unknown");
|
|
76
|
-
|
|
131
|
+
// A null identity on either side is "no information", never a match — the
|
|
132
|
+
// old null-equals-null comparison folded factory calls onto their
|
|
133
|
+
// rehydrate/config argument once the callee unwrap stopped inventing an
|
|
134
|
+
// identity for the algebra side.
|
|
135
|
+
return algKeys.length > 0 && algKeys.every((t) => t.identity !== null && innerKeys.has(identityKey(t.identity)));
|
|
77
136
|
}
|
|
78
137
|
/**
|
|
79
138
|
* Walker layered above `resolveType`. Applies three folding rules
|
|
@@ -87,7 +146,21 @@ function isPassThroughWrapper(algResult, inner) {
|
|
|
87
146
|
*
|
|
88
147
|
* See spec: docs/superpowers/specs/2026-05-19-wrapper-folding-engine-extension-design.md §4.3
|
|
89
148
|
*/
|
|
90
|
-
export function walkWithFolding(graph, fileGraph, type, argMap, guard = createCycleGuard()
|
|
149
|
+
export function walkWithFolding(graph, fileGraph, type, argMap, guard = createCycleGuard(),
|
|
150
|
+
// Ruling 20: the identity `foldTerminals` falls back to when a terminal
|
|
151
|
+
// carries no identity of its own (no `source`, no unreducible TypeOf/
|
|
152
|
+
// DynamicImport leaf). Defaults to `type` (the walked node itself) —
|
|
153
|
+
// unchanged behaviour for every caller that doesn't pass this. The
|
|
154
|
+
// `Function` arm below is the ONE place that overrides it: a component's
|
|
155
|
+
// own return (`return priceElement;`, `return contactModals;` — a bare
|
|
156
|
+
// JSX-value alias or a hook-return leaf) must keep deriving identity as
|
|
157
|
+
// "nothing", so the engine's own fallback (the rendered declaration, e.g.
|
|
158
|
+
// `OfferPrice`) wins — never a same-file local alias like `priceElement`
|
|
159
|
+
// that happens to sit in `foldTerminals`' fallback slot. Per-branch
|
|
160
|
+
// identity from a fanout `source` (#131 — Union([TypeOf A, TypeOf B])) is
|
|
161
|
+
// untouched: `source` is checked BEFORE this fallback in `foldTerminals`.
|
|
162
|
+
identityFallback) {
|
|
163
|
+
const fallback = identityFallback ?? type;
|
|
91
164
|
switch (type.kind) {
|
|
92
165
|
case "ReturnTypeOf":
|
|
93
166
|
return walkReturnTypeOf(graph, fileGraph, type, argMap, guard);
|
|
@@ -95,13 +168,165 @@ export function walkWithFolding(graph, fileGraph, type, argMap, guard = createCy
|
|
|
95
168
|
if (type.member === DYNAMIC_MEMBER_KEY) {
|
|
96
169
|
return walkDynamicMember(graph, fileGraph, type, argMap, guard);
|
|
97
170
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
171
|
+
return walkStaticMember(graph, fileGraph, type, argMap, guard, fallback);
|
|
172
|
+
case "Function":
|
|
173
|
+
// Ruling 19: a component's own returns can contain a nested
|
|
174
|
+
// ReturnTypeOf/MemberOf (`List = ({items}) => items.map(render)`) that
|
|
175
|
+
// needs wrapper-folding's steps (the opaque-callee / dynamic-map
|
|
176
|
+
// handling below), not `resolveType`'s plain algebra — `resolveType`'s
|
|
177
|
+
// own "Function" case recurses via plain `resolveType`, which never
|
|
178
|
+
// re-enters this walk, so a JSX usage resolving to such a Function
|
|
179
|
+
// (e.g. `<List/>`) silently produced zero occurrences before this.
|
|
180
|
+
// Mirrors `resolveType`'s "Function" case, one level up. Ruling 20:
|
|
181
|
+
// pass THIS Function as the identity fallback for every return — a
|
|
182
|
+
// Function is never TypeOf/DynamicImport, so `deriveIdentity` on it
|
|
183
|
+
// is always null, matching the pre-ruling-19 semantics this restores.
|
|
184
|
+
return type.returns.flatMap((r) => walkWithFolding(graph, fileGraph, r, argMap, guard, type));
|
|
185
|
+
case "TypeOf":
|
|
186
|
+
return walkTypeOf(graph, fileGraph, type, argMap, guard, fallback);
|
|
187
|
+
case "Union":
|
|
188
|
+
// Structural re-entry (D1/D3): each branch walks through the walker so
|
|
189
|
+
// a TypeOf branch reaches `walkTypeOf` and decides its own identity
|
|
190
|
+
// (per-branch attribution, #131) while a bare JSX branch — a lowered
|
|
191
|
+
// ternary/logical render body — derives from the outer fallback exactly
|
|
192
|
+
// as the algebra's `source ?? fallback` did.
|
|
193
|
+
return type.types.flatMap((branch) => walkWithFolding(graph, fileGraph, branch, argMap, guard, fallback));
|
|
194
|
+
case "ParameterOf":
|
|
195
|
+
case "DynamicImport":
|
|
196
|
+
case "Object":
|
|
197
|
+
case "Array":
|
|
198
|
+
case "JSX":
|
|
199
|
+
case "Str":
|
|
200
|
+
case "Unknown":
|
|
201
|
+
return foldLeaf(graph, fileGraph, type, argMap, guard, fallback);
|
|
202
|
+
default:
|
|
203
|
+
return assertNever(type);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The leaf fold: pure algebra, then identity synthesis through the ONE
|
|
208
|
+
* unreducible-leaf seam (`foldTerminals`). This is the only place the walker
|
|
209
|
+
* hands a subtree to `resolveType`; every structural kind (TypeOf, Union,
|
|
210
|
+
* MemberOf, Function, ReturnTypeOf) has its own arm above and re-enters
|
|
211
|
+
* `walkWithFolding` instead.
|
|
212
|
+
*/
|
|
213
|
+
function foldLeaf(graph, fileGraph, type, argMap, guard, fallback) {
|
|
214
|
+
return foldTerminals(graph, fileGraph, resolveType(graph, fileGraph, type, argMap, guard), fallback);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* THE seam for "walk a Function in its authoring file" (spec D4). A Function
|
|
218
|
+
* declared in another module must walk in THAT module's graph, because the
|
|
219
|
+
* walker's file-scoped lookups inside it — a dynamic map's binding
|
|
220
|
+
* (`findMapBinding`), an import table — only resolve where the code was
|
|
221
|
+
* written. Originally `walkDispatcherReturns`' rationale (#136): a
|
|
222
|
+
* cross-module `function getMapped(k) { return MAP[k]; }` walked in the
|
|
223
|
+
* caller's fileGraph misses `MAP` entirely and loses the `dynamic-map` hop.
|
|
224
|
+
* `walkTypeOf` needs the identical switch, so it is one function, not two.
|
|
225
|
+
* Falls back to the caller's graph when the file is absent or unindexed.
|
|
226
|
+
*/
|
|
227
|
+
function fileGraphForFunction(graph, fileGraph, fn) {
|
|
228
|
+
return fn.enclosingBinding?.file && fn.enclosingBinding.file !== fileGraph.filePath
|
|
229
|
+
? (graph.files.get(fn.enclosingBinding.file) ?? fileGraph)
|
|
230
|
+
: fileGraph;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Structural re-entry for a reference (spec D1–D4, D6; #521). Resolves ONE
|
|
234
|
+
* hop and walks the resolved value through `walkWithFolding`, so every walker
|
|
235
|
+
* rule — wrapper hops, dynamic maps, Ruling 19's `.map()` bodies — applies
|
|
236
|
+
* through an alias exactly as at a direct render site. Before this arm the
|
|
237
|
+
* default fold handed the whole subtree to `resolveType`, which never comes
|
|
238
|
+
* back, so a map-bodied `List` reached via `MAP[k]` died as Unknown.
|
|
239
|
+
*
|
|
240
|
+
* Identity (D2): a terminal that comes back with no identity of its own is
|
|
241
|
+
* stamped with THIS reference's identity only when the reference names a
|
|
242
|
+
* component-shaped value — `evalKind === "component"`, the one component-
|
|
243
|
+
* shape seam, called not re-derived. A reference to a JSX value
|
|
244
|
+
* (`return priceElement`) or to a parameter binding stays null so the outer
|
|
245
|
+
* fallback wins: Ruling 20 by construction rather than by special case.
|
|
246
|
+
*
|
|
247
|
+
* An import the graph cannot see (`resolved` is Unknown) is NOT re-entered
|
|
248
|
+
* (D6): it is the identity-bearing leaf `foldLeaf` synthesises from
|
|
249
|
+
* (#135/#274), and `foldToWrappedArg`'s rescue relies on that shape.
|
|
250
|
+
*
|
|
251
|
+
* Cross-file (D4): a Function authored elsewhere walks in ITS file graph
|
|
252
|
+
* through the `fileGraphForFunction` seam — the same switch
|
|
253
|
+
* `walkDispatcherReturns` makes — so file-scoped lookups inside it (a dynamic
|
|
254
|
+
* map's binding) resolve where they were written.
|
|
255
|
+
*
|
|
256
|
+
* Cycle guard: mirrors `resolveType`'s node guard. `const A = B; const B = A`
|
|
257
|
+
* is legal JS and would otherwise re-enter forever.
|
|
258
|
+
*/
|
|
259
|
+
function walkTypeOf(graph, fileGraph, type, argMap, guard, fallback) {
|
|
260
|
+
if (guard.pushNode(type) === "cycle") {
|
|
261
|
+
return [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
const resolved = resolveReference(graph, fileGraph, type.ref, guard);
|
|
265
|
+
if (resolved.kind === "Unknown") {
|
|
266
|
+
// Pop before delegating: foldLeaf re-enters `resolveType`, which pushes
|
|
267
|
+
// this SAME node onto the guard's path again — leaving it pushed here
|
|
268
|
+
// would self-collide as a spurious cycle (guard tracks by object
|
|
269
|
+
// identity) on every import-backed leaf, not just real cycles. The
|
|
270
|
+
// `finally` below still runs (a no-op double pop) once this returns.
|
|
271
|
+
guard.popNode(type);
|
|
272
|
+
return foldLeaf(graph, fileGraph, type, argMap, guard, fallback);
|
|
273
|
+
}
|
|
274
|
+
const innerGraph = resolved.kind === "Function" ? fileGraphForFunction(graph, fileGraph, resolved) : fileGraph;
|
|
275
|
+
const inner = walkWithFolding(graph, innerGraph, resolved, argMap, guard, fallback);
|
|
276
|
+
if (inner.every((t) => t.identity !== null))
|
|
277
|
+
return inner;
|
|
278
|
+
const self = resolved.kind !== "ParameterOf" && evalKind(resolved, graph, innerGraph, argMap) === "component"
|
|
279
|
+
? deriveIdentity(type, fileGraph, graph)
|
|
280
|
+
: null;
|
|
281
|
+
const stamp = self ?? deriveIdentity(fallback, fileGraph, graph);
|
|
282
|
+
return inner.map((t) => (t.identity === null ? { type: t.type, viaTrail: t.viaTrail, identity: stamp } : t));
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
guard.popNode(type);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Static member access (spec D5; #512). Mirrors `walkDynamicMember` for one
|
|
290
|
+
* named prop and without the `dynamic-map` hop: resolve the object, take the
|
|
291
|
+
* member, walk the member's VALUE through `walkWithFolding` — so a wrapper
|
|
292
|
+
* product stored in a map (`MAP.memo` → `memo(Button)`) reaches
|
|
293
|
+
* `walkReturnTypeOf` and keeps its `hoc-wrapper` hop. Anything that is not an
|
|
294
|
+
* in-graph Object (an Array, an import-backed namespace the graph cannot see)
|
|
295
|
+
* takes the leaf fold exactly as before.
|
|
296
|
+
*
|
|
297
|
+
* Cycle guard: mirrors `walkTypeOf`. `const A = { Item: () => B.Item };
|
|
298
|
+
* const B = { Item: () => A.Item }` is legal, loadable JS and the walk
|
|
299
|
+
* re-enters this arm forever without it — `resolveType` pops every node it
|
|
300
|
+
* pushes, so nothing on such a cycle stays on the guard's path otherwise.
|
|
301
|
+
*/
|
|
302
|
+
function walkStaticMember(graph, fileGraph, type, argMap, guard, fallback) {
|
|
303
|
+
if (guard.pushNode(type) === "cycle") {
|
|
304
|
+
return [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const objects = resolveType(graph, fileGraph, type.obj, argMap, guard)
|
|
308
|
+
.map((r) => r.type)
|
|
309
|
+
.filter((t) => t.kind === "Object");
|
|
310
|
+
if (objects.length === 0) {
|
|
311
|
+
// Pop before delegating: foldLeaf re-enters `resolveType`, which pushes
|
|
312
|
+
// this SAME node onto the guard's path again — leaving it pushed here
|
|
313
|
+
// would self-collide as a spurious cycle (guard tracks by object
|
|
314
|
+
// identity), not just on real cycles. The `finally` below still runs
|
|
315
|
+
// (a no-op double pop) once this returns.
|
|
316
|
+
guard.popNode(type);
|
|
317
|
+
return foldLeaf(graph, fileGraph, type, argMap, guard, fallback);
|
|
104
318
|
}
|
|
319
|
+
const out = [];
|
|
320
|
+
for (const obj of objects) {
|
|
321
|
+
const value = obj.props[type.member];
|
|
322
|
+
if (value === undefined)
|
|
323
|
+
continue;
|
|
324
|
+
out.push(...walkWithFolding(graph, fileGraph, value, argMap, guard, fallback));
|
|
325
|
+
}
|
|
326
|
+
return out.length > 0 ? out : [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
|
|
327
|
+
}
|
|
328
|
+
finally {
|
|
329
|
+
guard.popNode(type);
|
|
105
330
|
}
|
|
106
331
|
}
|
|
107
332
|
function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
|
|
@@ -172,6 +397,23 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
|
|
|
172
397
|
const dispatcherResult = walkDispatcherReturns(graph, fileGraph, type, argMap, guard);
|
|
173
398
|
if (dispatcherResult)
|
|
174
399
|
return dispatcherResult;
|
|
400
|
+
// (2b) Structural pass-through (spec D9 arm 1; #328). When every Function
|
|
401
|
+
// the callee resolves to returns its own parameter (a bare `ParameterOf`, or a `TypeOf` that resolves to one) bound by THIS call,
|
|
402
|
+
// the wrapper is identity-preserving by construction — `c => c`; React's
|
|
403
|
+
// memo/forwardRef reach this arm through a library stub table, regardless
|
|
404
|
+
// of import spelling (named, default, or namespace). Fold straight to that argument and record the wrapper as a
|
|
405
|
+
// hoc-wrapper hop. Decided from the callee's shape, never from whether the
|
|
406
|
+
// algebra happened to succeed, so a local wrapped component and an external
|
|
407
|
+
// one attribute identically. Folding goes through `foldToWrappedArg`, the
|
|
408
|
+
// same tail step 4 uses, so both arms synthesise identity identically.
|
|
409
|
+
const passThroughIndex = passThroughParameterIndex(graph, fileGraph, type, argMap, guard);
|
|
410
|
+
if (passThroughIndex !== null) {
|
|
411
|
+
const wrapped = type.args[passThroughIndex];
|
|
412
|
+
if (wrapped !== undefined) {
|
|
413
|
+
const inner = walkWithFolding(graph, fileGraph, wrapped, argMap, guard);
|
|
414
|
+
return foldToWrappedArg(inner, buildHocVia(type, wrapped, fileGraph));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
175
417
|
// (3) Pure algebra walk. Per-leaf identity uses each terminal's source
|
|
176
418
|
// (the leaf at the innermost fanout fork that produced it) so dynamic-map /
|
|
177
419
|
// Union / Array fanouts attribute each branch to its own binding instead
|
|
@@ -209,30 +451,57 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
|
|
|
209
451
|
return [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
|
|
210
452
|
}
|
|
211
453
|
const inner = walkWithFolding(graph, fileGraph, lastArg, argMap, guard);
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
// AND covers the in-graph case where #101 promoted the inner identity from
|
|
227
|
-
// `imported` to `local` (without this, ~128 HOC-wrapped occurrences in
|
|
228
|
-
// example-web's connect/flow/withTranslators patterns drop silently —
|
|
229
|
-
// diagnosed via location-set diff on PR #141).
|
|
230
|
-
const synthesisIdentity = inner.find((t) => t.identity !== null)?.identity ?? null;
|
|
231
|
-
if (synthesisIdentity) {
|
|
232
|
-
return [{ type: { kind: "JSX" }, viaTrail: [via], identity: synthesisIdentity }];
|
|
454
|
+
// Ruling 19: a data-method render call (`items.map(render)`) is not a
|
|
455
|
+
// wrapper — its result IS the callback's rendered JSX, not a component
|
|
456
|
+
// received as a prop. Return the callback's terminals AS-IS: no
|
|
457
|
+
// `hoc-wrapper` hop, and identity stays null so the engine's own fallback
|
|
458
|
+
// (the declaration being walked, e.g. `List`) wins rather than whatever
|
|
459
|
+
// the callback itself happened to resolve to. Import-backed MemberOf
|
|
460
|
+
// callees (`Sentry.withProfiler`) fall through to the existing fold+hop
|
|
461
|
+
// below — arm 3 (D9) stands for those.
|
|
462
|
+
if (isDataMethodRenderCall(type, graph, fileGraph, guard)) {
|
|
463
|
+
const nonUnknown = inner.filter((t) => t.type.kind !== "Unknown");
|
|
464
|
+
if (nonUnknown.length > 0) {
|
|
465
|
+
return nonUnknown.map((t) => ({ type: t.type, viaTrail: t.viaTrail, identity: null }));
|
|
466
|
+
}
|
|
467
|
+
return [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
|
|
233
468
|
}
|
|
234
|
-
|
|
235
|
-
|
|
469
|
+
return foldToWrappedArg(inner, buildHocVia(type, lastArg, fileGraph));
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Index of the argument an identity-preserving wrapper returns, or null when
|
|
473
|
+
* the callee is not structurally pass-through. Pass-through means: every
|
|
474
|
+
* Function the callee resolves to has ≥1 return, every return reduces (bare, or
|
|
475
|
+
* a `TypeOf` resolved one hop) to a `ParameterOf` whose `fn` is the ref this
|
|
476
|
+
* call binds args to (`bindingRef`), and every return names the same index.
|
|
477
|
+
* Unions, JSX, nested Functions or an unresolvable callee all yield null so the
|
|
478
|
+
* existing algebra / opaque-HOC paths handle them exactly as before.
|
|
479
|
+
*/
|
|
480
|
+
function passThroughParameterIndex(graph, fileGraph, type, argMap, guard) {
|
|
481
|
+
const calleeFns = resolveToFunctions(graph, fileGraph, type.callee, argMap, guard);
|
|
482
|
+
if (calleeFns.length === 0)
|
|
483
|
+
return null;
|
|
484
|
+
let index = null;
|
|
485
|
+
for (const { fn, bindingRef } of calleeFns) {
|
|
486
|
+
if (fn.kind !== "Function" || bindingRef === null || fn.returns.length === 0)
|
|
487
|
+
return null;
|
|
488
|
+
const boundKey = argumentRefKey(bindingRef);
|
|
489
|
+
for (const ret of fn.returns) {
|
|
490
|
+
// The parser lowers `(C) => C` to a TypeOf return whose ref names the
|
|
491
|
+
// parameter binding; the ParameterOf lives on that binding's declaration
|
|
492
|
+
// in the function's body scope. Resolve one hop so real parser output
|
|
493
|
+
// reaches this arm, not only hand-built graphs.
|
|
494
|
+
const leaf = ret.kind === "TypeOf" ? resolveReference(graph, fileGraph, ret.ref, guard) : ret;
|
|
495
|
+
if (leaf.kind !== "ParameterOf")
|
|
496
|
+
return null;
|
|
497
|
+
if (argumentRefKey(leaf.fn) !== boundKey)
|
|
498
|
+
return null;
|
|
499
|
+
if (index !== null && index !== leaf.index)
|
|
500
|
+
return null;
|
|
501
|
+
index = leaf.index;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return index;
|
|
236
505
|
}
|
|
237
506
|
/**
|
|
238
507
|
* Cross-module dispatcher walk (#136). When the callee of a ReturnTypeOf
|
|
@@ -257,9 +526,7 @@ function walkDispatcherReturns(graph, fileGraph, type, argMap, guard) {
|
|
|
257
526
|
for (const { fn, bindingRef } of calleeFns) {
|
|
258
527
|
if (fn.kind !== "Function")
|
|
259
528
|
continue;
|
|
260
|
-
const fnFileGraph =
|
|
261
|
-
? (graph.files.get(fn.enclosingBinding.file) ?? fileGraph)
|
|
262
|
-
: fileGraph;
|
|
529
|
+
const fnFileGraph = fileGraphForFunction(graph, fileGraph, fn);
|
|
263
530
|
if (bindingRef)
|
|
264
531
|
argMap.bind(bindingRef, type.args);
|
|
265
532
|
try {
|
|
@@ -326,36 +593,49 @@ function findFirstDynamicImport(t, fileGraph, depthBudget) {
|
|
|
326
593
|
}
|
|
327
594
|
return null;
|
|
328
595
|
}
|
|
596
|
+
/**
|
|
597
|
+
* Cycle guard: mirrors `walkStaticMember`. A mutually-recursive object map
|
|
598
|
+
* reached through `MAP[k]` re-enters this arm forever without it. No
|
|
599
|
+
* `foldLeaf` path here, so no early pop is needed.
|
|
600
|
+
*/
|
|
329
601
|
function walkDynamicMember(graph, fileGraph, type, argMap, guard) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
602
|
+
if (guard.pushNode(type) === "cycle") {
|
|
603
|
+
return [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
const objResults = resolveType(graph, fileGraph, type.obj, argMap, guard);
|
|
607
|
+
const out = [];
|
|
608
|
+
for (const { type: obj } of objResults) {
|
|
609
|
+
if (obj.kind !== "Object")
|
|
610
|
+
continue;
|
|
611
|
+
const mapBinding = findMapBinding(type.obj, fileGraph);
|
|
612
|
+
const mapName = mapBinding?.symbol ?? "unknown";
|
|
613
|
+
const mapLoc = {
|
|
614
|
+
file: fileGraph.filePath,
|
|
615
|
+
line: mapBinding?.loc.line ?? 0,
|
|
616
|
+
column: mapBinding?.loc.column ?? 0,
|
|
617
|
+
};
|
|
618
|
+
for (const propValue of Object.values(obj.props)) {
|
|
619
|
+
// Recurse via walkWithFolding so identity is derived from each branch's
|
|
620
|
+
// leaf TypeOf (Foo/Bar resolve to their respective bindings).
|
|
621
|
+
const inner = walkWithFolding(graph, fileGraph, propValue, argMap, guard);
|
|
622
|
+
const via = { kind: "dynamic-map", mapName, mapLoc };
|
|
623
|
+
for (const t of inner) {
|
|
624
|
+
if (t.type.kind === "Unknown")
|
|
625
|
+
continue;
|
|
626
|
+
out.push({
|
|
627
|
+
type: t.type,
|
|
628
|
+
viaTrail: [via, ...t.viaTrail],
|
|
629
|
+
identity: t.identity, // per-branch identity, distinct per map value
|
|
630
|
+
});
|
|
631
|
+
}
|
|
355
632
|
}
|
|
356
633
|
}
|
|
634
|
+
return out;
|
|
635
|
+
}
|
|
636
|
+
finally {
|
|
637
|
+
guard.popNode(type);
|
|
357
638
|
}
|
|
358
|
-
return out;
|
|
359
639
|
}
|
|
360
640
|
function findMapBinding(obj, fileGraph) {
|
|
361
641
|
if (obj.kind !== "TypeOf")
|
|
@@ -577,9 +857,18 @@ function memberLocalIdentity(graph, fileGraph, specifier, imported) {
|
|
|
577
857
|
return null;
|
|
578
858
|
return { kind: "local", filePath: member.absFile, export: member.exportName };
|
|
579
859
|
}
|
|
860
|
+
/** Append a residual member chain (#518) to a derived identity's export name. */
|
|
861
|
+
function withResidual(identity, residual) {
|
|
862
|
+
if (identity === null || residual.length === 0)
|
|
863
|
+
return identity;
|
|
864
|
+
return identity.kind === "local"
|
|
865
|
+
? { ...identity, export: compoundExportName(identity.export, residual) }
|
|
866
|
+
: { ...identity, imported: compoundExportName(identity.imported, residual) };
|
|
867
|
+
}
|
|
580
868
|
/**
|
|
581
|
-
*
|
|
582
|
-
*
|
|
869
|
+
* Resolve the input type's leaf TypeOf ref (or DynamicImport) to either an
|
|
870
|
+
* imported or a local identity. Any other shape — a `ReturnTypeOf` included —
|
|
871
|
+
* has no identity of its own and yields null.
|
|
583
872
|
*
|
|
584
873
|
* Switches FileGraph context via `ref.originFile` when set, so refs that have
|
|
585
874
|
* travelled across module boundaries (e.g. cross-module function returns) are
|
|
@@ -587,11 +876,11 @@ function memberLocalIdentity(graph, fileGraph, specifier, imported) {
|
|
|
587
876
|
* that match the ref actually live.
|
|
588
877
|
*/
|
|
589
878
|
export function deriveIdentity(type, fileGraph, graph) {
|
|
590
|
-
//
|
|
591
|
-
//
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
879
|
+
// A ReturnTypeOf never derives identity from its callee (spec D9 arm 2):
|
|
880
|
+
// a resolvable factory call's identity is the declaration holding the call,
|
|
881
|
+
// which every engine push site already supplies as its fallback. Returning
|
|
882
|
+
// null here is what makes that fallback win. (#271, #328)
|
|
883
|
+
const cur = type;
|
|
595
884
|
if (cur.kind === "DynamicImport") {
|
|
596
885
|
const importedName = cur.projection[0] ?? "default";
|
|
597
886
|
const resolvedFile = graph ? resolveImportedFile(graph, fileGraph, cur.specifier, importedName) : null;
|
|
@@ -622,33 +911,39 @@ export function deriveIdentity(type, fileGraph, graph) {
|
|
|
622
911
|
if (imp) {
|
|
623
912
|
// Namespace-import members canonicalise to the member name (#394),
|
|
624
913
|
// matching the direct-import path's `effectiveImported` (engine/index.ts,
|
|
625
|
-
// #362) so both paths mint the same identity.
|
|
914
|
+
// #362) so both paths mint the same identity. The residual chain (#518)
|
|
915
|
+
// is appended AFTER every root lookup, so `Dialog.Popup` on an unparsed
|
|
916
|
+
// package and `NS.Inline` on a workspace member both mirror the direct
|
|
917
|
+
// path.
|
|
626
918
|
const imported = effectiveExportName(imp.imported, cur.ref.memberChain);
|
|
919
|
+
const residual = residualMemberChain(imp.imported, cur.ref.memberChain);
|
|
627
920
|
const resolvedFile = graph ? resolveImportedFile(graph, lookup, imp.specifier, imported) : null;
|
|
628
921
|
const workspaceLocal = workspaceSiblingIdentity(graph, imp.specifier, imported, resolvedFile);
|
|
629
922
|
if (workspaceLocal)
|
|
630
|
-
return workspaceLocal;
|
|
923
|
+
return withResidual(workspaceLocal, residual);
|
|
631
924
|
const relativeLocal = relativeChainLocalIdentity(graph, lookup, imp.specifier, imported);
|
|
632
925
|
if (relativeLocal)
|
|
633
|
-
return relativeLocal;
|
|
926
|
+
return withResidual(relativeLocal, residual);
|
|
634
927
|
const memberLocal = memberLocalIdentity(graph, lookup, imp.specifier, imported);
|
|
635
928
|
if (memberLocal)
|
|
636
|
-
return memberLocal;
|
|
637
|
-
return {
|
|
929
|
+
return withResidual(memberLocal, residual);
|
|
930
|
+
return withResidual({
|
|
638
931
|
kind: "imported",
|
|
639
932
|
specifier: imp.specifier,
|
|
640
933
|
imported,
|
|
641
934
|
resolvedFile,
|
|
642
|
-
};
|
|
935
|
+
}, residual);
|
|
643
936
|
}
|
|
644
|
-
// Check local declarations.
|
|
937
|
+
// Check local declarations. A same-file symbol is never a namespace
|
|
938
|
+
// import, so the whole member chain is residual (mirror of
|
|
939
|
+
// `pushLocalOccurrence`'s fallback, #518) — an empty chain is a no-op.
|
|
645
940
|
for (const [, decl] of lookup.declarations) {
|
|
646
941
|
if (decl && decl.symbol === symbol) {
|
|
647
|
-
return {
|
|
942
|
+
return withResidual({
|
|
648
943
|
kind: "local",
|
|
649
944
|
filePath: lookup.filePath,
|
|
650
945
|
export: decl.exportedAs ?? symbol,
|
|
651
|
-
};
|
|
946
|
+
}, cur.ref.memberChain);
|
|
652
947
|
}
|
|
653
948
|
}
|
|
654
949
|
}
|