@component-compass/reference-graph 0.1.21 → 0.1.23
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/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { asTagName, computeOccurrenceId, serialiseComponentId, projectPropUsages
|
|
|
2
2
|
import { MODULE_SCOPE } from "../index.js";
|
|
3
3
|
import { createArgumentMap } from "./argument-map.js";
|
|
4
4
|
import { resolveReference } from "./resolve-reference.js";
|
|
5
|
-
import { walkWithFolding, argumentProvenance } from "./wrapper-folding.js";
|
|
5
|
+
import { walkWithFolding, argumentProvenance, foldedArgument } from "./wrapper-folding.js";
|
|
6
6
|
import { packageNameFromSpecifier, externalSubpath } from "./specifier.js";
|
|
7
7
|
import { followReExportChain } from "./re-export-chain.js";
|
|
8
8
|
import { buildHelperCallers, findLocalDeclaration } from "./helper-callers.js";
|
|
@@ -118,238 +118,176 @@ export function resolve(graph, opts) {
|
|
|
118
118
|
? { fileUsageIdx, kind, parentUsageIdx }
|
|
119
119
|
: { fileUsageIdx, kind };
|
|
120
120
|
}
|
|
121
|
+
return occurrences.length - lengthBefore;
|
|
121
122
|
};
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
stampComposition(jsxLengthBefore, "jsx", usageIdx, jsxDepth, jsxParentUsageIdx);
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
const packageName = packageNameFromSpecifier(matchedImport.specifier);
|
|
163
|
-
// Resolve namespace-import members to their export name once, up front —
|
|
164
|
-
// every export-name / re-export lookup below keys on this, not the raw
|
|
165
|
-
// `imported`. `via.import` keeps the raw `imported` (provenance).
|
|
166
|
-
const effectiveImported = effectiveExportName(matchedImport.imported, usage.ref.memberChain);
|
|
167
|
-
// Lookups below key on `effectiveImported` (the ROOT); the
|
|
168
|
-
// identity stamped on a fallback is the root plus the residual chain.
|
|
169
|
-
// A member the walker resolves to its own declaration never reaches a
|
|
170
|
-
// fallback, so this only names members with no declaration of their own.
|
|
171
|
-
const residual = residualMemberChain(matchedImport.imported, usage.ref.memberChain);
|
|
172
|
-
if (!packageName) {
|
|
173
|
-
// Relative / absolute specifier — always a local file reference.
|
|
174
|
-
// Resolve the target path; normalize absolute → graph-relative if possible.
|
|
175
|
-
const resolvedAbsTarget = graph.moduleResolver(filePath, matchedImport.specifier);
|
|
176
|
-
if (!resolvedAbsTarget) {
|
|
177
|
-
// A JSX usage whose import failed to resolve is a dropped local
|
|
178
|
-
// reference — surface it instead of vanishing.
|
|
179
|
-
opts?.collector?.emit({
|
|
180
|
-
code: "unresolved-import",
|
|
181
|
-
severity: "warning",
|
|
182
|
-
filePath,
|
|
183
|
-
line: usage.loc.line,
|
|
184
|
-
column: usage.loc.column,
|
|
185
|
-
specifier: matchedImport.specifier,
|
|
186
|
-
});
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
const resolvedTarget = graph.resolveToGraphKey
|
|
190
|
-
? (graph.resolveToGraphKey(resolvedAbsTarget) ?? resolvedAbsTarget)
|
|
191
|
-
: resolvedAbsTarget;
|
|
192
|
-
// Resolve the type to confirm it's a JSX component. Cross-file resolution
|
|
193
|
-
// works when resolveToGraphKey is present (graph keys are repo-relative).
|
|
194
|
-
const refType = resolveReference(graph, fileGraph, usage.ref);
|
|
195
|
-
const folded = walkWithFolding(graph, fileGraph, refType, argMap);
|
|
196
|
-
const jsxTerminals = folded.filter((t) => t.type.kind === "JSX");
|
|
197
|
-
if (jsxTerminals.length === 0) {
|
|
198
|
-
emitLazyImportUnsupported(opts, filePath, usage.loc, refType);
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
// Determine the canonical source file + local export name. When the
|
|
202
|
-
// target is a re-export barrel (`export { X } from './source'`),
|
|
203
|
-
// follow the chain to the actual definition file so JSX edges
|
|
204
|
-
// attribute to the source rather than the barrel. Without
|
|
205
|
-
// this, two consumers importing the same component through different
|
|
206
|
-
// re-export paths fragment across phantom identities.
|
|
207
|
-
const chained = followReExportChain(graph, resolvedTarget, effectiveImported);
|
|
208
|
-
const canonicalFile = chained?.file ?? resolvedTarget;
|
|
209
|
-
let exportName = chained?.localExport ?? effectiveImported;
|
|
210
|
-
// When the re-export chain didn't follow (no chain present), still
|
|
211
|
-
// look up the local export name at the resolved target to match the
|
|
212
|
-
// local-index seed's hash convention.
|
|
213
|
-
if (!chained) {
|
|
214
|
-
const targetGraph = graph.files.get(resolvedTarget);
|
|
215
|
-
if (targetGraph && effectiveImported === "default") {
|
|
216
|
-
const defaultExp = targetGraph.exports.find((e) => e.kind === "default");
|
|
217
|
-
if (defaultExp && "local" in defaultExp) {
|
|
218
|
-
exportName = defaultExp.local;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
else if (targetGraph) {
|
|
222
|
-
const namedExp = targetGraph.exports.find((e) => e.kind === "named" && "exportedAs" in e && e.exportedAs === effectiveImported && "local" in e);
|
|
223
|
-
if (namedExp && "local" in namedExp) {
|
|
224
|
-
exportName = namedExp.local;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
const outerVia = viaOverride ?? {
|
|
229
|
-
kind: "direct-import",
|
|
230
|
-
specifier: matchedImport.specifier,
|
|
231
|
-
import: matchedImport.imported,
|
|
232
|
-
};
|
|
233
|
-
const fallbackComponentId = {
|
|
234
|
-
kind: componentKindFor(fileGraph.dialect),
|
|
235
|
-
export: compoundExportName(exportName, residual),
|
|
236
|
-
source: { type: "local", filePath: canonicalFile },
|
|
237
|
-
};
|
|
238
|
-
if (viaOverride !== undefined) {
|
|
239
|
-
// Prop-forward: parser has pre-decided the owner — single-occurrence path.
|
|
240
|
-
const rawOwnerComponentId = ownerIdFromRef(graph, ownerRef, fileGraph.filePath);
|
|
241
|
-
for (const ft of jsxTerminals) {
|
|
242
|
-
const rawComponentId = componentIdFromIdentity(ft.identity, fallbackComponentId, fileGraph.dialect, {
|
|
243
|
-
filePath,
|
|
244
|
-
resolveExternalLeaf: opts?.resolveExternalLeaf,
|
|
245
|
-
});
|
|
246
|
-
const viaChain = [viaOverride];
|
|
247
|
-
const provisionalId = serialiseComponentId(rawComponentId);
|
|
248
|
-
const occurrenceId = computeOccurrenceId(provisionalId, filePath, usage.loc.line, usage.loc.column, rawOwnerComponentId !== undefined ? serialiseComponentId(rawOwnerComponentId) : undefined);
|
|
249
|
-
occurrences.push({
|
|
250
|
-
rawComponentId,
|
|
251
|
-
filePath,
|
|
252
|
-
line: usage.loc.line,
|
|
253
|
-
column: usage.loc.column,
|
|
254
|
-
via: viaChain[0] ?? outerVia,
|
|
255
|
-
viaChain,
|
|
256
|
-
props: projectPropUsages(usage.props),
|
|
257
|
-
events: usage.events ?? [],
|
|
258
|
-
occurrenceId,
|
|
259
|
-
depth: 0,
|
|
260
|
-
...(rawOwnerComponentId !== undefined ? { rawOwnerComponentId } : {}),
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
// Helper-call fanout: resolve owner chain and emit per (terminal × resolution).
|
|
266
|
-
const ownerResolutions = resolveOwnerChain(ownerRef, graph, helperIndex, createCycleGuard());
|
|
267
|
-
for (const ft of jsxTerminals) {
|
|
268
|
-
const rawComponentId = componentIdFromIdentity(ft.identity, fallbackComponentId, fileGraph.dialect, {
|
|
269
|
-
filePath,
|
|
270
|
-
resolveExternalLeaf: opts?.resolveExternalLeaf,
|
|
271
|
-
});
|
|
272
|
-
for (const res of ownerResolutions) {
|
|
273
|
-
const rawOwnerComponentId = ownerIdFromResolution(graph, res.ownerDecl);
|
|
274
|
-
const viaChain = [...res.viaPrefix, outerVia, ...ft.viaTrail];
|
|
275
|
-
const provisionalId = serialiseComponentId(rawComponentId);
|
|
276
|
-
const occurrenceId = computeOccurrenceId(provisionalId, filePath, usage.loc.line, usage.loc.column, rawOwnerComponentId !== undefined ? serialiseComponentId(rawOwnerComponentId) : undefined);
|
|
277
|
-
occurrences.push({
|
|
278
|
-
rawComponentId,
|
|
279
|
-
filePath,
|
|
280
|
-
line: usage.loc.line,
|
|
281
|
-
column: usage.loc.column,
|
|
282
|
-
via: viaChain[0] ?? outerVia,
|
|
283
|
-
viaChain,
|
|
284
|
-
props: projectPropUsages(usage.props),
|
|
285
|
-
events: usage.events ?? [],
|
|
286
|
-
occurrenceId,
|
|
287
|
-
depth: 0,
|
|
288
|
-
...(rawOwnerComponentId !== undefined ? { rawOwnerComponentId } : {}),
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
stampComposition(jsxLengthBefore, "jsx", usageIdx, jsxDepth, jsxParentUsageIdx);
|
|
294
|
-
continue;
|
|
295
|
-
}
|
|
296
|
-
// Check whether the import resolves to a local file that IS in the graph.
|
|
297
|
-
// If it does, resolve properly to avoid false positives (e.g. importing a
|
|
298
|
-
// utility function that happens to be named with a capital letter from a
|
|
299
|
-
// workspace sibling). For imports whose target is outside the graph
|
|
300
|
-
// (external packages in node_modules), trust the JSX call site directly.
|
|
123
|
+
const pushJsxUsageOccurrences = (filePath, fileGraph, usage, ownership) => {
|
|
124
|
+
// Resolve owner symbol ref for this usage (for composition attribution).
|
|
125
|
+
const ownerRef = ownership?.ownerSymbolRef ?? null;
|
|
126
|
+
const viaOverride = ownership?.viaOverride;
|
|
127
|
+
// Find the import record that introduced the root symbol at module scope.
|
|
128
|
+
// We check usage.ref.symbol (the root binding name), not member-chain
|
|
129
|
+
// segments, because ImportRecord.local binds the top-level name.
|
|
130
|
+
const matchedImport = fileGraph.importsByLocal.get(usage.ref.symbol);
|
|
131
|
+
if (!matchedImport) {
|
|
132
|
+
// Symbol is locally declared (no import) — emit local identity.
|
|
133
|
+
const decl = findLocalDeclaration(fileGraph, usage.ref);
|
|
134
|
+
if (!decl)
|
|
135
|
+
return { kind: "unbound" };
|
|
136
|
+
return {
|
|
137
|
+
kind: "resolved",
|
|
138
|
+
value: pushLocalOccurrence(occurrences, filePath, usage, decl, graph, fileGraph, argMap, ownerRef, helperIndex, opts, viaOverride),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const packageName = packageNameFromSpecifier(matchedImport.specifier);
|
|
142
|
+
// Resolve namespace-import members to their export name once, up front —
|
|
143
|
+
// every export-name / re-export lookup below keys on this, not the raw
|
|
144
|
+
// `imported`. `via.import` keeps the raw `imported` (provenance).
|
|
145
|
+
const effectiveImported = effectiveExportName(matchedImport.imported, usage.ref.memberChain);
|
|
146
|
+
// Lookups below key on `effectiveImported` (the ROOT); the
|
|
147
|
+
// identity stamped on a fallback is the root plus the residual chain.
|
|
148
|
+
// A member the walker resolves to its own declaration never reaches a
|
|
149
|
+
// fallback, so this only names members with no declaration of their own.
|
|
150
|
+
const residual = residualMemberChain(matchedImport.imported, usage.ref.memberChain);
|
|
151
|
+
const holderDefinition = (file, exportName) => {
|
|
152
|
+
if (residual.length === 0)
|
|
153
|
+
return undefined;
|
|
154
|
+
const loc = graph.files.get(file)?.declarations.get(`${MODULE_SCOPE}::${exportName}`)?.loc;
|
|
155
|
+
return loc === undefined ? undefined : { line: loc.line, column: loc.column };
|
|
156
|
+
};
|
|
157
|
+
if (!packageName) {
|
|
158
|
+
// Relative / absolute specifier — always a local file reference.
|
|
159
|
+
// Resolve the target path; normalize absolute → graph-relative if possible.
|
|
301
160
|
const resolvedAbsTarget = graph.moduleResolver(filePath, matchedImport.specifier);
|
|
302
|
-
|
|
303
|
-
|
|
161
|
+
if (!resolvedAbsTarget)
|
|
162
|
+
return { kind: "import-failed", specifier: matchedImport.specifier };
|
|
163
|
+
const resolvedTarget = graph.resolveToGraphKey
|
|
304
164
|
? (graph.resolveToGraphKey(resolvedAbsTarget) ?? resolvedAbsTarget)
|
|
305
165
|
: resolvedAbsTarget;
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
166
|
+
// Resolve the type to confirm it's a JSX component. Cross-file resolution
|
|
167
|
+
// works when resolveToGraphKey is present (graph keys are repo-relative).
|
|
168
|
+
const refType = resolveReference(graph, fileGraph, usage.ref);
|
|
169
|
+
const folded = walkWithFolding(graph, fileGraph, refType, argMap);
|
|
170
|
+
const jsxTerminals = folded.filter((t) => t.type.kind === "JSX");
|
|
171
|
+
if (jsxTerminals.length === 0)
|
|
172
|
+
return { kind: "resolved", value: refType };
|
|
173
|
+
// Determine the canonical source file + local export name. When the
|
|
174
|
+
// target is a re-export barrel (`export { X } from './source'`),
|
|
175
|
+
// follow the chain to the actual definition file so JSX edges
|
|
176
|
+
// attribute to the source rather than the barrel. Without
|
|
177
|
+
// this, two consumers importing the same component through different
|
|
178
|
+
// re-export paths fragment across phantom identities.
|
|
179
|
+
const chained = followReExportChain(graph, resolvedTarget, effectiveImported);
|
|
180
|
+
const canonicalFile = chained?.file ?? resolvedTarget;
|
|
181
|
+
let exportName = chained?.localExport ?? effectiveImported;
|
|
182
|
+
// When the re-export chain didn't follow (no chain present), still
|
|
183
|
+
// look up the local export name at the resolved target to match the
|
|
184
|
+
// local-index seed's hash convention.
|
|
185
|
+
if (!chained) {
|
|
186
|
+
const targetGraph = graph.files.get(resolvedTarget);
|
|
187
|
+
if (targetGraph && effectiveImported === "default") {
|
|
188
|
+
const defaultExp = targetGraph.exports.find((e) => e.kind === "default");
|
|
189
|
+
if (defaultExp && "local" in defaultExp) {
|
|
190
|
+
exportName = defaultExp.local;
|
|
191
|
+
}
|
|
319
192
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
pushOccurrence(occurrences, filePath, usage, matchedImport, compoundExportName(effectiveImported, residual), packageName, graph, fileGraph, ft, ownerRef, helperIndex, opts, localFallback, viaOverride);
|
|
193
|
+
else if (targetGraph) {
|
|
194
|
+
const namedExp = targetGraph.exports.find((e) => e.kind === "named" && "exportedAs" in e && e.exportedAs === effectiveImported && "local" in e);
|
|
195
|
+
if (namedExp && "local" in namedExp) {
|
|
196
|
+
exportName = namedExp.local;
|
|
197
|
+
}
|
|
326
198
|
}
|
|
327
199
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
else {
|
|
349
|
-
pushOccurrence(occurrences, filePath, usage, matchedImport, compoundExportName(effectiveImported, residual), packageName, graph, fileGraph, sentinelFt, ownerRef, helperIndex, opts, undefined, viaOverride);
|
|
350
|
-
}
|
|
200
|
+
const outerVia = viaOverride ?? {
|
|
201
|
+
kind: "direct-import",
|
|
202
|
+
specifier: matchedImport.specifier,
|
|
203
|
+
import: matchedImport.imported,
|
|
204
|
+
};
|
|
205
|
+
const fallbackComponentId = {
|
|
206
|
+
kind: componentKindFor(fileGraph.dialect),
|
|
207
|
+
export: compoundExportName(exportName, residual),
|
|
208
|
+
source: { type: "local", filePath: canonicalFile },
|
|
209
|
+
};
|
|
210
|
+
const site = usageSite(filePath, usage);
|
|
211
|
+
const owners = usageOwners(graph, helperIndex, ownerRef, viaOverride, fileGraph.filePath);
|
|
212
|
+
const definition = holderDefinition(canonicalFile, exportName);
|
|
213
|
+
for (const ft of jsxTerminals) {
|
|
214
|
+
const rawComponentId = componentIdFromIdentity(ft.identity, fallbackComponentId, fileGraph.dialect, {
|
|
215
|
+
filePath,
|
|
216
|
+
resolveExternalLeaf: opts?.resolveExternalLeaf,
|
|
217
|
+
});
|
|
218
|
+
emitOccurrence(occurrences, site, rawComponentId, owners, viaOverride !== undefined ? [viaOverride] : [outerVia, ...ft.viaTrail], ft.identity === null ? definition : undefined);
|
|
351
219
|
}
|
|
352
|
-
|
|
220
|
+
return { kind: "resolved", value: refType };
|
|
221
|
+
}
|
|
222
|
+
// Check whether the import resolves to a local file that IS in the graph.
|
|
223
|
+
// If it does, resolve properly to avoid false positives (e.g. importing a
|
|
224
|
+
// utility function that happens to be named with a capital letter from a
|
|
225
|
+
// workspace sibling). For imports whose target is outside the graph
|
|
226
|
+
// (external packages in node_modules), trust the JSX call site directly.
|
|
227
|
+
const resolvedAbsTarget = graph.moduleResolver(filePath, matchedImport.specifier);
|
|
228
|
+
// Normalize absolute path → repo-relative graph key when possible.
|
|
229
|
+
const resolvedNormTarget = resolvedAbsTarget !== null && graph.resolveToGraphKey
|
|
230
|
+
? (graph.resolveToGraphKey(resolvedAbsTarget) ?? resolvedAbsTarget)
|
|
231
|
+
: resolvedAbsTarget;
|
|
232
|
+
const targetInGraph = resolvedNormTarget !== null && graph.files.has(resolvedNormTarget);
|
|
233
|
+
if (targetInGraph) {
|
|
234
|
+
// Target file is in graph — workspace sibling. Resolve to check
|
|
235
|
+
// JSX type, fan out per terminal. The fallback identity is local so
|
|
236
|
+
// null-identity terminals still attribute to the sibling file rather
|
|
237
|
+
// than a phantom external stub. Terminal identities from deriveIdentity
|
|
238
|
+
// are already local when applicable (workspaceSiblingIdentity fires).
|
|
239
|
+
const refType = resolveReference(graph, fileGraph, usage.ref);
|
|
240
|
+
const foldedTerminals = walkWithFolding(graph, fileGraph, refType, argMap);
|
|
241
|
+
const jsxTerminals = foldedTerminals.filter((t) => t.type.kind === "JSX");
|
|
242
|
+
if (jsxTerminals.length === 0)
|
|
243
|
+
return { kind: "resolved", value: refType };
|
|
244
|
+
const siblingFallback = workspaceSiblingFallback(graph, resolvedNormTarget, matchedImport, effectiveImported, fileGraph.dialect);
|
|
245
|
+
const localFallback = siblingFallback !== undefined && siblingFallback.kind !== "custom-element"
|
|
246
|
+
? { ...siblingFallback, export: compoundExportName(siblingFallback.export, residual) }
|
|
247
|
+
: siblingFallback;
|
|
248
|
+
const definition = siblingFallback !== undefined && siblingFallback.kind !== "custom-element" && siblingFallback.source.type === "local"
|
|
249
|
+
? holderDefinition(siblingFallback.source.filePath, siblingFallback.export)
|
|
250
|
+
: undefined;
|
|
251
|
+
for (const ft of jsxTerminals) {
|
|
252
|
+
pushOccurrence(occurrences, filePath, usage, matchedImport, compoundExportName(effectiveImported, residual), packageName, graph, fileGraph, ft, ownerRef, helperIndex, opts, localFallback, viaOverride, ft.identity === null ? definition : undefined);
|
|
253
|
+
}
|
|
254
|
+
return { kind: "resolved", value: refType };
|
|
255
|
+
}
|
|
256
|
+
// Target not in graph. Workspace membership: a
|
|
257
|
+
// resolved target owned by a workspace member is first-party — emit
|
|
258
|
+
// LOCAL, pinned to the definition file (bounded re-export resolution)
|
|
259
|
+
// so identity is scope-invariant between whole-repo and single-app
|
|
260
|
+
// scans. Otherwise: external, trusting the JSX call site (external
|
|
261
|
+
// package or unresolvable specifier — identity null →
|
|
262
|
+
// matchedImport-based componentId). See `unparsedMemberDefinition`.
|
|
263
|
+
const member = resolvedAbsTarget !== null
|
|
264
|
+
? unparsedMemberDefinition(graph, resolvedAbsTarget, effectiveImported)
|
|
265
|
+
: null;
|
|
266
|
+
const sentinelFt = { type: { kind: "JSX" }, viaTrail: [], identity: null };
|
|
267
|
+
if (member !== null) {
|
|
268
|
+
const localFallback = {
|
|
269
|
+
kind: componentKindFor(fileGraph.dialect),
|
|
270
|
+
export: compoundExportName(member.exportName, residual),
|
|
271
|
+
source: { type: "local", filePath: member.absFile },
|
|
272
|
+
};
|
|
273
|
+
pushOccurrence(occurrences, filePath, usage, matchedImport, compoundExportName(effectiveImported, residual), packageName, graph, fileGraph, sentinelFt, ownerRef, helperIndex, opts, localFallback, viaOverride);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
pushOccurrence(occurrences, filePath, usage, matchedImport, compoundExportName(effectiveImported, residual), packageName, graph, fileGraph, sentinelFt, ownerRef, helperIndex, opts, undefined, viaOverride);
|
|
277
|
+
}
|
|
278
|
+
return { kind: "resolved", value: null };
|
|
279
|
+
};
|
|
280
|
+
for (const [filePath, fileGraph] of graph.files) {
|
|
281
|
+
for (let usageIdx = 0; usageIdx < fileGraph.jsxUsages.length; usageIdx++) {
|
|
282
|
+
const usage = fileGraph.jsxUsages[usageIdx];
|
|
283
|
+
if (!usage)
|
|
284
|
+
continue;
|
|
285
|
+
const ownership = fileGraph.ownership[usageIdx];
|
|
286
|
+
const lengthBefore = occurrences.length;
|
|
287
|
+
const outcome = pushJsxUsageOccurrences(filePath, fileGraph, usage, ownership);
|
|
288
|
+
const admitted = stampComposition(lengthBefore, "jsx", usageIdx, ownership?.depth ?? 0, ownership?.parentUsageIdx);
|
|
289
|
+
if (admitted === 0)
|
|
290
|
+
reportDroppedRender(opts, filePath, fileGraph.dialect, usage, outcome);
|
|
353
291
|
}
|
|
354
292
|
// Tag-usage resolution (Vue auto-imports, Lit tag-only references, HTML).
|
|
355
293
|
for (let i = 0; i < fileGraph.tagUsages.length; i++) {
|
|
@@ -410,9 +348,10 @@ export function resolve(graph, opts) {
|
|
|
410
348
|
// every terminal carries neither an identity nor a `hoc-wrapper` hop, the
|
|
411
349
|
// holder is its own identity
|
|
412
350
|
// (the callee was visible and built a new function) and credited
|
|
413
|
-
// none of its arguments, so each
|
|
414
|
-
//
|
|
415
|
-
// position, owned by the holder.
|
|
351
|
+
// none of its arguments, so each argument of its call that resolves to a
|
|
352
|
+
// component — an identifier, or one reached through a call-valued argument —
|
|
353
|
+
// gets one occurrence at the identifier's own position, owned by the holder.
|
|
354
|
+
// A fold that carried an identity credited
|
|
416
355
|
// an argument already and seeds nothing — a site is never
|
|
417
356
|
// counted twice. The registry gate keeps an unused, unexported holder from
|
|
418
357
|
// owning anything (no phantom owner by construction). Runs after the usage
|
|
@@ -469,10 +408,7 @@ export function resolve(graph, opts) {
|
|
|
469
408
|
loc: { line: 0, column: 0 },
|
|
470
409
|
originFile: filePath,
|
|
471
410
|
};
|
|
472
|
-
const owners = () =>
|
|
473
|
-
rawOwnerComponentId: ownerIdFromResolution(graph, res.ownerDecl),
|
|
474
|
-
viaPrefix: res.viaPrefix,
|
|
475
|
-
}));
|
|
411
|
+
const owners = () => chainOwners(graph, helperIndex, ownerRef);
|
|
476
412
|
const lengthBefore = occurrences.length;
|
|
477
413
|
pushCallArgumentOccurrences(occurrences, filePath, call, owners, graph, fileGraph, argMap, opts);
|
|
478
414
|
compactSlice(lengthBefore);
|
|
@@ -695,29 +631,65 @@ effectiveImported, consumerDialect) {
|
|
|
695
631
|
};
|
|
696
632
|
}
|
|
697
633
|
/**
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
*
|
|
634
|
+
* THE occurrence constructor: one occurrence per owner, `viaChain` being the
|
|
635
|
+
* owner's prefix followed by `chain`. Every emission path calls it; never
|
|
636
|
+
* build an `EngineOccurrence` literal anywhere else. The `occurrenceId` it
|
|
637
|
+
* computes is provisional, keyed on the serialised componentId; scan.ts
|
|
638
|
+
* re-derives it from the hashed componentId.
|
|
703
639
|
*/
|
|
704
|
-
function
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
640
|
+
function emitOccurrence(occurrences, site, rawComponentId, owners, chain, definition) {
|
|
641
|
+
const provisionalId = serialiseComponentId(rawComponentId);
|
|
642
|
+
for (const { rawOwnerComponentId, viaPrefix } of owners) {
|
|
643
|
+
const viaChain = [...viaPrefix, ...chain];
|
|
644
|
+
occurrences.push({
|
|
645
|
+
rawComponentId,
|
|
646
|
+
filePath: site.filePath,
|
|
647
|
+
line: site.line,
|
|
648
|
+
column: site.column,
|
|
649
|
+
via: viaChain[0] ?? chain[0],
|
|
650
|
+
viaChain,
|
|
651
|
+
props: site.props,
|
|
652
|
+
events: site.events,
|
|
653
|
+
occurrenceId: computeOccurrenceId(provisionalId, site.filePath, site.line, site.column, rawOwnerComponentId !== undefined ? serialiseComponentId(rawOwnerComponentId) : undefined),
|
|
654
|
+
depth: 0,
|
|
655
|
+
...(definition !== undefined ? { definition } : {}),
|
|
656
|
+
...(rawOwnerComponentId !== undefined ? { rawOwnerComponentId } : {}),
|
|
657
|
+
});
|
|
709
658
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
659
|
+
}
|
|
660
|
+
function usageSite(filePath, usage) {
|
|
661
|
+
return {
|
|
662
|
+
filePath,
|
|
663
|
+
line: usage.loc.line,
|
|
664
|
+
column: usage.loc.column,
|
|
665
|
+
props: projectPropUsages(usage.props),
|
|
666
|
+
events: usage.events ?? [],
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
/** The owner chain of `ownerRef`, one owner per resolution with its `helper-call` prefix. */
|
|
670
|
+
function chainOwners(graph, helperIndex, ownerRef) {
|
|
671
|
+
return resolveOwnerChain(ownerRef, graph, helperIndex, createCycleGuard()).map((res) => ({
|
|
672
|
+
rawOwnerComponentId: ownerIdFromResolution(graph, res.ownerDecl),
|
|
673
|
+
viaPrefix: res.viaPrefix,
|
|
674
|
+
}));
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Owners of a JSX usage. A `viaOverride` means the parser pre-decided the
|
|
678
|
+
* owner (prop-forward): one owner, no prefix. Otherwise the owner chain fans
|
|
679
|
+
* out, each resolution contributing its `helper-call` prefix.
|
|
680
|
+
*/
|
|
681
|
+
function usageOwners(graph, helperIndex, ownerRef, viaOverride, fallbackFile) {
|
|
682
|
+
if (viaOverride !== undefined) {
|
|
683
|
+
return [{ rawOwnerComponentId: ownerIdFromRef(graph, ownerRef, fallbackFile), viaPrefix: [] }];
|
|
684
|
+
}
|
|
685
|
+
return chainOwners(graph, helperIndex, ownerRef);
|
|
714
686
|
}
|
|
715
687
|
function pushOccurrence(occurrences, filePath, usage, matchedImport,
|
|
716
688
|
/** Export name for the fallback identity — the root export resolved via
|
|
717
689
|
* `effectiveExportName` joined with the residual member chain via
|
|
718
690
|
* `compoundExportName`. Distinct from `matchedImport.imported`,
|
|
719
691
|
* which stays raw for `via` provenance. */
|
|
720
|
-
effectiveImported, packageName, graph, fileGraph, ft, ownerRef, helperIndex, opts, fallbackOverride, viaOverride) {
|
|
692
|
+
effectiveImported, packageName, graph, fileGraph, ft, ownerRef, helperIndex, opts, fallbackOverride, viaOverride, definition) {
|
|
721
693
|
const fallback = fallbackOverride ?? {
|
|
722
694
|
kind: componentKindFor(fileGraph.dialect),
|
|
723
695
|
export: effectiveImported,
|
|
@@ -728,81 +700,55 @@ effectiveImported, packageName, graph, fileGraph, ft, ownerRef, helperIndex, opt
|
|
|
728
700
|
fallbackSpecifier: matchedImport.specifier,
|
|
729
701
|
resolveExternalLeaf: opts?.resolveExternalLeaf,
|
|
730
702
|
});
|
|
731
|
-
// Derive wraps cross-link for external react-component occurrences.
|
|
732
|
-
// wrapCallback only fires for external packages — local react-components are
|
|
733
|
-
// user-written, not wrappers. Does NOT affect rawComponentId or hashing.
|
|
734
|
-
const wraps = computeWraps(rawComponentId, opts?.wrapCallback);
|
|
735
703
|
const outerVia = viaOverride ?? {
|
|
736
704
|
kind: "direct-import",
|
|
737
705
|
specifier: matchedImport.specifier,
|
|
738
706
|
import: matchedImport.imported,
|
|
739
707
|
};
|
|
740
|
-
|
|
741
|
-
// Prop-forward: parser has pre-decided the owner — single-occurrence path.
|
|
742
|
-
const rawOwnerComponentId = ownerIdFromRef(graph, ownerRef, filePath);
|
|
743
|
-
const viaChain = [viaOverride];
|
|
744
|
-
const provisionalId = serialiseComponentId(rawComponentId);
|
|
745
|
-
const occurrenceId = computeOccurrenceId(provisionalId, filePath, usage.loc.line, usage.loc.column, rawOwnerComponentId !== undefined ? serialiseComponentId(rawOwnerComponentId) : undefined);
|
|
746
|
-
occurrences.push({
|
|
747
|
-
rawComponentId,
|
|
748
|
-
filePath,
|
|
749
|
-
line: usage.loc.line,
|
|
750
|
-
column: usage.loc.column,
|
|
751
|
-
via: viaChain[0] ?? outerVia,
|
|
752
|
-
viaChain,
|
|
753
|
-
props: projectPropUsages(usage.props),
|
|
754
|
-
events: usage.events ?? [],
|
|
755
|
-
occurrenceId,
|
|
756
|
-
depth: 0,
|
|
757
|
-
...(rawOwnerComponentId !== undefined ? { rawOwnerComponentId } : {}),
|
|
758
|
-
...(wraps !== undefined ? { wraps } : {}),
|
|
759
|
-
});
|
|
760
|
-
return;
|
|
761
|
-
}
|
|
762
|
-
// Helper-call fanout: resolve owner chain and emit per resolution.
|
|
763
|
-
const ownerResolutions = resolveOwnerChain(ownerRef, graph, helperIndex, createCycleGuard());
|
|
764
|
-
for (const res of ownerResolutions) {
|
|
765
|
-
const rawOwnerComponentId = ownerIdFromResolution(graph, res.ownerDecl);
|
|
766
|
-
const viaChain = [...res.viaPrefix, outerVia, ...ft.viaTrail];
|
|
767
|
-
// Provisional occurrenceId keyed on the serialised (human-readable) form.
|
|
768
|
-
// Scan.ts re-derives the final occurrenceId using the hashed componentId.
|
|
769
|
-
const provisionalId = serialiseComponentId(rawComponentId);
|
|
770
|
-
const occurrenceId = computeOccurrenceId(provisionalId, filePath, usage.loc.line, usage.loc.column, rawOwnerComponentId !== undefined ? serialiseComponentId(rawOwnerComponentId) : undefined);
|
|
771
|
-
occurrences.push({
|
|
772
|
-
rawComponentId,
|
|
773
|
-
filePath,
|
|
774
|
-
line: usage.loc.line,
|
|
775
|
-
column: usage.loc.column,
|
|
776
|
-
via: viaChain[0] ?? outerVia,
|
|
777
|
-
viaChain,
|
|
778
|
-
props: projectPropUsages(usage.props),
|
|
779
|
-
events: usage.events ?? [],
|
|
780
|
-
occurrenceId,
|
|
781
|
-
depth: 0,
|
|
782
|
-
...(rawOwnerComponentId !== undefined ? { rawOwnerComponentId } : {}),
|
|
783
|
-
...(wraps !== undefined ? { wraps } : {}),
|
|
784
|
-
});
|
|
785
|
-
}
|
|
708
|
+
emitOccurrence(occurrences, usageSite(filePath, usage), rawComponentId, usageOwners(graph, helperIndex, ownerRef, viaOverride, filePath), viaOverride !== undefined ? [viaOverride] : [outerVia, ...ft.viaTrail], definition);
|
|
786
709
|
}
|
|
787
710
|
/**
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
711
|
+
* The one reporter for a JSX usage that produced no occurrence. A host-element
|
|
712
|
+
* root reports nothing. A failed import is `unresolved-import`. A walked value
|
|
713
|
+
* holding an `import()` is `lazy-import-unsupported`, naming the file the
|
|
714
|
+
* `import()` is declared in when that is not the render file. Anything else is
|
|
715
|
+
* `unresolved-reference`, except for a resolved Vue-dialect usage (#621).
|
|
793
716
|
*/
|
|
794
|
-
function
|
|
795
|
-
const
|
|
796
|
-
if (
|
|
717
|
+
function reportDroppedRender(opts, filePath, dialect, usage, outcome) {
|
|
718
|
+
const collector = opts?.collector;
|
|
719
|
+
if (collector === undefined)
|
|
797
720
|
return;
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
721
|
+
if (isHostElementRoot(usage.ref))
|
|
722
|
+
return;
|
|
723
|
+
const at = { filePath, line: usage.loc.line, column: usage.loc.column };
|
|
724
|
+
if (outcome.kind === "import-failed") {
|
|
725
|
+
collector.emit({ code: "unresolved-import", severity: "warning", ...at, specifier: outcome.specifier });
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (outcome.kind === "resolved") {
|
|
729
|
+
// An import() in the walked value is a lazy shape the pipeline could not
|
|
730
|
+
// resolve to a component: a `.then` callback that does more than pick a
|
|
731
|
+
// property, or a target whose component shape could not be confirmed.
|
|
732
|
+
const di = outcome.value === null ? null : firstDynamicImport(outcome.value);
|
|
733
|
+
if (di) {
|
|
734
|
+
collector.emit({
|
|
735
|
+
code: "lazy-import-unsupported",
|
|
736
|
+
severity: "warning",
|
|
737
|
+
...at,
|
|
738
|
+
specifier: di.specifier,
|
|
739
|
+
detail: `the import() target could not be resolved to a component${di.originFile === filePath ? "" : ` (declared in ${di.originFile})`}`,
|
|
740
|
+
});
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (dialect !== "react")
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
collector.emit({
|
|
747
|
+
code: "unresolved-reference",
|
|
748
|
+
severity: "info",
|
|
749
|
+
...at,
|
|
750
|
+
symbol: usage.ref.symbol,
|
|
751
|
+
memberChain: [...usage.ref.memberChain],
|
|
806
752
|
});
|
|
807
753
|
}
|
|
808
754
|
/**
|
|
@@ -818,80 +764,25 @@ function pushLocalOccurrence(occurrences, filePath, usage, decl, graph, fileGrap
|
|
|
818
764
|
const value = usage.ref.memberChain.length > 0 ? resolveReference(graph, fileGraph, usage.ref) : decl.value;
|
|
819
765
|
const folded = walkWithFolding(graph, fileGraph, value, argMap);
|
|
820
766
|
const jsxTerminals = folded.filter((t) => t.type.kind === "JSX");
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
// unresolvable alias — emits no occurrence.
|
|
824
|
-
if (jsxTerminals.length === 0) {
|
|
825
|
-
emitLazyImportUnsupported(opts, filePath, usage.loc, value);
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
767
|
+
if (jsxTerminals.length === 0)
|
|
768
|
+
return value;
|
|
828
769
|
const outerVia = viaOverride ?? { kind: "local-component" };
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
};
|
|
838
|
-
const rawComponentId = componentIdFromIdentity(ft.identity, fallback, fileGraph.dialect, {
|
|
839
|
-
filePath,
|
|
840
|
-
resolveExternalLeaf: opts?.resolveExternalLeaf,
|
|
841
|
-
});
|
|
842
|
-
const viaChain = [viaOverride];
|
|
843
|
-
const provisionalId = serialiseComponentId(rawComponentId);
|
|
844
|
-
const occurrenceId = computeOccurrenceId(provisionalId, filePath, usage.loc.line, usage.loc.column, rawOwnerComponentId !== undefined ? serialiseComponentId(rawOwnerComponentId) : undefined);
|
|
845
|
-
occurrences.push({
|
|
846
|
-
rawComponentId,
|
|
847
|
-
filePath,
|
|
848
|
-
line: usage.loc.line,
|
|
849
|
-
column: usage.loc.column,
|
|
850
|
-
via: viaChain[0] ?? outerVia,
|
|
851
|
-
viaChain,
|
|
852
|
-
props: projectPropUsages(usage.props),
|
|
853
|
-
events: usage.events ?? [],
|
|
854
|
-
occurrenceId,
|
|
855
|
-
depth: 0,
|
|
856
|
-
definition: { line: decl.loc.line, column: decl.loc.column },
|
|
857
|
-
...(rawOwnerComponentId !== undefined ? { rawOwnerComponentId } : {}),
|
|
858
|
-
});
|
|
859
|
-
}
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
// Helper-call fanout: resolve owner chain and emit per (terminal × resolution).
|
|
863
|
-
const ownerResolutions = resolveOwnerChain(ownerRef, graph, helperIndex, createCycleGuard());
|
|
770
|
+
const site = usageSite(filePath, usage);
|
|
771
|
+
const owners = usageOwners(graph, helperIndex, ownerRef, viaOverride, fileGraph.filePath);
|
|
772
|
+
const fallback = {
|
|
773
|
+
kind: componentKindFor(fileGraph.dialect),
|
|
774
|
+
export: compoundExportName(usage.ref.symbol, usage.ref.memberChain),
|
|
775
|
+
source: { type: "local", filePath },
|
|
776
|
+
};
|
|
777
|
+
const definition = { line: decl.loc.line, column: decl.loc.column };
|
|
864
778
|
for (const ft of jsxTerminals) {
|
|
865
|
-
const fallback = {
|
|
866
|
-
kind: componentKindFor(fileGraph.dialect),
|
|
867
|
-
export: compoundExportName(usage.ref.symbol, usage.ref.memberChain),
|
|
868
|
-
source: { type: "local", filePath },
|
|
869
|
-
};
|
|
870
779
|
const rawComponentId = componentIdFromIdentity(ft.identity, fallback, fileGraph.dialect, {
|
|
871
780
|
filePath,
|
|
872
781
|
resolveExternalLeaf: opts?.resolveExternalLeaf,
|
|
873
782
|
});
|
|
874
|
-
|
|
875
|
-
const rawOwnerComponentId = ownerIdFromResolution(graph, res.ownerDecl);
|
|
876
|
-
const viaChain = [...res.viaPrefix, outerVia, ...ft.viaTrail];
|
|
877
|
-
const provisionalId = serialiseComponentId(rawComponentId);
|
|
878
|
-
const occurrenceId = computeOccurrenceId(provisionalId, filePath, usage.loc.line, usage.loc.column, rawOwnerComponentId !== undefined ? serialiseComponentId(rawOwnerComponentId) : undefined);
|
|
879
|
-
occurrences.push({
|
|
880
|
-
rawComponentId,
|
|
881
|
-
filePath,
|
|
882
|
-
line: usage.loc.line,
|
|
883
|
-
column: usage.loc.column,
|
|
884
|
-
via: viaChain[0] ?? outerVia,
|
|
885
|
-
viaChain,
|
|
886
|
-
props: projectPropUsages(usage.props),
|
|
887
|
-
events: usage.events ?? [],
|
|
888
|
-
occurrenceId,
|
|
889
|
-
depth: 0,
|
|
890
|
-
definition: { line: decl.loc.line, column: decl.loc.column },
|
|
891
|
-
...(rawOwnerComponentId !== undefined ? { rawOwnerComponentId } : {}),
|
|
892
|
-
});
|
|
893
|
-
}
|
|
783
|
+
emitOccurrence(occurrences, site, rawComponentId, owners, viaOverride !== undefined ? [viaOverride] : [outerVia, ...ft.viaTrail], ft.identity === null ? definition : undefined);
|
|
894
784
|
}
|
|
785
|
+
return value;
|
|
895
786
|
}
|
|
896
787
|
/**
|
|
897
788
|
* A holder is folded away iff its fold reaches JSX and EVERY JSX
|
|
@@ -922,17 +813,28 @@ function foldedHolder(holderJsx, filePath) {
|
|
|
922
813
|
* the same helper `hoc-wrapper` uses. The caller compacts the slice
|
|
923
814
|
* (admission + dedup).
|
|
924
815
|
*/
|
|
925
|
-
function pushArgumentSiteOccurrences(occurrences, filePath, holder, call, holderJsx, graph, fileGraph, argMap, opts) {
|
|
816
|
+
function pushArgumentSiteOccurrences(occurrences, filePath, holder, call, holderJsx, graph, fileGraph, argMap, opts, depth = 0) {
|
|
926
817
|
// The holder is its own identity iff its fold reaches JSX and no
|
|
927
818
|
// terminal carries an identity of its own OR a `hoc-wrapper` hop — the hop
|
|
928
819
|
// is the fold's own record that it credited an argument,
|
|
929
820
|
// even when that argument was anonymous (`forwardRef((p, r) => <b/>, x)`),
|
|
930
821
|
// so the holder is a wrapper of it and its other arguments are
|
|
931
|
-
// configuration, not wrapped components.
|
|
932
|
-
|
|
933
|
-
|
|
822
|
+
// configuration, not wrapped components. A wrapper that folded to a call
|
|
823
|
+
// (`memo(createField(Base))`) hands that call over to be judged in the
|
|
824
|
+
// holder's place.
|
|
825
|
+
const shape = callFoldShape(holderJsx);
|
|
826
|
+
if (shape === "wrapper") {
|
|
827
|
+
if (depth >= MAX_ARGUMENT_CALL_DEPTH || !call.args.some((a) => a.kind === "ReturnTypeOf"))
|
|
828
|
+
return;
|
|
829
|
+
const folded = foldedArgument(graph, fileGraph, call, holderJsx, argMap);
|
|
830
|
+
if (folded?.arg.kind !== "ReturnTypeOf")
|
|
831
|
+
return;
|
|
832
|
+
const innerJsx = walkWithFolding(graph, fileGraph, folded.arg, argMap).filter((t) => t.type.kind === "JSX");
|
|
833
|
+
pushArgumentSiteOccurrences(occurrences, filePath, holder, folded.arg, innerJsx, graph, fileGraph, argMap, opts, depth + 1);
|
|
934
834
|
return;
|
|
935
835
|
}
|
|
836
|
+
if (shape !== "product")
|
|
837
|
+
return;
|
|
936
838
|
const holderRef = {
|
|
937
839
|
symbol: holder.symbol,
|
|
938
840
|
scope: MODULE_SCOPE,
|
|
@@ -947,7 +849,24 @@ function pushArgumentSiteOccurrences(occurrences, filePath, holder, call, holder
|
|
|
947
849
|
const rawOwnerComponentId = hasOnlyHostElementNames(holder.symbol, fileGraph)
|
|
948
850
|
? undefined
|
|
949
851
|
: ownerIdFromRef(graph, holderRef, filePath);
|
|
950
|
-
pushCallArgumentOccurrences(occurrences, filePath, call, () => [{ rawOwnerComponentId, viaPrefix: [] }], graph, fileGraph, argMap, opts);
|
|
852
|
+
pushCallArgumentOccurrences(occurrences, filePath, call, () => [{ rawOwnerComponentId, viaPrefix: [] }], graph, fileGraph, argMap, opts, depth);
|
|
853
|
+
}
|
|
854
|
+
/** Calls below the outermost one that argument seeding descends through. */
|
|
855
|
+
const MAX_ARGUMENT_CALL_DEPTH = 2;
|
|
856
|
+
/**
|
|
857
|
+
* What a call's fold says about the call, from its JSX terminals. A
|
|
858
|
+
* `product` is its own identity: no terminal carries an identity or a
|
|
859
|
+
* `hoc-wrapper` hop, so the fold credited none of its arguments. A `wrapper`
|
|
860
|
+
* credited an argument on every terminal. Null when the fold reaches no JSX
|
|
861
|
+
* or its terminals disagree.
|
|
862
|
+
*/
|
|
863
|
+
function callFoldShape(jsx) {
|
|
864
|
+
if (jsx.length === 0)
|
|
865
|
+
return null;
|
|
866
|
+
const credited = jsx.filter((t) => t.identity !== null || t.viaTrail.some((v) => v.kind === "hoc-wrapper")).length;
|
|
867
|
+
if (credited === 0)
|
|
868
|
+
return "product";
|
|
869
|
+
return credited === jsx.length ? "wrapper" : null;
|
|
951
870
|
}
|
|
952
871
|
/** React's own hook predicate (`isHookName` in rules-of-hooks and the compiler). */
|
|
953
872
|
const HOOK_NAME = /^use[A-Z0-9]/;
|
|
@@ -957,27 +876,36 @@ const HOOK_NAME = /^use[A-Z0-9]/;
|
|
|
957
876
|
* module-scope holder loop and the hook loop in `resolve` both call it;
|
|
958
877
|
* neither re-derives the argument rules or the push.
|
|
959
878
|
*
|
|
960
|
-
*
|
|
961
|
-
* the wrapper, they are
|
|
962
|
-
* resolved through the walker so
|
|
963
|
-
* leaves take the same path a render
|
|
964
|
-
*
|
|
879
|
+
* A call's own arguments only — the arguments of a call in its callee chain
|
|
880
|
+
* configure the wrapper (`compose(withRouter, withTheme)(Foo)`), they are
|
|
881
|
+
* not wrapped. An identifier argument is resolved through the walker so
|
|
882
|
+
* aliases, re-exports and import-backed leaves take the same path a render
|
|
883
|
+
* site does; a null identity is skipped, never invented. An identifier that
|
|
884
|
+
* is a host-element name (`useSelector(pick, shallowEqual)`) can never be
|
|
965
885
|
* rendered as a component, whatever it resolves to — an external leaf has
|
|
966
886
|
* no shape to check and would otherwise synthesise a component out of any
|
|
967
887
|
* import; the same predicate the held-reference seam and the roster naming
|
|
968
888
|
* gate apply.
|
|
969
889
|
*
|
|
890
|
+
* A call-valued argument is followed by what its own fold says
|
|
891
|
+
* (`callFoldShape`), at most `MAX_ARGUMENT_CALL_DEPTH` calls below the
|
|
892
|
+
* outermost one. A wrapper is followed to the argument its fold credited
|
|
893
|
+
* (`foldedArgument`), and the hop joins the chain after `passed-as-argument`.
|
|
894
|
+
* A product becomes the site: its own arguments are seeded and the hops
|
|
895
|
+
* start again. A call whose fold reaches no JSX is configuration. Any other
|
|
896
|
+
* argument kind is skipped.
|
|
897
|
+
*
|
|
970
898
|
* `owners` is resolved on the first push and reused: most hook calls
|
|
971
899
|
* (`useState(x)`, `useEffect(fn, deps)`) never reach a push, and resolving
|
|
972
900
|
* the owner chain for each of them measured as roughly half of the hook
|
|
973
901
|
* loop's scan-time cost on a large consumer repo.
|
|
974
902
|
*/
|
|
975
|
-
function pushCallArgumentOccurrences(occurrences, filePath, call, owners, graph, fileGraph, argMap, opts) {
|
|
903
|
+
function pushCallArgumentOccurrences(occurrences, filePath, call, owners, graph, fileGraph, argMap, opts, depth = 0) {
|
|
976
904
|
let resolvedOwners;
|
|
977
|
-
|
|
978
|
-
if (
|
|
905
|
+
const seedIdentifier = (site, index, arg, hops) => {
|
|
906
|
+
if (isHostElementName(arg.ref.symbol))
|
|
979
907
|
return;
|
|
980
|
-
const outerVia = { kind: "passed-as-argument", index, ...argumentProvenance(
|
|
908
|
+
const outerVia = { kind: "passed-as-argument", index, ...argumentProvenance(site, arg, fileGraph) };
|
|
981
909
|
const argDecl = findLocalDeclaration(fileGraph, arg.ref);
|
|
982
910
|
// Never applied: every terminal used below carries a non-null identity,
|
|
983
911
|
// and `buildComponentId` returns the fallback only for a null one. The
|
|
@@ -990,29 +918,33 @@ function pushCallArgumentOccurrences(occurrences, filePath, call, owners, graph,
|
|
|
990
918
|
filePath,
|
|
991
919
|
resolveExternalLeaf: opts?.resolveExternalLeaf,
|
|
992
920
|
});
|
|
993
|
-
const wraps = computeWraps(rawComponentId, opts?.wrapCallback);
|
|
994
921
|
resolvedOwners ??= owners();
|
|
995
|
-
|
|
996
|
-
const viaChain = [...viaPrefix, outerVia, ...ft.viaTrail];
|
|
997
|
-
const occurrenceId = computeOccurrenceId(serialiseComponentId(rawComponentId), filePath, arg.ref.loc.line, arg.ref.loc.column, rawOwnerComponentId !== undefined ? serialiseComponentId(rawOwnerComponentId) : undefined);
|
|
998
|
-
occurrences.push({
|
|
999
|
-
rawComponentId,
|
|
1000
|
-
filePath,
|
|
1001
|
-
line: arg.ref.loc.line,
|
|
1002
|
-
column: arg.ref.loc.column,
|
|
1003
|
-
via: viaChain[0] ?? outerVia,
|
|
1004
|
-
viaChain,
|
|
1005
|
-
props: {},
|
|
1006
|
-
events: [],
|
|
1007
|
-
occurrenceId,
|
|
1008
|
-
depth: 0,
|
|
1009
|
-
...(argDecl !== null ? { definition: { line: argDecl.loc.line, column: argDecl.loc.column } } : {}),
|
|
1010
|
-
...(rawOwnerComponentId !== undefined ? { rawOwnerComponentId } : {}),
|
|
1011
|
-
...(wraps !== undefined ? { wraps } : {}),
|
|
1012
|
-
});
|
|
1013
|
-
}
|
|
922
|
+
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);
|
|
1014
923
|
}
|
|
1015
|
-
}
|
|
924
|
+
};
|
|
925
|
+
const seedArguments = (site, siteDepth) => {
|
|
926
|
+
site.args.forEach((arg, index) => seedArgument(site, index, arg, [], siteDepth));
|
|
927
|
+
};
|
|
928
|
+
const seedArgument = (site, index, arg, hops, callDepth) => {
|
|
929
|
+
if (arg.kind === "TypeOf") {
|
|
930
|
+
seedIdentifier(site, index, arg, hops);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
if (arg.kind !== "ReturnTypeOf" || callDepth >= MAX_ARGUMENT_CALL_DEPTH)
|
|
934
|
+
return;
|
|
935
|
+
const argJsx = walkWithFolding(graph, fileGraph, arg, argMap).filter((t) => t.type.kind === "JSX");
|
|
936
|
+
const shape = callFoldShape(argJsx);
|
|
937
|
+
if (shape === "product") {
|
|
938
|
+
seedArguments(arg, callDepth + 1);
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
if (shape !== "wrapper")
|
|
942
|
+
return;
|
|
943
|
+
const folded = foldedArgument(graph, fileGraph, arg, argJsx, argMap);
|
|
944
|
+
if (folded !== null)
|
|
945
|
+
seedArgument(site, index, folded.arg, [...hops, folded.via], callDepth + 1);
|
|
946
|
+
};
|
|
947
|
+
seedArguments(call, depth);
|
|
1016
948
|
}
|
|
1017
949
|
/**
|
|
1018
950
|
* Find the ownership entry for the `usageIdx`-th tag usage. The `ownership`
|
|
@@ -1037,27 +969,10 @@ function findTagOwnership(ownership, usageIdx) {
|
|
|
1037
969
|
*/
|
|
1038
970
|
function pushTagOccurrence(occurrences, filePath, usage, rawComponentId, ownerRef, viaOverride, graph, helperIndex) {
|
|
1039
971
|
const via = viaOverride ?? { kind: "html-tag" };
|
|
1040
|
-
// Owner-chain fanout uses the same machinery as JsxUsage
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
const viaChain = [...res.viaPrefix, via];
|
|
1045
|
-
const provisionalId = serialiseComponentId(rawComponentId);
|
|
1046
|
-
const occurrenceId = computeOccurrenceId(provisionalId, filePath, usage.loc.line, usage.loc.column, rawOwnerComponentId !== undefined ? serialiseComponentId(rawOwnerComponentId) : undefined);
|
|
1047
|
-
occurrences.push({
|
|
1048
|
-
rawComponentId,
|
|
1049
|
-
filePath,
|
|
1050
|
-
line: usage.loc.line,
|
|
1051
|
-
column: usage.loc.column,
|
|
1052
|
-
via: viaChain[0] ?? via,
|
|
1053
|
-
viaChain,
|
|
1054
|
-
props: projectPropUsages(usage.props),
|
|
1055
|
-
events: usage.events ?? [],
|
|
1056
|
-
occurrenceId,
|
|
1057
|
-
depth: 0,
|
|
1058
|
-
...(rawOwnerComponentId !== undefined ? { rawOwnerComponentId } : {}),
|
|
1059
|
-
});
|
|
1060
|
-
}
|
|
972
|
+
// Owner-chain fanout uses the same machinery as JsxUsage, a `viaOverride`
|
|
973
|
+
// notwithstanding.
|
|
974
|
+
const owners = chainOwners(graph, helperIndex, ownerRef);
|
|
975
|
+
emitOccurrence(occurrences, usageSite(filePath, usage), rawComponentId, owners, [via]);
|
|
1061
976
|
}
|
|
1062
977
|
/**
|
|
1063
978
|
* Decide the `kind` for a JSX-emitted ComponentId. A concrete
|