@component-compass/reference-graph 0.1.24 → 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.
- package/dist/engine/component-shape.d.ts +26 -2
- package/dist/engine/component-shape.js +73 -10
- package/dist/engine/component-shape.js.map +1 -1
- package/dist/engine/denotation.d.ts +7 -0
- package/dist/engine/denotation.js +15 -0
- package/dist/engine/denotation.js.map +1 -1
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/index.js +353 -261
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/member-identity.d.ts +8 -0
- package/dist/engine/member-identity.js +15 -0
- package/dist/engine/member-identity.js.map +1 -1
- package/dist/engine/registry.d.ts +57 -13
- package/dist/engine/registry.js +105 -57
- package/dist/engine/registry.js.map +1 -1
- package/dist/engine/wrapper-folding.d.ts +10 -3
- package/dist/engine/wrapper-folding.js +133 -76
- package/dist/engine/wrapper-folding.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/package.json +1 -1
package/dist/engine/index.js
CHANGED
|
@@ -8,16 +8,16 @@ import { followReExportChain } from "./re-export-chain.js";
|
|
|
8
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, residualMemberChain, compoundExportName } from "./member-identity.js";
|
|
12
|
-
import { buildComponentRegistry, excludeFoldedHolders, excludeHostElementNames, entryKey } from "./registry.js";
|
|
11
|
+
import { unparsedMemberDefinition, effectiveExportName, residualMemberChain, compoundExportName, memberOfObjectPath } from "./member-identity.js";
|
|
12
|
+
import { buildComponentRegistry, excludeFoldedHolders, excludeHostElementNames, entryKey, findDeclaration } from "./registry.js";
|
|
13
13
|
import { hasOnlyHostElementNames, isHostElementName, HOOK_NAME } from "./host-element.js";
|
|
14
14
|
import { firstDynamicImport } from "./component-shape.js";
|
|
15
|
-
import { creditedTerminals, renderOutcome } from "./denotation.js";
|
|
15
|
+
import { creditedTerminals, isCallable, renderOutcome } from "./denotation.js";
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* A bare, lowercase-initial element name (`div`, `svg`, `web-button`). On a
|
|
18
|
+
* React tag it is the host element: the tag is never evaluated, credited or
|
|
19
|
+
* reported. A member chain (`motion.div`) or a capitalised name is a
|
|
20
|
+
* component reference.
|
|
21
21
|
*/
|
|
22
22
|
function isHostElementRoot(ref) {
|
|
23
23
|
return ref.memberChain.length === 0 && isHostElementName(ref.symbol);
|
|
@@ -25,42 +25,41 @@ function isHostElementRoot(ref) {
|
|
|
25
25
|
export function resolve(graph, opts) {
|
|
26
26
|
const occurrences = [];
|
|
27
27
|
const argMap = createArgumentMap();
|
|
28
|
-
// The registry
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
const
|
|
28
|
+
// The walk reads no registry, owner or helper index, so every JSX tag is
|
|
29
|
+
// evaluated once here, before the registry; the registry reads the
|
|
30
|
+
// declarations the tags landed on as functions, and the render loop below
|
|
31
|
+
// reads the result.
|
|
32
|
+
const tagEvaluations = new Map();
|
|
33
|
+
for (const [filePath, fileGraph] of graph.files) {
|
|
34
|
+
tagEvaluations.set(filePath, fileGraph.jsxUsages.map((usage) => evaluateJsxUsage(graph, filePath, fileGraph, usage, argMap)));
|
|
35
|
+
}
|
|
36
|
+
// The registry is the one judge of component-ness: every declaration a
|
|
37
|
+
// tag landed on as a function is a member. Owner classification asks it
|
|
38
|
+
// (through the host-element narrowing, so a lowercase member is a helper
|
|
39
|
+
// for ownership while the base registry still gates the argument-site
|
|
40
|
+
// holder loop below).
|
|
41
|
+
const registry = buildComponentRegistry(graph, taggedDeclarations(graph, tagEvaluations));
|
|
33
42
|
const helperIndex = buildHelperCallers(graph, excludeHostElementNames(registry, graph));
|
|
34
|
-
// THE admission rule
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// resolver pins it but never parses it — a lazy `import("./PhoneInput")`
|
|
46
|
-
// whose target file wasn't walked) is rejected only when its path HAS an
|
|
47
|
-
// extension that is provably not code (`hero.png`, `.SVG`). An
|
|
48
|
-
// extensionless path (`./PhoneInput` resolved without a suffix — the
|
|
49
|
-
// resolver's own concern, not this rule's) is admitted: it's far more
|
|
50
|
-
// likely an unwalked source file than a non-code asset, and this rule has
|
|
51
|
-
// no way to tell without walking it.
|
|
43
|
+
// THE admission rule: a local react-component identity pinned to a
|
|
44
|
+
// workspace-member path outside the graph (the bounded resolver pins it but
|
|
45
|
+
// never parses it — a lazy `import("./PhoneInput")` whose target file wasn't
|
|
46
|
+
// walked) is rejected when that path HAS an extension that is provably not
|
|
47
|
+
// code (`hero.png`, `.SVG`). An extensionless path (`./PhoneInput` resolved
|
|
48
|
+
// without a suffix — the resolver's own concern, not this rule's) is
|
|
49
|
+
// admitted: it's far more likely an unwalked source file than a non-code
|
|
50
|
+
// asset, and this rule has no way to tell without walking it. Every other
|
|
51
|
+
// identity is admitted: the evaluator decides at the tag, and argument
|
|
52
|
+
// sites and non-`callable` local tag credits ask the registry's component
|
|
53
|
+
// judge (`registryHolds`).
|
|
52
54
|
const CODE_EXT = /\.(?:[cm]?[jt]sx?|vue)$/i;
|
|
53
55
|
const HAS_EXT = /\.[^./\\]+$/;
|
|
54
56
|
const admit = (id) => {
|
|
55
57
|
if (id.kind !== "react-component" || id.source.type !== "local")
|
|
56
58
|
return true;
|
|
57
59
|
const { filePath } = id.source;
|
|
58
|
-
|
|
59
|
-
if (declaring === undefined)
|
|
60
|
-
return !HAS_EXT.test(filePath) || CODE_EXT.test(filePath);
|
|
61
|
-
if (declaring.dialect === "vue")
|
|
60
|
+
if (graph.files.has(filePath))
|
|
62
61
|
return true;
|
|
63
|
-
return
|
|
62
|
+
return !HAS_EXT.test(filePath) || CODE_EXT.test(filePath);
|
|
64
63
|
};
|
|
65
64
|
// Parallel sidecar holding the per-file usage index, kind, and (pre-resolved)
|
|
66
65
|
// parent-usage index for each emitted occurrence. Used in pass 2 to resolve
|
|
@@ -77,10 +76,12 @@ export function resolve(graph, opts) {
|
|
|
77
76
|
// emit so helper-call fanout (multiple occurrences per usage) carries
|
|
78
77
|
// identical composition metadata across the fan.
|
|
79
78
|
//
|
|
80
|
-
// Dedup:
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
79
|
+
// Dedup: one evaluation can credit several terminals naming the same
|
|
80
|
+
// identity — a function walked as a value (an argument, a factory product
|
|
81
|
+
// the algebra flattens) yields one per `return <JSX>` branch, and a
|
|
82
|
+
// same-identity fan-out at a tag (`flag ? Card : Card`) one per branch —
|
|
83
|
+
// emitting N records sharing an `occurrenceId` (componentId + file + line +
|
|
84
|
+
// column + owner).
|
|
84
85
|
// Collapsing is scoped to THIS usage's slice, so genuinely-distinct
|
|
85
86
|
// cross-usage repeats that share an occurrenceId (e.g. a prop-forward
|
|
86
87
|
// of one `const x = <Y/>` literal into two sibling call sites) are separate
|
|
@@ -89,7 +90,7 @@ export function resolve(graph, opts) {
|
|
|
89
90
|
// `composition` sidecar isn't populated for this slice until the stamp loop
|
|
90
91
|
// below, so the compaction only touches `occurrences`.
|
|
91
92
|
// THE per-slice compaction seam (admission + dedup). Every emission
|
|
92
|
-
// path — a usage's slice via `stampComposition`,
|
|
93
|
+
// path — a usage's slice via `stampComposition`, each argument-site loop's
|
|
93
94
|
// slice below — routes through it; never re-derive admission or dedup at a
|
|
94
95
|
// push site.
|
|
95
96
|
const compactSlice = (lengthBefore) => {
|
|
@@ -121,174 +122,21 @@ export function resolve(graph, opts) {
|
|
|
121
122
|
}
|
|
122
123
|
return occurrences.length - lengthBefore;
|
|
123
124
|
};
|
|
124
|
-
const pushJsxUsageOccurrences = (filePath, fileGraph, usage, ownership) => {
|
|
125
|
-
// Resolve owner symbol ref for this usage (for composition attribution).
|
|
126
|
-
const ownerRef = ownership?.ownerSymbolRef ?? null;
|
|
127
|
-
const viaOverride = ownership?.viaOverride;
|
|
128
|
-
// Find the import record that introduced the root symbol at module scope.
|
|
129
|
-
// We check usage.ref.symbol (the root binding name), not member-chain
|
|
130
|
-
// segments, because ImportRecord.local binds the top-level name.
|
|
131
|
-
const matchedImport = fileGraph.importsByLocal.get(usage.ref.symbol);
|
|
132
|
-
if (!matchedImport) {
|
|
133
|
-
// Symbol is locally declared (no import) — emit local identity.
|
|
134
|
-
const decl = findLocalDeclaration(fileGraph, usage.ref);
|
|
135
|
-
if (!decl)
|
|
136
|
-
return { kind: "unbound" };
|
|
137
|
-
return {
|
|
138
|
-
kind: "evaluated",
|
|
139
|
-
...pushLocalOccurrence(occurrences, filePath, usage, decl, graph, fileGraph, argMap, ownerRef, helperIndex, opts, viaOverride),
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
const packageName = packageNameFromSpecifier(matchedImport.specifier);
|
|
143
|
-
// Resolve namespace-import members to their export name once, up front —
|
|
144
|
-
// every export-name / re-export lookup below keys on this, not the raw
|
|
145
|
-
// `imported`. `via.import` keeps the raw `imported` (provenance).
|
|
146
|
-
const effectiveImported = effectiveExportName(matchedImport.imported, usage.ref.memberChain);
|
|
147
|
-
// Lookups below key on `effectiveImported` (the ROOT); the
|
|
148
|
-
// identity stamped on a fallback is the root plus the residual chain.
|
|
149
|
-
// A member the walker resolves to its own declaration never reaches a
|
|
150
|
-
// fallback, so this only names members with no declaration of their own.
|
|
151
|
-
const residual = residualMemberChain(matchedImport.imported, usage.ref.memberChain);
|
|
152
|
-
const holderDefinition = (file, exportName) => {
|
|
153
|
-
if (residual.length === 0)
|
|
154
|
-
return undefined;
|
|
155
|
-
const loc = graph.files.get(file)?.declarations.get(`${MODULE_SCOPE}::${exportName}`)?.loc;
|
|
156
|
-
return loc === undefined ? undefined : { line: loc.line, column: loc.column };
|
|
157
|
-
};
|
|
158
|
-
if (!packageName) {
|
|
159
|
-
// Relative / absolute specifier — always a local file reference.
|
|
160
|
-
// Resolve the target path; normalize absolute → graph-relative if possible.
|
|
161
|
-
const resolvedAbsTarget = graph.moduleResolver(filePath, matchedImport.specifier);
|
|
162
|
-
if (!resolvedAbsTarget)
|
|
163
|
-
return { kind: "import-failed", specifier: matchedImport.specifier };
|
|
164
|
-
const resolvedTarget = graph.resolveToGraphKey
|
|
165
|
-
? (graph.resolveToGraphKey(resolvedAbsTarget) ?? resolvedAbsTarget)
|
|
166
|
-
: resolvedAbsTarget;
|
|
167
|
-
// Resolve the type to confirm it's a JSX component. Cross-file resolution
|
|
168
|
-
// works when resolveToGraphKey is present (graph keys are repo-relative).
|
|
169
|
-
const refType = resolveReference(graph, fileGraph, usage.ref);
|
|
170
|
-
const evaluation = walkWithFolding(graph, fileGraph, refType, argMap);
|
|
171
|
-
const jsxTerminals = creditedTerminals(evaluation);
|
|
172
|
-
if (jsxTerminals.length === 0)
|
|
173
|
-
return { kind: "evaluated", evaluation, value: refType };
|
|
174
|
-
// Determine the canonical source file + local export name. When the
|
|
175
|
-
// target is a re-export barrel (`export { X } from './source'`),
|
|
176
|
-
// follow the chain to the actual definition file so JSX edges
|
|
177
|
-
// attribute to the source rather than the barrel. Without
|
|
178
|
-
// this, two consumers importing the same component through different
|
|
179
|
-
// re-export paths fragment across phantom identities.
|
|
180
|
-
const chained = followReExportChain(graph, resolvedTarget, effectiveImported);
|
|
181
|
-
const canonicalFile = chained?.file ?? resolvedTarget;
|
|
182
|
-
let exportName = chained?.localExport ?? effectiveImported;
|
|
183
|
-
// When the re-export chain didn't follow (no chain present), still
|
|
184
|
-
// look up the local export name at the resolved target to match the
|
|
185
|
-
// local-index seed's hash convention.
|
|
186
|
-
if (!chained) {
|
|
187
|
-
const targetGraph = graph.files.get(resolvedTarget);
|
|
188
|
-
if (targetGraph && effectiveImported === "default") {
|
|
189
|
-
const defaultExp = targetGraph.exports.find((e) => e.kind === "default");
|
|
190
|
-
if (defaultExp && "local" in defaultExp) {
|
|
191
|
-
exportName = defaultExp.local;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
else if (targetGraph) {
|
|
195
|
-
const namedExp = targetGraph.exports.find((e) => e.kind === "named" && "exportedAs" in e && e.exportedAs === effectiveImported && "local" in e);
|
|
196
|
-
if (namedExp && "local" in namedExp) {
|
|
197
|
-
exportName = namedExp.local;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
const outerVia = viaOverride ?? {
|
|
202
|
-
kind: "direct-import",
|
|
203
|
-
specifier: matchedImport.specifier,
|
|
204
|
-
import: matchedImport.imported,
|
|
205
|
-
};
|
|
206
|
-
const fallbackComponentId = {
|
|
207
|
-
kind: componentKindFor(fileGraph.dialect),
|
|
208
|
-
export: compoundExportName(exportName, residual),
|
|
209
|
-
source: { type: "local", filePath: canonicalFile },
|
|
210
|
-
};
|
|
211
|
-
const site = usageSite(filePath, usage);
|
|
212
|
-
const owners = usageOwners(graph, helperIndex, ownerRef, viaOverride, fileGraph.filePath);
|
|
213
|
-
const definition = holderDefinition(canonicalFile, exportName);
|
|
214
|
-
for (const ft of jsxTerminals) {
|
|
215
|
-
const rawComponentId = componentIdFromIdentity(ft.identity, fallbackComponentId, fileGraph.dialect, {
|
|
216
|
-
filePath,
|
|
217
|
-
resolveExternalLeaf: opts?.resolveExternalLeaf,
|
|
218
|
-
});
|
|
219
|
-
emitOccurrence(occurrences, site, rawComponentId, owners, viaOverride !== undefined ? [viaOverride] : [outerVia, ...ft.viaTrail], ft.identity === null ? definition : undefined);
|
|
220
|
-
}
|
|
221
|
-
return { kind: "evaluated", evaluation, value: refType };
|
|
222
|
-
}
|
|
223
|
-
// Check whether the import resolves to a local file that IS in the graph.
|
|
224
|
-
// If it does, resolve properly to avoid false positives (e.g. importing a
|
|
225
|
-
// utility function that happens to be named with a capital letter from a
|
|
226
|
-
// workspace sibling). For imports whose target is outside the graph
|
|
227
|
-
// (external packages in node_modules), trust the JSX call site directly.
|
|
228
|
-
const resolvedAbsTarget = graph.moduleResolver(filePath, matchedImport.specifier);
|
|
229
|
-
// Normalize absolute path → repo-relative graph key when possible.
|
|
230
|
-
const resolvedNormTarget = resolvedAbsTarget !== null && graph.resolveToGraphKey
|
|
231
|
-
? (graph.resolveToGraphKey(resolvedAbsTarget) ?? resolvedAbsTarget)
|
|
232
|
-
: resolvedAbsTarget;
|
|
233
|
-
const targetInGraph = resolvedNormTarget !== null && graph.files.has(resolvedNormTarget);
|
|
234
|
-
if (targetInGraph) {
|
|
235
|
-
// Target file is in graph — workspace sibling. Resolve to check
|
|
236
|
-
// JSX type, fan out per terminal. The fallback identity is local so
|
|
237
|
-
// null-identity terminals still attribute to the sibling file rather
|
|
238
|
-
// than a phantom external stub. Terminal identities from deriveIdentity
|
|
239
|
-
// are already local when applicable (workspaceSiblingIdentity fires).
|
|
240
|
-
const refType = resolveReference(graph, fileGraph, usage.ref);
|
|
241
|
-
const evaluation = walkWithFolding(graph, fileGraph, refType, argMap);
|
|
242
|
-
const jsxTerminals = creditedTerminals(evaluation);
|
|
243
|
-
if (jsxTerminals.length === 0)
|
|
244
|
-
return { kind: "evaluated", evaluation, value: refType };
|
|
245
|
-
const siblingFallback = workspaceSiblingFallback(graph, resolvedNormTarget, matchedImport, effectiveImported, fileGraph.dialect);
|
|
246
|
-
const localFallback = siblingFallback !== undefined && siblingFallback.kind !== "custom-element"
|
|
247
|
-
? { ...siblingFallback, export: compoundExportName(siblingFallback.export, residual) }
|
|
248
|
-
: siblingFallback;
|
|
249
|
-
const definition = siblingFallback !== undefined && siblingFallback.kind !== "custom-element" && siblingFallback.source.type === "local"
|
|
250
|
-
? holderDefinition(siblingFallback.source.filePath, siblingFallback.export)
|
|
251
|
-
: undefined;
|
|
252
|
-
for (const ft of jsxTerminals) {
|
|
253
|
-
pushOccurrence(occurrences, filePath, usage, matchedImport, compoundExportName(effectiveImported, residual), packageName, graph, fileGraph, ft, ownerRef, helperIndex, opts, localFallback, viaOverride, ft.identity === null ? definition : undefined);
|
|
254
|
-
}
|
|
255
|
-
return { kind: "evaluated", evaluation, value: refType };
|
|
256
|
-
}
|
|
257
|
-
// Target not in graph. Workspace membership: a
|
|
258
|
-
// resolved target owned by a workspace member is first-party — emit
|
|
259
|
-
// LOCAL, pinned to the definition file (bounded re-export resolution)
|
|
260
|
-
// so identity is scope-invariant between whole-repo and single-app
|
|
261
|
-
// scans. Otherwise: external, trusting the JSX call site (external
|
|
262
|
-
// package or unresolvable specifier — identity null →
|
|
263
|
-
// matchedImport-based componentId). See `unparsedMemberDefinition`.
|
|
264
|
-
const member = resolvedAbsTarget !== null
|
|
265
|
-
? unparsedMemberDefinition(graph, resolvedAbsTarget, effectiveImported)
|
|
266
|
-
: null;
|
|
267
|
-
const sentinelFt = { denotation: { kind: "foreign" }, viaTrail: [], identity: null };
|
|
268
|
-
if (member !== null) {
|
|
269
|
-
const localFallback = {
|
|
270
|
-
kind: componentKindFor(fileGraph.dialect),
|
|
271
|
-
export: compoundExportName(member.exportName, residual),
|
|
272
|
-
source: { type: "local", filePath: member.absFile },
|
|
273
|
-
};
|
|
274
|
-
pushOccurrence(occurrences, filePath, usage, matchedImport, compoundExportName(effectiveImported, residual), packageName, graph, fileGraph, sentinelFt, ownerRef, helperIndex, opts, localFallback, viaOverride);
|
|
275
|
-
}
|
|
276
|
-
else {
|
|
277
|
-
pushOccurrence(occurrences, filePath, usage, matchedImport, compoundExportName(effectiveImported, residual), packageName, graph, fileGraph, sentinelFt, ownerRef, helperIndex, opts, undefined, viaOverride);
|
|
278
|
-
}
|
|
279
|
-
return { kind: "evaluated", evaluation: [sentinelFt], value: null };
|
|
280
|
-
};
|
|
281
125
|
for (const [filePath, fileGraph] of graph.files) {
|
|
126
|
+
const evaluations = tagEvaluations.get(filePath) ?? [];
|
|
282
127
|
for (let usageIdx = 0; usageIdx < fileGraph.jsxUsages.length; usageIdx++) {
|
|
283
128
|
const usage = fileGraph.jsxUsages[usageIdx];
|
|
284
|
-
|
|
129
|
+
const evaluated = evaluations[usageIdx];
|
|
130
|
+
if (!usage || !evaluated)
|
|
285
131
|
continue;
|
|
286
132
|
const ownership = fileGraph.ownership[usageIdx];
|
|
287
133
|
const lengthBefore = occurrences.length;
|
|
288
|
-
|
|
134
|
+
if (evaluated.kind === "evaluated") {
|
|
135
|
+
emitTagCredits(occurrences, graph, filePath, fileGraph, usage, ownership, evaluated.credits, helperIndex, registry, opts);
|
|
136
|
+
}
|
|
289
137
|
const admitted = stampComposition(lengthBefore, "jsx", usageIdx, ownership?.depth ?? 0, ownership?.parentUsageIdx);
|
|
290
138
|
if (admitted === 0)
|
|
291
|
-
reportDroppedRender(opts, filePath, fileGraph.dialect, usage,
|
|
139
|
+
reportDroppedRender(opts, filePath, fileGraph.dialect, usage, evaluated);
|
|
292
140
|
}
|
|
293
141
|
// Tag-usage resolution (Vue auto-imports, Lit tag-only references, HTML).
|
|
294
142
|
for (let i = 0; i < fileGraph.tagUsages.length; i++) {
|
|
@@ -377,7 +225,7 @@ export function resolve(graph, opts) {
|
|
|
377
225
|
if (foldedAway !== null)
|
|
378
226
|
folded.set(entryKey(filePath, decl.symbol), foldedAway);
|
|
379
227
|
const lengthBefore = occurrences.length;
|
|
380
|
-
pushArgumentSiteOccurrences(occurrences, filePath, decl, decl.value, holderJsx, graph, fileGraph, argMap, opts);
|
|
228
|
+
pushArgumentSiteOccurrences(occurrences, filePath, decl, decl.value, holderJsx, graph, fileGraph, argMap, registry, opts);
|
|
381
229
|
compactSlice(lengthBefore);
|
|
382
230
|
}
|
|
383
231
|
}
|
|
@@ -411,7 +259,7 @@ export function resolve(graph, opts) {
|
|
|
411
259
|
};
|
|
412
260
|
const owners = () => chainOwners(graph, helperIndex, ownerRef);
|
|
413
261
|
const lengthBefore = occurrences.length;
|
|
414
|
-
pushCallArgumentOccurrences(occurrences, filePath, call, owners, graph, fileGraph, argMap, opts);
|
|
262
|
+
pushCallArgumentOccurrences(occurrences, filePath, call, owners, graph, fileGraph, argMap, registry, opts);
|
|
415
263
|
compactSlice(lengthBefore);
|
|
416
264
|
}
|
|
417
265
|
}
|
|
@@ -685,47 +533,297 @@ function usageOwners(graph, helperIndex, ownerRef, viaOverride, fallbackFile) {
|
|
|
685
533
|
}
|
|
686
534
|
return chainOwners(graph, helperIndex, ownerRef);
|
|
687
535
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
*
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
536
|
+
/** A compound name (`NS.Member`) names a member of an object literal: the
|
|
537
|
+
* holder's value, followed down `path` (`memberOfObjectPath`), is an `Object`
|
|
538
|
+
* at every step and has the member. An empty path names the holder itself. */
|
|
539
|
+
function namesObjectMember(holder, path) {
|
|
540
|
+
return path.length === 0 || memberOfObjectPath(holder, path) !== undefined;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Evaluate one JSX usage's tag and name each credited terminal's fallback
|
|
544
|
+
* identity, chain head and `definition`. A bare lowercase React tag is
|
|
545
|
+
* `host-element` and is not evaluated; a Vue usage's reference names a
|
|
546
|
+
* binding, so the rule does not apply to it. Every walk here is in tag
|
|
547
|
+
* position. A same-file symbol walks its declaration, or the member its member
|
|
548
|
+
* chain names; an undeclared one is `unbound`. A relative import walks the
|
|
549
|
+
* reference and falls back to the definition file behind any re-export chain;
|
|
550
|
+
* one that does not resolve is `import-failed`. A package import whose target
|
|
551
|
+
* is in the graph (a workspace sibling) walks the reference and falls back to
|
|
552
|
+
* the sibling, else the external package. A package import whose target is
|
|
553
|
+
* outside the graph is not walked: it credits a `foreign` sentinel that falls
|
|
554
|
+
* back to the workspace member's definition, else the external package. A
|
|
555
|
+
* terminal with no identity of its own is credited under a compound fallback
|
|
556
|
+
* on a local or in-graph holder only when that holder names an object member
|
|
557
|
+
* there (`namesObjectMember`).
|
|
558
|
+
*/
|
|
559
|
+
function evaluateJsxUsage(graph, filePath, fileGraph, usage, argMap) {
|
|
560
|
+
if (fileGraph.dialect === "react" && isHostElementRoot(usage.ref))
|
|
561
|
+
return { kind: "host-element" };
|
|
562
|
+
// Find the import record that introduced the root symbol at module scope.
|
|
563
|
+
// We check usage.ref.symbol (the root binding name), not member-chain
|
|
564
|
+
// segments, because ImportRecord.local binds the top-level name.
|
|
565
|
+
const matchedImport = fileGraph.importsByLocal.get(usage.ref.symbol);
|
|
566
|
+
if (!matchedImport) {
|
|
567
|
+
// Symbol is locally declared (no import) — local identity.
|
|
568
|
+
const decl = findLocalDeclaration(fileGraph, usage.ref);
|
|
569
|
+
if (!decl)
|
|
570
|
+
return { kind: "unbound" };
|
|
571
|
+
// A member access on a same-file binding (`<LocalNS.Leaf/>`) walks the
|
|
572
|
+
// MEMBER, not the whole object — `resolveReference` applies the usage's
|
|
573
|
+
// member chain exactly as the import path already gets it (the same
|
|
574
|
+
// static-MemberOf arm). A bare reference is unchanged:
|
|
575
|
+
// `decl.value` is what `resolveReference` returns for an empty chain.
|
|
576
|
+
const value = usage.ref.memberChain.length > 0 ? resolveReference(graph, fileGraph, usage.ref) : decl.value;
|
|
577
|
+
const evaluation = walkWithFolding(graph, fileGraph, value, argMap, createCycleGuard(), undefined, "tag");
|
|
578
|
+
const fallback = {
|
|
579
|
+
kind: componentKindFor(fileGraph.dialect),
|
|
580
|
+
export: compoundExportName(usage.ref.symbol, usage.ref.memberChain),
|
|
581
|
+
source: { type: "local", filePath },
|
|
582
|
+
};
|
|
583
|
+
const definition = { line: decl.loc.line, column: decl.loc.column };
|
|
584
|
+
const outerVia = { kind: "local-component" };
|
|
585
|
+
const rendered = usage.ref.memberChain.length === 0 ? { declaration: decl } : {};
|
|
586
|
+
const named = namesObjectMember(decl.value, usage.ref.memberChain);
|
|
587
|
+
const credits = creditedTerminals(evaluation).filter((t) => t.identity !== null || named).map((terminal) => ({
|
|
588
|
+
terminal,
|
|
589
|
+
fallback,
|
|
590
|
+
outerVia,
|
|
591
|
+
...(terminal.identity === null ? { definition, ...rendered } : {}),
|
|
592
|
+
}));
|
|
593
|
+
return { kind: "evaluated", evaluation, value, credits };
|
|
594
|
+
}
|
|
595
|
+
const packageName = packageNameFromSpecifier(matchedImport.specifier);
|
|
596
|
+
// Resolve namespace-import members to their export name once, up front —
|
|
597
|
+
// every export-name / re-export lookup below keys on this, not the raw
|
|
598
|
+
// `imported`. `via.import` keeps the raw `imported` (provenance).
|
|
599
|
+
const effectiveImported = effectiveExportName(matchedImport.imported, usage.ref.memberChain);
|
|
600
|
+
// Lookups below key on `effectiveImported` (the ROOT); the
|
|
601
|
+
// identity stamped on a fallback is the root plus the residual chain.
|
|
602
|
+
// A member the walker resolves to its own declaration never reaches a
|
|
603
|
+
// fallback, so this only names members with no declaration of their own.
|
|
604
|
+
const residual = residualMemberChain(matchedImport.imported, usage.ref.memberChain);
|
|
605
|
+
const holderDecl = (file, exportName) => {
|
|
606
|
+
const holderFile = graph.files.get(file);
|
|
607
|
+
return holderFile === undefined ? undefined : findDeclaration(holderFile, exportName);
|
|
698
608
|
};
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
609
|
+
const holderDefinition = (file, exportName) => {
|
|
610
|
+
if (residual.length === 0)
|
|
611
|
+
return undefined;
|
|
612
|
+
const loc = holderDecl(file, exportName)?.loc;
|
|
613
|
+
return loc === undefined ? undefined : { line: loc.line, column: loc.column };
|
|
614
|
+
};
|
|
615
|
+
const namesHolderMember = (file, exportName) => namesObjectMember(holderDecl(file, exportName)?.value, residual);
|
|
616
|
+
const outerVia = {
|
|
705
617
|
kind: "direct-import",
|
|
706
618
|
specifier: matchedImport.specifier,
|
|
707
619
|
import: matchedImport.imported,
|
|
708
620
|
};
|
|
709
|
-
|
|
621
|
+
if (!packageName) {
|
|
622
|
+
// Relative / absolute specifier — always a local file reference.
|
|
623
|
+
// Resolve the target path; normalize absolute → graph-relative if possible.
|
|
624
|
+
const resolvedAbsTarget = graph.moduleResolver(filePath, matchedImport.specifier);
|
|
625
|
+
if (!resolvedAbsTarget)
|
|
626
|
+
return { kind: "import-failed", specifier: matchedImport.specifier };
|
|
627
|
+
const resolvedTarget = graph.resolveToGraphKey
|
|
628
|
+
? (graph.resolveToGraphKey(resolvedAbsTarget) ?? resolvedAbsTarget)
|
|
629
|
+
: resolvedAbsTarget;
|
|
630
|
+
// Resolve the type to confirm it's a JSX component. Cross-file resolution
|
|
631
|
+
// works when resolveToGraphKey is present (graph keys are repo-relative).
|
|
632
|
+
const refType = resolveReference(graph, fileGraph, usage.ref);
|
|
633
|
+
const evaluation = walkWithFolding(graph, fileGraph, refType, argMap, createCycleGuard(), undefined, "tag");
|
|
634
|
+
const jsxTerminals = creditedTerminals(evaluation);
|
|
635
|
+
if (jsxTerminals.length === 0)
|
|
636
|
+
return { kind: "evaluated", evaluation, value: refType, credits: [] };
|
|
637
|
+
// Determine the canonical source file + local export name. When the
|
|
638
|
+
// target is a re-export barrel (`export { X } from './source'`),
|
|
639
|
+
// follow the chain to the actual definition file so JSX edges
|
|
640
|
+
// attribute to the source rather than the barrel. Without
|
|
641
|
+
// this, two consumers importing the same component through different
|
|
642
|
+
// re-export paths fragment across phantom identities.
|
|
643
|
+
const chained = followReExportChain(graph, resolvedTarget, effectiveImported);
|
|
644
|
+
const canonicalFile = chained?.file ?? resolvedTarget;
|
|
645
|
+
let exportName = chained?.localExport ?? effectiveImported;
|
|
646
|
+
// When the re-export chain didn't follow (no chain present), still
|
|
647
|
+
// look up the local export name at the resolved target to match the
|
|
648
|
+
// local-index seed's hash convention.
|
|
649
|
+
if (!chained) {
|
|
650
|
+
const targetGraph = graph.files.get(resolvedTarget);
|
|
651
|
+
if (targetGraph && effectiveImported === "default") {
|
|
652
|
+
const defaultExp = targetGraph.exports.find((e) => e.kind === "default");
|
|
653
|
+
if (defaultExp && "local" in defaultExp) {
|
|
654
|
+
exportName = defaultExp.local;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
else if (targetGraph) {
|
|
658
|
+
const namedExp = targetGraph.exports.find((e) => e.kind === "named" && "exportedAs" in e && e.exportedAs === effectiveImported && "local" in e);
|
|
659
|
+
if (namedExp && "local" in namedExp) {
|
|
660
|
+
exportName = namedExp.local;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
const fallback = {
|
|
665
|
+
kind: componentKindFor(fileGraph.dialect),
|
|
666
|
+
export: compoundExportName(exportName, residual),
|
|
667
|
+
source: { type: "local", filePath: canonicalFile },
|
|
668
|
+
};
|
|
669
|
+
const definition = holderDefinition(canonicalFile, exportName);
|
|
670
|
+
const named = namesHolderMember(canonicalFile, exportName);
|
|
671
|
+
const credits = jsxTerminals.filter((t) => t.identity !== null || named).map((terminal) => ({
|
|
672
|
+
terminal,
|
|
673
|
+
fallback,
|
|
674
|
+
outerVia,
|
|
675
|
+
...(terminal.identity === null && definition !== undefined ? { definition } : {}),
|
|
676
|
+
}));
|
|
677
|
+
return { kind: "evaluated", evaluation, value: refType, credits };
|
|
678
|
+
}
|
|
679
|
+
const externalFallback = {
|
|
680
|
+
kind: componentKindFor(fileGraph.dialect),
|
|
681
|
+
export: compoundExportName(effectiveImported, residual),
|
|
682
|
+
source: externalSource(packageName, matchedImport.specifier),
|
|
683
|
+
};
|
|
684
|
+
// Check whether the import resolves to a local file that IS in the graph.
|
|
685
|
+
// If it does, resolve properly to avoid false positives (e.g. importing a
|
|
686
|
+
// utility function that happens to be named with a capital letter from a
|
|
687
|
+
// workspace sibling). For imports whose target is outside the graph
|
|
688
|
+
// (external packages in node_modules), trust the JSX call site directly.
|
|
689
|
+
const resolvedAbsTarget = graph.moduleResolver(filePath, matchedImport.specifier);
|
|
690
|
+
// Normalize absolute path → repo-relative graph key when possible.
|
|
691
|
+
const resolvedNormTarget = resolvedAbsTarget !== null && graph.resolveToGraphKey
|
|
692
|
+
? (graph.resolveToGraphKey(resolvedAbsTarget) ?? resolvedAbsTarget)
|
|
693
|
+
: resolvedAbsTarget;
|
|
694
|
+
const targetInGraph = resolvedNormTarget !== null && graph.files.has(resolvedNormTarget);
|
|
695
|
+
if (targetInGraph) {
|
|
696
|
+
// Target file is in graph — workspace sibling. Resolve to check
|
|
697
|
+
// JSX type, fan out per terminal. The fallback identity is local so
|
|
698
|
+
// null-identity terminals still attribute to the sibling file rather
|
|
699
|
+
// than a phantom external stub. Terminal identities from deriveIdentity
|
|
700
|
+
// are already local when applicable (workspaceSiblingIdentity fires).
|
|
701
|
+
const refType = resolveReference(graph, fileGraph, usage.ref);
|
|
702
|
+
const evaluation = walkWithFolding(graph, fileGraph, refType, argMap, createCycleGuard(), undefined, "tag");
|
|
703
|
+
const jsxTerminals = creditedTerminals(evaluation);
|
|
704
|
+
if (jsxTerminals.length === 0)
|
|
705
|
+
return { kind: "evaluated", evaluation, value: refType, credits: [] };
|
|
706
|
+
const siblingFallback = workspaceSiblingFallback(graph, resolvedNormTarget, matchedImport, effectiveImported, fileGraph.dialect);
|
|
707
|
+
const localFallback = siblingFallback !== undefined && siblingFallback.kind !== "custom-element"
|
|
708
|
+
? { ...siblingFallback, export: compoundExportName(siblingFallback.export, residual) }
|
|
709
|
+
: siblingFallback;
|
|
710
|
+
const localHolder = siblingFallback !== undefined && siblingFallback.kind !== "custom-element" && siblingFallback.source.type === "local"
|
|
711
|
+
? { filePath: siblingFallback.source.filePath, exportName: siblingFallback.export }
|
|
712
|
+
: undefined;
|
|
713
|
+
const definition = localHolder === undefined ? undefined : holderDefinition(localHolder.filePath, localHolder.exportName);
|
|
714
|
+
const named = localHolder === undefined || namesHolderMember(localHolder.filePath, localHolder.exportName);
|
|
715
|
+
const fallback = localFallback ?? externalFallback;
|
|
716
|
+
const credits = jsxTerminals.filter((t) => t.identity !== null || named).map((terminal) => ({
|
|
717
|
+
terminal,
|
|
718
|
+
fallback,
|
|
719
|
+
fallbackSpecifier: matchedImport.specifier,
|
|
720
|
+
outerVia,
|
|
721
|
+
...(terminal.identity === null && definition !== undefined ? { definition } : {}),
|
|
722
|
+
}));
|
|
723
|
+
return { kind: "evaluated", evaluation, value: refType, credits };
|
|
724
|
+
}
|
|
725
|
+
// Target not in graph. Workspace membership: a
|
|
726
|
+
// resolved target owned by a workspace member is first-party — credit
|
|
727
|
+
// LOCAL, pinned to the definition file (bounded re-export resolution)
|
|
728
|
+
// so identity is scope-invariant between whole-repo and single-app
|
|
729
|
+
// scans. Otherwise: external, trusting the JSX call site (external
|
|
730
|
+
// package or unresolvable specifier — identity null →
|
|
731
|
+
// matchedImport-based componentId). See `unparsedMemberDefinition`.
|
|
732
|
+
const member = resolvedAbsTarget !== null
|
|
733
|
+
? unparsedMemberDefinition(graph, resolvedAbsTarget, effectiveImported)
|
|
734
|
+
: null;
|
|
735
|
+
const sentinel = { denotation: { kind: "foreign" }, viaTrail: [], identity: null };
|
|
736
|
+
const fallback = member !== null
|
|
737
|
+
? {
|
|
738
|
+
kind: componentKindFor(fileGraph.dialect),
|
|
739
|
+
export: compoundExportName(member.exportName, residual),
|
|
740
|
+
source: { type: "local", filePath: member.absFile },
|
|
741
|
+
}
|
|
742
|
+
: externalFallback;
|
|
743
|
+
return {
|
|
744
|
+
kind: "evaluated",
|
|
745
|
+
evaluation: [sentinel],
|
|
746
|
+
value: null,
|
|
747
|
+
credits: [{ terminal: sentinel, fallback, fallbackSpecifier: matchedImport.specifier, outerVia }],
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
/** The declaration a credit carries: its identity's tag-position stamp, else
|
|
751
|
+
* the render site's own binding for a credit with no identity. */
|
|
752
|
+
function carriedDeclaration(credit) {
|
|
753
|
+
const { identity } = credit.terminal;
|
|
754
|
+
if (identity === null)
|
|
755
|
+
return credit.declaration;
|
|
756
|
+
return identity.kind === "local" ? identity.declaration : undefined;
|
|
757
|
+
}
|
|
758
|
+
/** Every local declaration a tag's walk landed on as a function: the
|
|
759
|
+
* terminal's own identity, else the render site's fallback, named exactly as
|
|
760
|
+
* the render loop names it, with the declaration the identity's stamp or the
|
|
761
|
+
* render site resolved when there is one. */
|
|
762
|
+
function taggedDeclarations(graph, tagEvaluations) {
|
|
763
|
+
const tagged = [];
|
|
764
|
+
for (const [filePath, evaluations] of tagEvaluations) {
|
|
765
|
+
const dialect = dialectForFile(graph, filePath);
|
|
766
|
+
for (const evaluated of evaluations) {
|
|
767
|
+
if (evaluated.kind !== "evaluated")
|
|
768
|
+
continue;
|
|
769
|
+
for (const credit of evaluated.credits) {
|
|
770
|
+
if (!isCallable(credit.terminal))
|
|
771
|
+
continue;
|
|
772
|
+
const { identity } = credit.terminal;
|
|
773
|
+
const id = buildComponentId(identity, credit.fallback, dialect);
|
|
774
|
+
if (id.kind === "react-component" && id.source.type === "local") {
|
|
775
|
+
const declaration = carriedDeclaration(credit);
|
|
776
|
+
tagged.push({ filePath: id.source.filePath, exportName: id.export, ...(declaration !== undefined ? { declaration } : {}) });
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return tagged;
|
|
782
|
+
}
|
|
783
|
+
/** Push one usage's credited terminals: one occurrence per credit × owner,
|
|
784
|
+
* through the external-leaf relabel seam and `emitOccurrence`. A credit that
|
|
785
|
+
* is not `callable` is pushed only when `registryHolds` its identity, a
|
|
786
|
+
* `callable` one only when `registryTagged` its identity. */
|
|
787
|
+
function emitTagCredits(occurrences, graph, filePath, fileGraph, usage, ownership, credits, helperIndex, registry, opts) {
|
|
788
|
+
if (credits.length === 0)
|
|
789
|
+
return;
|
|
790
|
+
const viaOverride = ownership?.viaOverride;
|
|
791
|
+
const site = usageSite(filePath, usage);
|
|
792
|
+
const owners = usageOwners(graph, helperIndex, ownership?.ownerSymbolRef ?? null, viaOverride, filePath);
|
|
793
|
+
for (const credit of credits) {
|
|
794
|
+
const rawComponentId = componentIdFromIdentity(credit.terminal.identity, credit.fallback, fileGraph.dialect, {
|
|
795
|
+
filePath,
|
|
796
|
+
...(credit.fallbackSpecifier !== undefined ? { fallbackSpecifier: credit.fallbackSpecifier } : {}),
|
|
797
|
+
resolveExternalLeaf: opts?.resolveExternalLeaf,
|
|
798
|
+
});
|
|
799
|
+
const declaration = carriedDeclaration(credit);
|
|
800
|
+
const stands = isCallable(credit.terminal)
|
|
801
|
+
? registryTagged(registry, graph, rawComponentId, declaration)
|
|
802
|
+
: registryHolds(registry, graph, rawComponentId, declaration);
|
|
803
|
+
if (!stands)
|
|
804
|
+
continue;
|
|
805
|
+
emitOccurrence(occurrences, site, rawComponentId, owners, viaOverride !== undefined ? [viaOverride] : [credit.outerVia, ...credit.terminal.viaTrail], credit.definition);
|
|
806
|
+
}
|
|
710
807
|
}
|
|
711
808
|
/**
|
|
712
809
|
* The one reporter for a JSX usage that produced no occurrence. A host-element
|
|
713
|
-
*
|
|
810
|
+
* tag reports nothing. A failed import is `unresolved-import`, an unbound root
|
|
714
811
|
* `unresolved-reference`. A walked value holding an `import()` is
|
|
715
812
|
* `lazy-import-unsupported`, naming the file the `import()` is declared in when
|
|
716
813
|
* that is not the render file; the `import()` in the walked value is checked
|
|
717
814
|
* before the evaluation. Everything the evaluation itself decides
|
|
718
815
|
* is `renderOutcome`'s: a value that is not a component reports nothing, a
|
|
719
816
|
* value supplied elsewhere reports `late-bound-render`, and anything else —
|
|
720
|
-
* including a credit
|
|
721
|
-
* or one diagnostic) — reports
|
|
817
|
+
* including a credit the registry's judge, its tagged membership or admission
|
|
818
|
+
* rejected (#544: one occurrence or one diagnostic) — reports
|
|
819
|
+
* `unresolved-reference`. A non-react dialect
|
|
722
820
|
* reports only the import codes (#621).
|
|
723
821
|
*/
|
|
724
822
|
function reportDroppedRender(opts, filePath, dialect, usage, outcome) {
|
|
725
823
|
const collector = opts?.collector;
|
|
726
824
|
if (collector === undefined)
|
|
727
825
|
return;
|
|
728
|
-
if (
|
|
826
|
+
if (outcome.kind === "host-element")
|
|
729
827
|
return;
|
|
730
828
|
const at = { filePath, line: usage.loc.line, column: usage.loc.column };
|
|
731
829
|
const positional = { ...at, symbol: usage.ref.symbol, memberChain: [...usage.ref.memberChain] };
|
|
@@ -759,39 +857,6 @@ function reportDroppedRender(opts, filePath, dialect, usage, outcome) {
|
|
|
759
857
|
}
|
|
760
858
|
collector.emit({ code: decided.code, severity: "info", ...positional });
|
|
761
859
|
}
|
|
762
|
-
/**
|
|
763
|
-
* Push local-scope occurrences for a symbol declared in the same file (no import).
|
|
764
|
-
* Walks the decl's value via walkWithFolding and fans out per JSX terminal.
|
|
765
|
-
*/
|
|
766
|
-
function pushLocalOccurrence(occurrences, filePath, usage, decl, graph, fileGraph, argMap, ownerRef, helperIndex, opts, viaOverride) {
|
|
767
|
-
// A member access on a same-file binding (`<LocalNS.Leaf/>`) walks the
|
|
768
|
-
// MEMBER, not the whole object — `resolveReference` applies the usage's
|
|
769
|
-
// member chain exactly as the import path already gets it (the same
|
|
770
|
-
// static-MemberOf arm). A bare reference is unchanged:
|
|
771
|
-
// `decl.value` is what `resolveReference` returns for an empty chain.
|
|
772
|
-
const value = usage.ref.memberChain.length > 0 ? resolveReference(graph, fileGraph, usage.ref) : decl.value;
|
|
773
|
-
const evaluation = walkWithFolding(graph, fileGraph, value, argMap);
|
|
774
|
-
const jsxTerminals = creditedTerminals(evaluation);
|
|
775
|
-
if (jsxTerminals.length === 0)
|
|
776
|
-
return { evaluation, value };
|
|
777
|
-
const outerVia = viaOverride ?? { kind: "local-component" };
|
|
778
|
-
const site = usageSite(filePath, usage);
|
|
779
|
-
const owners = usageOwners(graph, helperIndex, ownerRef, viaOverride, fileGraph.filePath);
|
|
780
|
-
const fallback = {
|
|
781
|
-
kind: componentKindFor(fileGraph.dialect),
|
|
782
|
-
export: compoundExportName(usage.ref.symbol, usage.ref.memberChain),
|
|
783
|
-
source: { type: "local", filePath },
|
|
784
|
-
};
|
|
785
|
-
const definition = { line: decl.loc.line, column: decl.loc.column };
|
|
786
|
-
for (const ft of jsxTerminals) {
|
|
787
|
-
const rawComponentId = componentIdFromIdentity(ft.identity, fallback, fileGraph.dialect, {
|
|
788
|
-
filePath,
|
|
789
|
-
resolveExternalLeaf: opts?.resolveExternalLeaf,
|
|
790
|
-
});
|
|
791
|
-
emitOccurrence(occurrences, site, rawComponentId, owners, viaOverride !== undefined ? [viaOverride] : [outerVia, ...ft.viaTrail], ft.identity === null ? definition : undefined);
|
|
792
|
-
}
|
|
793
|
-
return { evaluation, value };
|
|
794
|
-
}
|
|
795
860
|
/**
|
|
796
861
|
* A holder is folded away iff its fold reaches JSX and EVERY JSX
|
|
797
862
|
* terminal carries an identity — the fold credited someone else for every
|
|
@@ -821,7 +886,7 @@ function foldedHolder(holderJsx, filePath) {
|
|
|
821
886
|
* the same helper `hoc-wrapper` uses. The caller compacts the slice
|
|
822
887
|
* (admission + dedup).
|
|
823
888
|
*/
|
|
824
|
-
function pushArgumentSiteOccurrences(occurrences, filePath, holder, call, holderJsx, graph, fileGraph, argMap, opts, depth = 0) {
|
|
889
|
+
function pushArgumentSiteOccurrences(occurrences, filePath, holder, call, holderJsx, graph, fileGraph, argMap, registry, opts, depth = 0) {
|
|
825
890
|
// The holder is its own identity iff its fold reaches JSX and no
|
|
826
891
|
// terminal carries an identity of its own OR a `hoc-wrapper` hop — the hop
|
|
827
892
|
// is the fold's own record that it credited an argument,
|
|
@@ -838,7 +903,7 @@ function pushArgumentSiteOccurrences(occurrences, filePath, holder, call, holder
|
|
|
838
903
|
if (folded?.arg.kind !== "ReturnTypeOf")
|
|
839
904
|
return;
|
|
840
905
|
const innerJsx = creditedTerminals(walkWithFolding(graph, fileGraph, folded.arg, argMap));
|
|
841
|
-
pushArgumentSiteOccurrences(occurrences, filePath, holder, folded.arg, innerJsx, graph, fileGraph, argMap, opts, depth + 1);
|
|
906
|
+
pushArgumentSiteOccurrences(occurrences, filePath, holder, folded.arg, innerJsx, graph, fileGraph, argMap, registry, opts, depth + 1);
|
|
842
907
|
return;
|
|
843
908
|
}
|
|
844
909
|
if (shape !== "product")
|
|
@@ -857,7 +922,7 @@ function pushArgumentSiteOccurrences(occurrences, filePath, holder, call, holder
|
|
|
857
922
|
const rawOwnerComponentId = hasOnlyHostElementNames(holder.symbol, fileGraph)
|
|
858
923
|
? undefined
|
|
859
924
|
: ownerIdFromRef(graph, holderRef, filePath);
|
|
860
|
-
pushCallArgumentOccurrences(occurrences, filePath, call, () => [{ rawOwnerComponentId, viaPrefix: [] }], graph, fileGraph, argMap, opts, depth);
|
|
925
|
+
pushCallArgumentOccurrences(occurrences, filePath, call, () => [{ rawOwnerComponentId, viaPrefix: [] }], graph, fileGraph, argMap, registry, opts, depth);
|
|
861
926
|
}
|
|
862
927
|
/** Calls below the outermost one that argument seeding descends through. */
|
|
863
928
|
const MAX_ARGUMENT_CALL_DEPTH = 2;
|
|
@@ -876,6 +941,30 @@ function callFoldShape(jsx) {
|
|
|
876
941
|
return "product";
|
|
877
942
|
return credited === jsx.length ? "wrapper" : null;
|
|
878
943
|
}
|
|
944
|
+
/** A local identity declared in a parsed React-dialect file is a component
|
|
945
|
+
* only when the registry's judge says its declaration is one (`declaration`
|
|
946
|
+
* when the credit carries it); the declaring file's dialect decides, not the
|
|
947
|
+
* identity's kind. Any other identity is not the registry's to judge. */
|
|
948
|
+
function registryHolds(registry, graph, id, declaration) {
|
|
949
|
+
if (id.kind === "custom-element" || id.source.type !== "local")
|
|
950
|
+
return true;
|
|
951
|
+
if (graph.files.get(id.source.filePath)?.dialect !== "react")
|
|
952
|
+
return true;
|
|
953
|
+
return registry.isComponent(id.source.filePath, id.export, declaration);
|
|
954
|
+
}
|
|
955
|
+
/** A local, non-compound `react-component` identity declared in a parsed
|
|
956
|
+
* React-dialect file is what a `callable` credit names only when tagged
|
|
957
|
+
* membership marked a declaration for it (`isTagged`, `declaration` when the
|
|
958
|
+
* credit carries it). Any other identity is not the registry's to judge. */
|
|
959
|
+
function registryTagged(registry, graph, id, declaration) {
|
|
960
|
+
if (id.kind !== "react-component" || id.source.type !== "local")
|
|
961
|
+
return true;
|
|
962
|
+
if (graph.files.get(id.source.filePath)?.dialect !== "react")
|
|
963
|
+
return true;
|
|
964
|
+
if (parseCompoundExport(id.export).isCompound)
|
|
965
|
+
return true;
|
|
966
|
+
return registry.isTagged(id.source.filePath, id.export, declaration);
|
|
967
|
+
}
|
|
879
968
|
/**
|
|
880
969
|
* THE argument-site push seam: one `passed-as-argument` occurrence per
|
|
881
970
|
* (identifier argument that resolves to a component) × (owner). The
|
|
@@ -891,7 +980,8 @@ function callFoldShape(jsx) {
|
|
|
891
980
|
* rendered as a component, whatever it resolves to — an external leaf has
|
|
892
981
|
* no shape to check and would otherwise synthesise a component out of any
|
|
893
982
|
* import; the same predicate the held-reference seam and the roster naming
|
|
894
|
-
* gate apply.
|
|
983
|
+
* gate apply. A local identity declared in a React file is seeded only when
|
|
984
|
+
* the registry's component judge accepts its declaration (`registryHolds`).
|
|
895
985
|
*
|
|
896
986
|
* A call-valued argument is followed by what its own fold says
|
|
897
987
|
* (`callFoldShape`), at most `MAX_ARGUMENT_CALL_DEPTH` calls below the
|
|
@@ -906,7 +996,7 @@ function callFoldShape(jsx) {
|
|
|
906
996
|
* the owner chain for each of them measured as roughly half of the hook
|
|
907
997
|
* loop's scan-time cost on a large consumer repo.
|
|
908
998
|
*/
|
|
909
|
-
function pushCallArgumentOccurrences(occurrences, filePath, call, owners, graph, fileGraph, argMap, opts, depth = 0) {
|
|
999
|
+
function pushCallArgumentOccurrences(occurrences, filePath, call, owners, graph, fileGraph, argMap, registry, opts, depth = 0) {
|
|
910
1000
|
let resolvedOwners;
|
|
911
1001
|
const seedIdentifier = (site, index, arg, hops) => {
|
|
912
1002
|
if (isHostElementName(arg.ref.symbol))
|
|
@@ -924,6 +1014,8 @@ function pushCallArgumentOccurrences(occurrences, filePath, call, owners, graph,
|
|
|
924
1014
|
filePath,
|
|
925
1015
|
resolveExternalLeaf: opts?.resolveExternalLeaf,
|
|
926
1016
|
});
|
|
1017
|
+
if (!registryHolds(registry, graph, rawComponentId))
|
|
1018
|
+
continue;
|
|
927
1019
|
resolvedOwners ??= owners();
|
|
928
1020
|
emitOccurrence(occurrences, { filePath, line: arg.ref.loc.line, column: arg.ref.loc.column, props: {}, events: [] }, rawComponentId, resolvedOwners, [outerVia, ...hops, ...ft.viaTrail], argDecl !== null ? { line: argDecl.loc.line, column: argDecl.loc.column } : undefined);
|
|
929
1021
|
}
|