@component-compass/reference-graph 0.1.17 → 0.1.19
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/builder.d.ts +7 -1
- package/dist/builder.js +9 -3
- package/dist/builder.js.map +1 -1
- 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 +43 -28
- package/dist/engine/helper-callers.js +94 -96
- package/dist/engine/helper-callers.js.map +1 -1
- package/dist/engine/host-element.d.ts +37 -0
- package/dist/engine/host-element.js +55 -0
- package/dist/engine/host-element.js.map +1 -0
- package/dist/engine/index.d.ts +4 -0
- package/dist/engine/index.js +312 -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 +100 -0
- package/dist/engine/registry.js +242 -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 +6 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/types/file-graph.d.ts +21 -10
- package/package.json +2 -2
package/dist/engine/index.js
CHANGED
|
@@ -1,24 +1,71 @@
|
|
|
1
|
+
import { asTagName, computeOccurrenceId, serialiseComponentId, projectPropUsages, parseCompoundExport } from "@component-compass/plugin-core";
|
|
1
2
|
import { MODULE_SCOPE } from "../index.js";
|
|
2
|
-
import { asTagName, computeOccurrenceId, serialiseComponentId, projectPropUsages } from "@component-compass/plugin-core";
|
|
3
3
|
import { createArgumentMap } from "./argument-map.js";
|
|
4
4
|
import { resolveReference } from "./resolve-reference.js";
|
|
5
|
-
import { walkWithFolding } from "./wrapper-folding.js";
|
|
5
|
+
import { walkWithFolding, argumentProvenance } from "./wrapper-folding.js";
|
|
6
6
|
import { packageNameFromSpecifier, externalSubpath } from "./specifier.js";
|
|
7
7
|
import { followReExportChain } from "./re-export-chain.js";
|
|
8
|
-
import { buildHelperCallers } from "./helper-callers.js";
|
|
8
|
+
import { buildHelperCallers, findLocalDeclaration } from "./helper-callers.js";
|
|
9
9
|
import { resolveOwnerChain } from "./owner-resolution.js";
|
|
10
10
|
import { createCycleGuard } from "./cycle-detection.js";
|
|
11
|
-
import { unparsedMemberDefinition, effectiveExportName } from "./member-identity.js";
|
|
11
|
+
import { unparsedMemberDefinition, effectiveExportName, residualMemberChain, compoundExportName } from "./member-identity.js";
|
|
12
|
+
import { buildComponentRegistry, excludeFoldedHolders, excludeHostElementNames, entryKey } from "./registry.js";
|
|
13
|
+
import { hasOnlyHostElementNames, isHostElementName } from "./host-element.js";
|
|
14
|
+
/**
|
|
15
|
+
* JSX treats a bare, lowercase-initial element name as a host element
|
|
16
|
+
* (`div`, `svg`, `web-button`) — it never names a binding, so it is not an
|
|
17
|
+
* unresolved reference. A member chain (`motion.div`) or a capitalised name
|
|
18
|
+
* is a component reference and IS reported when unbound (#518 D7).
|
|
19
|
+
*/
|
|
20
|
+
function isHostElementRoot(ref) {
|
|
21
|
+
return ref.memberChain.length === 0 && isHostElementName(ref.symbol);
|
|
22
|
+
}
|
|
12
23
|
export function resolve(graph, opts) {
|
|
13
24
|
const occurrences = [];
|
|
14
25
|
const argMap = createArgumentMap();
|
|
15
|
-
|
|
26
|
+
// The registry is the one judge of component-ness (#559): owner
|
|
27
|
+
// classification asks it (through the host-element narrowing, so a
|
|
28
|
+
// lowercase member is a helper for ownership while the base registry still
|
|
29
|
+
// gates the argument-site holder loop below).
|
|
30
|
+
const registry = buildComponentRegistry(graph);
|
|
31
|
+
const helperIndex = buildHelperCallers(graph, excludeHostElementNames(registry, graph));
|
|
32
|
+
// THE admission rule (spec D1/D12, ruling 14: shape-only). A local
|
|
33
|
+
// react-component identity must be component-shaped; consumption is the
|
|
34
|
+
// usage itself (a JSX usage that resolves to a local declaration IS that
|
|
35
|
+
// declaration's consumption — the engine's own alias/union/dynamic-map/
|
|
36
|
+
// re-export/wrapper-fold resolution already did that work, so admission
|
|
37
|
+
// never re-derives it). Everything the registry does not type (externals,
|
|
38
|
+
// custom elements, vue-dialect declarations, workspace-member definitions
|
|
39
|
+
// outside the graph) is admitted as before — except a non-code file, which
|
|
40
|
+
// is never a component.
|
|
41
|
+
//
|
|
42
|
+
// Ruling 17: a workspace-member path outside the graph (the bounded
|
|
43
|
+
// resolver pins it but never parses it — a lazy `import("./PhoneInput")`
|
|
44
|
+
// whose target file wasn't walked) is rejected only when its path HAS an
|
|
45
|
+
// extension that is provably not code (`hero.png`, `.SVG`). An
|
|
46
|
+
// extensionless path (`./PhoneInput` resolved without a suffix — the
|
|
47
|
+
// resolver's own concern, not this rule's) is admitted: it's far more
|
|
48
|
+
// likely an unwalked source file than a non-code asset, and this rule has
|
|
49
|
+
// no way to tell without walking it.
|
|
50
|
+
const CODE_EXT = /\.(?:[cm]?[jt]sx?|vue)$/i;
|
|
51
|
+
const HAS_EXT = /\.[^./\\]+$/;
|
|
52
|
+
const admit = (id) => {
|
|
53
|
+
if (id.kind !== "react-component" || id.source.type !== "local")
|
|
54
|
+
return true;
|
|
55
|
+
const { filePath } = id.source;
|
|
56
|
+
const declaring = graph.files.get(filePath);
|
|
57
|
+
if (declaring === undefined)
|
|
58
|
+
return !HAS_EXT.test(filePath) || CODE_EXT.test(filePath);
|
|
59
|
+
if (declaring.dialect === "vue")
|
|
60
|
+
return true;
|
|
61
|
+
return registry.isComponentShaped(filePath, id.export);
|
|
62
|
+
};
|
|
16
63
|
// Parallel sidecar holding the per-file usage index, kind, and (pre-resolved)
|
|
17
64
|
// parent-usage index for each emitted occurrence. Used in pass 2 to resolve
|
|
18
65
|
// `parentRef`. Held off-band (not on `EngineOccurrence`) so the public type
|
|
19
|
-
// stays clean and scratch state can't leak into output. `null` slots
|
|
20
|
-
//
|
|
21
|
-
//
|
|
66
|
+
// stays clean and scratch state can't leak into output. `null` slots mean
|
|
67
|
+
// the occurrence has no lexical JSX parent — the argument-site seeding pass
|
|
68
|
+
// stamps nothing for its occurrences. Pre-capturing `parentUsageIdx` at
|
|
22
69
|
// stamp time lets pass 2 do an O(1) lookup per occurrence instead of an
|
|
23
70
|
// O(ownership) `.find()` — matters on large files (hundreds of usages).
|
|
24
71
|
const composition = [];
|
|
@@ -39,19 +86,28 @@ export function resolve(graph, opts) {
|
|
|
39
86
|
// same identity AND same owner — always a spurious duplicate. The
|
|
40
87
|
// `composition` sidecar isn't populated for this slice until the stamp loop
|
|
41
88
|
// below, so the compaction only touches `occurrences`.
|
|
42
|
-
|
|
89
|
+
// THE per-slice compaction seam (admission + #248 dedup). Every emission
|
|
90
|
+
// path — a usage's slice via `stampComposition`, a holder's argument-site
|
|
91
|
+
// slice below — routes through it; never re-derive admission or dedup at a
|
|
92
|
+
// push site.
|
|
93
|
+
const compactSlice = (lengthBefore) => {
|
|
43
94
|
const seenOccurrenceIds = new Set();
|
|
44
95
|
let write = lengthBefore;
|
|
45
96
|
for (let read = lengthBefore; read < occurrences.length; read++) {
|
|
46
97
|
const occ = occurrences[read];
|
|
47
98
|
if (occ === undefined)
|
|
48
99
|
continue;
|
|
100
|
+
if (!admit(occ.rawComponentId))
|
|
101
|
+
continue;
|
|
49
102
|
if (seenOccurrenceIds.has(occ.occurrenceId))
|
|
50
103
|
continue;
|
|
51
104
|
seenOccurrenceIds.add(occ.occurrenceId);
|
|
52
105
|
occurrences[write++] = occ;
|
|
53
106
|
}
|
|
54
107
|
occurrences.length = write;
|
|
108
|
+
};
|
|
109
|
+
const stampComposition = (lengthBefore, kind, fileUsageIdx, depth, parentUsageIdx) => {
|
|
110
|
+
compactSlice(lengthBefore);
|
|
55
111
|
for (let i = lengthBefore; i < occurrences.length; i++) {
|
|
56
112
|
const occ = occurrences[i];
|
|
57
113
|
if (!occ)
|
|
@@ -80,8 +136,24 @@ export function resolve(graph, opts) {
|
|
|
80
136
|
if (!matchedImport) {
|
|
81
137
|
// Symbol is locally declared (no import) — emit local identity.
|
|
82
138
|
const decl = findLocalDeclaration(fileGraph, usage.ref);
|
|
83
|
-
if (!decl)
|
|
139
|
+
if (!decl) {
|
|
140
|
+
if (!isHostElementRoot(usage.ref)) {
|
|
141
|
+
// No import record and no declaration in scope: the engine cannot
|
|
142
|
+
// attribute this usage. Surface it (#518 D7 / #111) instead of
|
|
143
|
+
// dropping it silently — `info`, because the usual cause is a
|
|
144
|
+
// global or macro-provided binding the scan cannot see.
|
|
145
|
+
opts?.collector?.emit({
|
|
146
|
+
code: "unresolved-reference",
|
|
147
|
+
severity: "info",
|
|
148
|
+
filePath,
|
|
149
|
+
line: usage.loc.line,
|
|
150
|
+
column: usage.loc.column,
|
|
151
|
+
symbol: usage.ref.symbol,
|
|
152
|
+
memberChain: [...usage.ref.memberChain],
|
|
153
|
+
});
|
|
154
|
+
}
|
|
84
155
|
continue;
|
|
156
|
+
}
|
|
85
157
|
pushLocalOccurrence(occurrences, filePath, usage, decl, graph, fileGraph, argMap, ownerRef, helperIndex, opts, viaOverride);
|
|
86
158
|
stampComposition(jsxLengthBefore, "jsx", usageIdx, jsxDepth, jsxParentUsageIdx);
|
|
87
159
|
continue;
|
|
@@ -91,6 +163,11 @@ export function resolve(graph, opts) {
|
|
|
91
163
|
// every export-name / re-export lookup below keys on this, not the raw
|
|
92
164
|
// `imported` (#362). `via.import` keeps the raw `imported` (provenance).
|
|
93
165
|
const effectiveImported = effectiveExportName(matchedImport.imported, usage.ref.memberChain);
|
|
166
|
+
// #518 D4: lookups below key on `effectiveImported` (the ROOT); the
|
|
167
|
+
// identity stamped on a fallback is the root plus the residual chain.
|
|
168
|
+
// A member the walker resolves to its own declaration never reaches a
|
|
169
|
+
// fallback, so this only names members with no declaration of their own.
|
|
170
|
+
const residual = residualMemberChain(matchedImport.imported, usage.ref.memberChain);
|
|
94
171
|
if (!packageName) {
|
|
95
172
|
// Relative / absolute specifier — always a local file reference.
|
|
96
173
|
// Resolve the target path; normalize absolute → graph-relative if possible.
|
|
@@ -152,7 +229,7 @@ export function resolve(graph, opts) {
|
|
|
152
229
|
};
|
|
153
230
|
const fallbackComponentId = {
|
|
154
231
|
kind: componentKindFor(fileGraph.dialect),
|
|
155
|
-
export: exportName,
|
|
232
|
+
export: compoundExportName(exportName, residual),
|
|
156
233
|
source: { type: "local", filePath: canonicalFile },
|
|
157
234
|
};
|
|
158
235
|
if (viaOverride !== undefined) {
|
|
@@ -235,9 +312,12 @@ export function resolve(graph, opts) {
|
|
|
235
312
|
const jsxTerminals = foldedTerminals.filter((t) => t.type.kind === "JSX");
|
|
236
313
|
if (jsxTerminals.length === 0)
|
|
237
314
|
continue;
|
|
238
|
-
const
|
|
315
|
+
const siblingFallback = workspaceSiblingFallback(graph, resolvedNormTarget, matchedImport, effectiveImported, fileGraph.dialect);
|
|
316
|
+
const localFallback = siblingFallback !== undefined && siblingFallback.kind !== "custom-element"
|
|
317
|
+
? { ...siblingFallback, export: compoundExportName(siblingFallback.export, residual) }
|
|
318
|
+
: siblingFallback;
|
|
239
319
|
for (const ft of jsxTerminals) {
|
|
240
|
-
pushOccurrence(occurrences, filePath, usage, matchedImport, effectiveImported, packageName, graph, fileGraph, ft, ownerRef, helperIndex, opts, localFallback, viaOverride);
|
|
320
|
+
pushOccurrence(occurrences, filePath, usage, matchedImport, compoundExportName(effectiveImported, residual), packageName, graph, fileGraph, ft, ownerRef, helperIndex, opts, localFallback, viaOverride);
|
|
241
321
|
}
|
|
242
322
|
}
|
|
243
323
|
else {
|
|
@@ -255,13 +335,13 @@ export function resolve(graph, opts) {
|
|
|
255
335
|
if (member !== null) {
|
|
256
336
|
const localFallback = {
|
|
257
337
|
kind: componentKindFor(fileGraph.dialect),
|
|
258
|
-
export: member.exportName,
|
|
338
|
+
export: compoundExportName(member.exportName, residual),
|
|
259
339
|
source: { type: "local", filePath: member.absFile },
|
|
260
340
|
};
|
|
261
|
-
pushOccurrence(occurrences, filePath, usage, matchedImport, effectiveImported, packageName, graph, fileGraph, sentinelFt, ownerRef, helperIndex, opts, localFallback, viaOverride);
|
|
341
|
+
pushOccurrence(occurrences, filePath, usage, matchedImport, compoundExportName(effectiveImported, residual), packageName, graph, fileGraph, sentinelFt, ownerRef, helperIndex, opts, localFallback, viaOverride);
|
|
262
342
|
}
|
|
263
343
|
else {
|
|
264
|
-
pushOccurrence(occurrences, filePath, usage, matchedImport, effectiveImported, packageName, graph, fileGraph, sentinelFt, ownerRef, helperIndex, opts, undefined, viaOverride);
|
|
344
|
+
pushOccurrence(occurrences, filePath, usage, matchedImport, compoundExportName(effectiveImported, residual), packageName, graph, fileGraph, sentinelFt, ownerRef, helperIndex, opts, undefined, viaOverride);
|
|
265
345
|
}
|
|
266
346
|
}
|
|
267
347
|
stampComposition(jsxLengthBefore, "jsx", usageIdx, jsxDepth, jsxParentUsageIdx);
|
|
@@ -319,6 +399,80 @@ export function resolve(graph, opts) {
|
|
|
319
399
|
stampComposition(tagLengthBefore, "tag", i, tagDepth, tagParentUsageIdx);
|
|
320
400
|
}
|
|
321
401
|
}
|
|
402
|
+
// Argument-site seeding (spec §4, D4/D5; PR 2 rulings R1–R3). A module-
|
|
403
|
+
// scope registry member whose value is a call — a wrapper product — is a
|
|
404
|
+
// *holder*. Its own fold decides everything: when the fold reaches JSX and
|
|
405
|
+
// every terminal carries neither an identity nor a `hoc-wrapper` hop, the
|
|
406
|
+
// holder is its own identity
|
|
407
|
+
// (D9 arm 2: the callee was visible and built a new function) and credited
|
|
408
|
+
// none of its arguments, so each identifier argument of the OUTERMOST call
|
|
409
|
+
// that resolves to a component gets one occurrence at the argument's own
|
|
410
|
+
// position, owned by the holder. A fold that carried an identity credited
|
|
411
|
+
// an argument already (arm 1 / arm 3) and seeds nothing — a site is never
|
|
412
|
+
// counted twice. The registry gate keeps an unused, unexported holder from
|
|
413
|
+
// owning anything (no phantom owner by construction). Runs after the usage
|
|
414
|
+
// loops so a holder's slice never interleaves with a usage's; there is no
|
|
415
|
+
// composition sidecar entry because an argument site has no lexical JSX
|
|
416
|
+
// parent.
|
|
417
|
+
// #539 D1/D2: the SAME fold answers membership. A holder whose every JSX
|
|
418
|
+
// terminal carries an identity has credited another identity for every
|
|
419
|
+
// render — it is the route to that identity, not a member. Collected here,
|
|
420
|
+
// applied once through `excludeFoldedHolders` at the return below.
|
|
421
|
+
const folded = new Map();
|
|
422
|
+
for (const [filePath, fileGraph] of graph.files) {
|
|
423
|
+
if (fileGraph.dialect === "vue")
|
|
424
|
+
continue;
|
|
425
|
+
for (const decl of fileGraph.declarations.values()) {
|
|
426
|
+
if (decl.scope !== MODULE_SCOPE || decl.value.kind !== "ReturnTypeOf")
|
|
427
|
+
continue;
|
|
428
|
+
if (!registry.hasLocal(filePath, decl.symbol))
|
|
429
|
+
continue;
|
|
430
|
+
const holderJsx = walkWithFolding(graph, fileGraph, decl.value, argMap).filter((t) => t.type.kind === "JSX");
|
|
431
|
+
const foldedAway = foldedHolder(holderJsx, filePath);
|
|
432
|
+
if (foldedAway !== null)
|
|
433
|
+
folded.set(entryKey(filePath, decl.symbol), foldedAway);
|
|
434
|
+
const lengthBefore = occurrences.length;
|
|
435
|
+
pushArgumentSiteOccurrences(occurrences, filePath, decl, decl.value, holderJsx, graph, fileGraph, argMap, opts);
|
|
436
|
+
compactSlice(lengthBefore);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// Hook argument seeding (#561). A component passed to a hook call inside a
|
|
440
|
+
// top-level function body (`const [modal] = useModalTwoStatic(Modal)`) is
|
|
441
|
+
// used at that site: a hook's product is a value the caller interpolates,
|
|
442
|
+
// never a tag, so no render site will ever pay for the argument and R1's
|
|
443
|
+
// "the fold credited it already" reasoning does not apply. Same R2/R3
|
|
444
|
+
// argument rules and the same push seam as the holder loop; the owner is
|
|
445
|
+
// the enclosing declaration through `resolveOwnerChain`, so a helper hook
|
|
446
|
+
// fans the seed out to each caller with a `helper-call` hop. A hook is
|
|
447
|
+
// named by `HOOK_NAME`, React's own `isHookName` predicate (rules-of-hooks
|
|
448
|
+
// and the compiler) — there is no structural signal for "hook".
|
|
449
|
+
for (const [filePath, fileGraph] of graph.files) {
|
|
450
|
+
if (fileGraph.dialect === "vue")
|
|
451
|
+
continue;
|
|
452
|
+
for (const bodyCall of fileGraph.bodyCalls) {
|
|
453
|
+
if (!HOOK_NAME.test(bodyCall.callee.symbol) || bodyCall.args.length === 0)
|
|
454
|
+
continue;
|
|
455
|
+
const call = {
|
|
456
|
+
kind: "ReturnTypeOf",
|
|
457
|
+
callee: { kind: "TypeOf", ref: bodyCall.callee },
|
|
458
|
+
args: bodyCall.args,
|
|
459
|
+
};
|
|
460
|
+
const ownerRef = {
|
|
461
|
+
symbol: bodyCall.ownerSymbol,
|
|
462
|
+
scope: MODULE_SCOPE,
|
|
463
|
+
memberChain: [],
|
|
464
|
+
loc: { line: 0, column: 0 },
|
|
465
|
+
originFile: filePath,
|
|
466
|
+
};
|
|
467
|
+
const owners = () => resolveOwnerChain(ownerRef, graph, helperIndex, createCycleGuard()).map((res) => ({
|
|
468
|
+
rawOwnerComponentId: ownerIdFromResolution(graph, res.ownerDecl),
|
|
469
|
+
viaPrefix: res.viaPrefix,
|
|
470
|
+
}));
|
|
471
|
+
const lengthBefore = occurrences.length;
|
|
472
|
+
pushCallArgumentOccurrences(occurrences, filePath, call, owners, graph, fileGraph, argMap, opts);
|
|
473
|
+
compactSlice(lengthBefore);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
322
476
|
// Pass 2: resolve `parentRef` from per-file `parentUsageIdx` ownership data.
|
|
323
477
|
// Pass 1 populated the `composition` sidecar with `(fileUsageIdx, kind)` per
|
|
324
478
|
// emitted occurrence, which lets us build a `(filePath, kind, fileUsageIdx)
|
|
@@ -343,25 +497,7 @@ export function resolve(graph, opts) {
|
|
|
343
497
|
if (parentIdx !== undefined)
|
|
344
498
|
occ.parentRef = parentIdx;
|
|
345
499
|
}
|
|
346
|
-
return { occurrences };
|
|
347
|
-
}
|
|
348
|
-
/**
|
|
349
|
-
* Walk the scope chain for `ref.symbol` in `fileGraph.declarations` and
|
|
350
|
-
* return the first matching BindingDecl, or null if not found.
|
|
351
|
-
*/
|
|
352
|
-
function findLocalDeclaration(fileGraph, ref) {
|
|
353
|
-
let scope = ref.scope;
|
|
354
|
-
while (true) {
|
|
355
|
-
const decl = fileGraph.declarations.get(`${scope}::${ref.symbol}`);
|
|
356
|
-
if (decl)
|
|
357
|
-
return decl;
|
|
358
|
-
if (scope === MODULE_SCOPE)
|
|
359
|
-
return null;
|
|
360
|
-
const parent = fileGraph.scopes.get(scope)?.parent;
|
|
361
|
-
if (parent === null || parent === undefined)
|
|
362
|
-
return null;
|
|
363
|
-
scope = parent;
|
|
364
|
-
}
|
|
500
|
+
return { occurrences, registry: excludeHostElementNames(excludeFoldedHolders(registry, folded), graph) };
|
|
365
501
|
}
|
|
366
502
|
/**
|
|
367
503
|
* Build a local-scope ComponentId for a symbol declared in `filePath`.
|
|
@@ -389,8 +525,8 @@ function dialectForFile(graph, filePath) {
|
|
|
389
525
|
}
|
|
390
526
|
/**
|
|
391
527
|
* Owner ComponentId for a parser-pre-decided owner reference (#151
|
|
392
|
-
* prop-forward
|
|
393
|
-
*
|
|
528
|
+
* prop-forward path). `fallbackFile` is the consuming file, used when the ref
|
|
529
|
+
* carries no originFile.
|
|
394
530
|
*
|
|
395
531
|
* This and `ownerIdFromResolution` are the ONLY places an occurrence's
|
|
396
532
|
* `rawOwnerComponentId` may be built — the dialect must come from the
|
|
@@ -503,12 +639,16 @@ function relabelExternalLeaf(rawComponentId, filePath, specifier, resolveExterna
|
|
|
503
639
|
rawComponentId.source.type !== "external") {
|
|
504
640
|
return rawComponentId;
|
|
505
641
|
}
|
|
506
|
-
|
|
642
|
+
// A compound export (#518) names a member of the package's ROOT export;
|
|
643
|
+
// the leaf walk resolves the root and the member chain is re-joined onto
|
|
644
|
+
// whatever export the leaf calls it.
|
|
645
|
+
const { root, path } = parseCompoundExport(rawComponentId.export);
|
|
646
|
+
const hit = resolveExternalLeaf(filePath, specifier, root);
|
|
507
647
|
if (!hit || hit.leafPackage === rawComponentId.source.package)
|
|
508
648
|
return rawComponentId;
|
|
509
649
|
return {
|
|
510
650
|
kind: rawComponentId.kind,
|
|
511
|
-
export: hit.exportName,
|
|
651
|
+
export: compoundExportName(hit.exportName, path),
|
|
512
652
|
source: externalSource(hit.leafPackage, `${hit.leafPackage}/${hit.modulePath}`),
|
|
513
653
|
};
|
|
514
654
|
}
|
|
@@ -571,9 +711,10 @@ function computeWraps(rawComponentId, wrapCallback) {
|
|
|
571
711
|
return { kind: "custom-element", tagName: hit.tagName, packageName: hit.packageName };
|
|
572
712
|
}
|
|
573
713
|
function pushOccurrence(occurrences, filePath, usage, matchedImport,
|
|
574
|
-
/** Export name for the fallback identity —
|
|
575
|
-
*
|
|
576
|
-
* `
|
|
714
|
+
/** Export name for the fallback identity — the root export resolved via
|
|
715
|
+
* `effectiveExportName` (#362) joined with the residual member chain via
|
|
716
|
+
* `compoundExportName` (#518). Distinct from `matchedImport.imported`,
|
|
717
|
+
* which stays raw for `via` provenance. */
|
|
577
718
|
effectiveImported, packageName, graph, fileGraph, ft, ownerRef, helperIndex, opts, fallbackOverride, viaOverride) {
|
|
578
719
|
const fallback = fallbackOverride ?? {
|
|
579
720
|
kind: componentKindFor(fileGraph.dialect),
|
|
@@ -646,17 +787,20 @@ effectiveImported, packageName, graph, fileGraph, ft, ownerRef, helperIndex, opt
|
|
|
646
787
|
* Walks the decl's value via walkWithFolding and fans out per JSX terminal.
|
|
647
788
|
*/
|
|
648
789
|
function pushLocalOccurrence(occurrences, filePath, usage, decl, graph, fileGraph, argMap, ownerRef, helperIndex, opts, viaOverride) {
|
|
649
|
-
|
|
790
|
+
// A member access on a same-file binding (`<LocalNS.Leaf/>`) walks the
|
|
791
|
+
// MEMBER, not the whole object — `resolveReference` applies the usage's
|
|
792
|
+
// member chain exactly as the import path already gets it (#520 shape 4,
|
|
793
|
+
// the same static-MemberOf arm as #512). A bare reference is unchanged:
|
|
794
|
+
// `decl.value` is what `resolveReference` returns for an empty chain.
|
|
795
|
+
const value = usage.ref.memberChain.length > 0 ? resolveReference(graph, fileGraph, usage.ref) : decl.value;
|
|
796
|
+
const folded = walkWithFolding(graph, fileGraph, value, argMap);
|
|
650
797
|
const jsxTerminals = folded.filter((t) => t.type.kind === "JSX");
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
pushDynamicBindingOccurrence(occurrences, filePath, usage, decl.dynamicSource, fileGraph.dialect, rawOwnerComponentId);
|
|
657
|
-
}
|
|
798
|
+
// A reference that resolves to no JSX terminal — a function parameter
|
|
799
|
+
// (`<Component/>` inside a factory body), a hook-return destructure, or an
|
|
800
|
+
// unresolvable alias — emits nothing (spec D4/D7). The old dynamic-binding
|
|
801
|
+
// phantom (`{ packageName: null, exportName: "Component" }`) is gone.
|
|
802
|
+
if (jsxTerminals.length === 0)
|
|
658
803
|
return;
|
|
659
|
-
}
|
|
660
804
|
const outerVia = viaOverride ?? { kind: "local-component" };
|
|
661
805
|
if (viaOverride !== undefined) {
|
|
662
806
|
// #151 prop-forward: parser has pre-decided the owner — single-occurrence path.
|
|
@@ -664,7 +808,7 @@ function pushLocalOccurrence(occurrences, filePath, usage, decl, graph, fileGrap
|
|
|
664
808
|
for (const ft of jsxTerminals) {
|
|
665
809
|
const fallback = {
|
|
666
810
|
kind: componentKindFor(fileGraph.dialect),
|
|
667
|
-
export: usage.ref.symbol,
|
|
811
|
+
export: compoundExportName(usage.ref.symbol, usage.ref.memberChain),
|
|
668
812
|
source: { type: "local", filePath },
|
|
669
813
|
};
|
|
670
814
|
const rawComponentId = componentIdFromIdentity(ft.identity, fallback, fileGraph.dialect, {
|
|
@@ -696,7 +840,7 @@ function pushLocalOccurrence(occurrences, filePath, usage, decl, graph, fileGrap
|
|
|
696
840
|
for (const ft of jsxTerminals) {
|
|
697
841
|
const fallback = {
|
|
698
842
|
kind: componentKindFor(fileGraph.dialect),
|
|
699
|
-
export: usage.ref.symbol,
|
|
843
|
+
export: compoundExportName(usage.ref.symbol, usage.ref.memberChain),
|
|
700
844
|
source: { type: "local", filePath },
|
|
701
845
|
};
|
|
702
846
|
const rawComponentId = componentIdFromIdentity(ft.identity, fallback, fileGraph.dialect, {
|
|
@@ -726,55 +870,124 @@ function pushLocalOccurrence(occurrences, filePath, usage, decl, graph, fileGrap
|
|
|
726
870
|
}
|
|
727
871
|
}
|
|
728
872
|
/**
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
* identity is
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
873
|
+
* #539 D1: a holder is folded away iff its fold reaches JSX and EVERY JSX
|
|
874
|
+
* terminal carries an identity — the fold credited someone else for every
|
|
875
|
+
* render. Any identity-less terminal means the holder is its own identity
|
|
876
|
+
* (spec D9 arm 2: `memo(createField(Base))`, `forwardRef((p, r) => <b/>)`).
|
|
877
|
+
* D4: the target is reported only when all terminals name ONE local member of
|
|
878
|
+
* the holder's own file; that member inherits `isDefault` in the seam.
|
|
879
|
+
* Returns `null` when the holder is not folded.
|
|
880
|
+
*/
|
|
881
|
+
function foldedHolder(holderJsx, filePath) {
|
|
882
|
+
if (holderJsx.length === 0 || holderJsx.some((t) => t.identity === null))
|
|
883
|
+
return null;
|
|
884
|
+
const first = holderJsx[0]?.identity;
|
|
885
|
+
const sameFileSingle = first !== undefined &&
|
|
886
|
+
first !== null &&
|
|
887
|
+
first.kind === "local" &&
|
|
888
|
+
first.filePath === filePath &&
|
|
889
|
+
holderJsx.every((t) => t.identity !== null && t.identity.kind === "local" && t.identity.filePath === filePath && t.identity.export === first.export);
|
|
890
|
+
return { target: sameFileSingle ? { filePath, export: first.export } : null };
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Argument-site seeding for one holder (spec §4; PR 2 rulings R1–R3) over the
|
|
894
|
+
* fold the caller already ran (#539 D2) — the rule is stated at the call site
|
|
895
|
+
* in `resolve`. Identity goes through `componentIdFromIdentity` (the
|
|
896
|
+
* external-leaf relabel seam) and the owner through `ownerIdFromRef`, exactly
|
|
897
|
+
* as every other push path; the binding is named through `argumentProvenance`,
|
|
898
|
+
* the same helper `hoc-wrapper` uses. The caller compacts the slice
|
|
899
|
+
* (admission + dedup).
|
|
900
|
+
*/
|
|
901
|
+
function pushArgumentSiteOccurrences(occurrences, filePath, holder, call, holderJsx, graph, fileGraph, argMap, opts) {
|
|
902
|
+
// R1: the holder is its own identity iff its fold reaches JSX and no
|
|
903
|
+
// terminal carries an identity of its own OR a `hoc-wrapper` hop — the hop
|
|
904
|
+
// is the fold's own record that it credited an argument (arm 1 / arm 3),
|
|
905
|
+
// even when that argument was anonymous (`forwardRef((p, r) => <b/>, x)`),
|
|
906
|
+
// so the holder is a wrapper of it and its other arguments are
|
|
907
|
+
// configuration, not wrapped components.
|
|
908
|
+
if (holderJsx.length === 0 ||
|
|
909
|
+
holderJsx.some((t) => t.identity !== null || t.viaTrail.some((v) => v.kind === "hoc-wrapper"))) {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
const holderRef = {
|
|
913
|
+
symbol: holder.symbol,
|
|
914
|
+
scope: MODULE_SCOPE,
|
|
915
|
+
memberChain: [],
|
|
916
|
+
loc: holder.loc,
|
|
917
|
+
originFile: filePath,
|
|
918
|
+
};
|
|
919
|
+
// #540: the holder still seeds (its argument is a real usage), but a
|
|
920
|
+
// declaration that can never be rendered under any of its names is not a
|
|
921
|
+
// component and cannot own — the phantom-owner rule the registry gate
|
|
922
|
+
// above encodes, applied to the one owner this loop builds directly.
|
|
923
|
+
const rawOwnerComponentId = hasOnlyHostElementNames(holder.symbol, fileGraph)
|
|
924
|
+
? undefined
|
|
925
|
+
: ownerIdFromRef(graph, holderRef, filePath);
|
|
926
|
+
pushCallArgumentOccurrences(occurrences, filePath, call, () => [{ rawOwnerComponentId, viaPrefix: [] }], graph, fileGraph, argMap, opts);
|
|
927
|
+
}
|
|
928
|
+
/** React's own hook predicate (`isHookName` in rules-of-hooks and the compiler). */
|
|
929
|
+
const HOOK_NAME = /^use[A-Z0-9]/;
|
|
930
|
+
/**
|
|
931
|
+
* THE argument-site push seam: one `passed-as-argument` occurrence per
|
|
932
|
+
* (identifier argument that resolves to a component) × (owner). The
|
|
933
|
+
* module-scope holder loop and the hook loop in `resolve` both call it;
|
|
934
|
+
* neither re-derives the argument rules or the push.
|
|
735
935
|
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
936
|
+
* R2: the outermost call's arguments only — an inner call's arguments
|
|
937
|
+
* configure the wrapper, they are not wrapped. R3: identifier arguments
|
|
938
|
+
* only, each resolved through the walker so aliases, re-exports and
|
|
939
|
+
* import-backed leaves take the same path a render site does; a null
|
|
940
|
+
* identity is skipped, never invented. R4 (#561): an identifier that is a
|
|
941
|
+
* host-element name (`useSelector(pick, shallowEqual)`) can never be
|
|
942
|
+
* rendered as a component, whatever it resolves to — an external leaf has
|
|
943
|
+
* no shape to check and would otherwise synthesise a component out of any
|
|
944
|
+
* import; the same predicate the held-reference seam and #540 apply.
|
|
738
945
|
*
|
|
739
|
-
* `
|
|
740
|
-
*
|
|
741
|
-
*
|
|
946
|
+
* `owners` is resolved on the first push and reused: most hook calls
|
|
947
|
+
* (`useState(x)`, `useEffect(fn, deps)`) never reach a push, and resolving
|
|
948
|
+
* the owner chain for each of them measured as roughly half of the hook
|
|
949
|
+
* loop's cost on WebClients (#561 PR body, scan time).
|
|
742
950
|
*/
|
|
743
|
-
function
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
951
|
+
function pushCallArgumentOccurrences(occurrences, filePath, call, owners, graph, fileGraph, argMap, opts) {
|
|
952
|
+
let resolvedOwners;
|
|
953
|
+
call.args.forEach((arg, index) => {
|
|
954
|
+
if (arg.kind !== "TypeOf" || isHostElementName(arg.ref.symbol))
|
|
955
|
+
return;
|
|
956
|
+
const outerVia = { kind: "passed-as-argument", index, ...argumentProvenance(call, arg, fileGraph) };
|
|
957
|
+
const argDecl = findLocalDeclaration(fileGraph, arg.ref);
|
|
958
|
+
// Never applied: every terminal used below carries a non-null identity,
|
|
959
|
+
// and `buildComponentId` returns the fallback only for a null one. The
|
|
960
|
+
// relabel seam's signature requires a fallback all the same.
|
|
961
|
+
const unusedFallback = makeLocalComponentId(filePath, arg.ref.symbol, fileGraph.dialect);
|
|
962
|
+
for (const ft of walkWithFolding(graph, fileGraph, arg, argMap)) {
|
|
963
|
+
if (ft.type.kind !== "JSX" || ft.identity === null)
|
|
964
|
+
continue;
|
|
965
|
+
const rawComponentId = componentIdFromIdentity(ft.identity, unusedFallback, fileGraph.dialect, {
|
|
966
|
+
filePath,
|
|
967
|
+
resolveExternalLeaf: opts?.resolveExternalLeaf,
|
|
968
|
+
});
|
|
969
|
+
const wraps = computeWraps(rawComponentId, opts?.wrapCallback);
|
|
970
|
+
resolvedOwners ??= owners();
|
|
971
|
+
for (const { rawOwnerComponentId, viaPrefix } of resolvedOwners) {
|
|
972
|
+
const viaChain = [...viaPrefix, outerVia, ...ft.viaTrail];
|
|
973
|
+
const occurrenceId = computeOccurrenceId(serialiseComponentId(rawComponentId), filePath, arg.ref.loc.line, arg.ref.loc.column, rawOwnerComponentId !== undefined ? serialiseComponentId(rawOwnerComponentId) : undefined);
|
|
974
|
+
occurrences.push({
|
|
975
|
+
rawComponentId,
|
|
976
|
+
filePath,
|
|
977
|
+
line: arg.ref.loc.line,
|
|
978
|
+
column: arg.ref.loc.column,
|
|
979
|
+
via: viaChain[0] ?? outerVia,
|
|
980
|
+
viaChain,
|
|
981
|
+
props: {},
|
|
982
|
+
events: [],
|
|
983
|
+
occurrenceId,
|
|
984
|
+
depth: 0,
|
|
985
|
+
...(argDecl !== null ? { definition: { line: argDecl.loc.line, column: argDecl.loc.column } } : {}),
|
|
986
|
+
...(rawOwnerComponentId !== undefined ? { rawOwnerComponentId } : {}),
|
|
987
|
+
...(wraps !== undefined ? { wraps } : {}),
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
}
|
|
778
991
|
});
|
|
779
992
|
}
|
|
780
993
|
/**
|