@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.
@@ -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
- for (const [filePath, fileGraph] of graph.files) {
123
- for (let usageIdx = 0; usageIdx < fileGraph.jsxUsages.length; usageIdx++) {
124
- const usage = fileGraph.jsxUsages[usageIdx];
125
- if (!usage)
126
- continue;
127
- // Resolve owner symbol ref for this usage (for composition attribution).
128
- const ownerRef = fileGraph.ownership[usageIdx]?.ownerSymbolRef ?? null;
129
- const viaOverride = fileGraph.ownership[usageIdx]?.viaOverride;
130
- const jsxDepth = fileGraph.ownership[usageIdx]?.depth ?? 0;
131
- const jsxParentUsageIdx = fileGraph.ownership[usageIdx]?.parentUsageIdx;
132
- const jsxLengthBefore = occurrences.length;
133
- // Find the import record that introduced the root symbol at module scope.
134
- // We check usage.ref.symbol (the root binding name), not member-chain
135
- // segments, because ImportRecord.local binds the top-level name.
136
- const matchedImport = fileGraph.importsByLocal.get(usage.ref.symbol);
137
- if (!matchedImport) {
138
- // Symbol is locally declared (no import) — emit local identity.
139
- const decl = findLocalDeclaration(fileGraph, usage.ref);
140
- if (!decl) {
141
- if (!isHostElementRoot(usage.ref)) {
142
- // No import record and no declaration in scope: the engine cannot
143
- // attribute this usage. Surface it instead of
144
- // dropping it silently — `info`, because the usual cause is a
145
- // global or macro-provided binding the scan cannot see.
146
- opts?.collector?.emit({
147
- code: "unresolved-reference",
148
- severity: "info",
149
- filePath,
150
- line: usage.loc.line,
151
- column: usage.loc.column,
152
- symbol: usage.ref.symbol,
153
- memberChain: [...usage.ref.memberChain],
154
- });
155
- }
156
- continue;
157
- }
158
- pushLocalOccurrence(occurrences, filePath, usage, decl, graph, fileGraph, argMap, ownerRef, helperIndex, opts, viaOverride);
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
- // Normalize absolute path → repo-relative graph key when possible.
303
- const resolvedNormTarget = resolvedAbsTarget !== null && graph.resolveToGraphKey
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
- const targetInGraph = resolvedNormTarget !== null && graph.files.has(resolvedNormTarget);
307
- if (targetInGraph) {
308
- // Target file is in graph — workspace sibling. Resolve to check
309
- // JSX type, fan out per terminal. The fallback identity is local so
310
- // null-identity terminals still attribute to the sibling file rather
311
- // than a phantom external stub. Terminal identities from deriveIdentity
312
- // are already local when applicable (workspaceSiblingIdentity fires).
313
- const refType = resolveReference(graph, fileGraph, usage.ref);
314
- const foldedTerminals = walkWithFolding(graph, fileGraph, refType, argMap);
315
- const jsxTerminals = foldedTerminals.filter((t) => t.type.kind === "JSX");
316
- if (jsxTerminals.length === 0) {
317
- emitLazyImportUnsupported(opts, filePath, usage.loc, refType);
318
- continue;
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
- const siblingFallback = workspaceSiblingFallback(graph, resolvedNormTarget, matchedImport, effectiveImported, fileGraph.dialect);
321
- const localFallback = siblingFallback !== undefined && siblingFallback.kind !== "custom-element"
322
- ? { ...siblingFallback, export: compoundExportName(siblingFallback.export, residual) }
323
- : siblingFallback;
324
- for (const ft of jsxTerminals) {
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
- else {
329
- // Target not in graph. Workspace membership: a
330
- // resolved target owned by a workspace member is first-party — emit
331
- // LOCAL, pinned to the definition file (bounded re-export resolution)
332
- // so identity is scope-invariant between whole-repo and single-app
333
- // scans. Otherwise: external, trusting the JSX call site (external
334
- // package or unresolvable specifier — identity null →
335
- // matchedImport-based componentId). See `unparsedMemberDefinition`.
336
- const member = resolvedAbsTarget !== null
337
- ? unparsedMemberDefinition(graph, resolvedAbsTarget, effectiveImported)
338
- : null;
339
- const sentinelFt = { type: { kind: "JSX" }, viaTrail: [], identity: null };
340
- if (member !== null) {
341
- const localFallback = {
342
- kind: componentKindFor(fileGraph.dialect),
343
- export: compoundExportName(member.exportName, residual),
344
- source: { type: "local", filePath: member.absFile },
345
- };
346
- pushOccurrence(occurrences, filePath, usage, matchedImport, compoundExportName(effectiveImported, residual), packageName, graph, fileGraph, sentinelFt, ownerRef, helperIndex, opts, localFallback, viaOverride);
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
- stampComposition(jsxLengthBefore, "jsx", usageIdx, jsxDepth, jsxParentUsageIdx);
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 identifier argument of the OUTERMOST call
414
- // that resolves to a component gets one occurrence at the argument's own
415
- // position, owned by the holder. A fold that carried an identity credited
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 = () => resolveOwnerChain(ownerRef, graph, helperIndex, createCycleGuard()).map((res) => ({
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
- * Derive the `wraps` cross-link for a given component identity by calling the
699
- * optional `wrapCallback`. Only fires for external react-component identities —
700
- * local components are user-written code, not wrappers. Returns undefined when
701
- * the callback is absent, returns null, or the identity is not an external
702
- * react-component.
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 computeWraps(rawComponentId, wrapCallback) {
705
- if (wrapCallback === undefined ||
706
- rawComponentId.kind !== "react-component" ||
707
- rawComponentId.source.type !== "external") {
708
- return undefined;
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
- const hit = wrapCallback(rawComponentId.source.package, rawComponentId.export);
711
- if (!hit)
712
- return undefined;
713
- return { kind: "custom-element", tagName: hit.tagName, packageName: hit.packageName };
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
- if (viaOverride !== undefined) {
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
- * A fold that reached no JSX while an import() sits in the walked value is a
789
- * lazy shape the pipeline could not resolve to a component: a `.then` callback
790
- * that does more than pick a property, or a target whose component shape could
791
- * not be confirmed. Report it at the render site so the miss is visible, and
792
- * name the file the `import()` is declared in when that is not the render file.
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 emitLazyImportUnsupported(opts, filePath, loc, value) {
795
- const di = opts?.collector ? firstDynamicImport(value) : null;
796
- if (!di)
717
+ function reportDroppedRender(opts, filePath, dialect, usage, outcome) {
718
+ const collector = opts?.collector;
719
+ if (collector === undefined)
797
720
  return;
798
- opts?.collector?.emit({
799
- code: "lazy-import-unsupported",
800
- severity: "warning",
801
- filePath,
802
- line: loc.line,
803
- column: loc.column,
804
- specifier: di.specifier,
805
- detail: `the import() target could not be resolved to a component${di.originFile === filePath ? "" : ` (declared in ${di.originFile})`}`,
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
- // A reference that resolves to no JSX terminal — a function parameter
822
- // (`<Component/>` inside a factory body), a hook-return destructure, or an
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
- if (viaOverride !== undefined) {
830
- // Prop-forward: parser has pre-decided the owner — single-occurrence path.
831
- const rawOwnerComponentId = ownerIdFromRef(graph, ownerRef, fileGraph.filePath);
832
- for (const ft of jsxTerminals) {
833
- const fallback = {
834
- kind: componentKindFor(fileGraph.dialect),
835
- export: compoundExportName(usage.ref.symbol, usage.ref.memberChain),
836
- source: { type: "local", filePath },
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
- for (const res of ownerResolutions) {
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
- if (holderJsx.length === 0 ||
933
- holderJsx.some((t) => t.identity !== null || t.viaTrail.some((v) => v.kind === "hoc-wrapper"))) {
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
- * The outermost call's arguments only — an inner call's arguments configure
961
- * the wrapper, they are not wrapped. Identifier arguments only, each
962
- * resolved through the walker so aliases, re-exports and import-backed
963
- * leaves take the same path a render site does; a null identity is skipped,
964
- * never invented. An identifier that is a host-element name (`useSelector(pick, shallowEqual)`) can never be
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
- call.args.forEach((arg, index) => {
978
- if (arg.kind !== "TypeOf" || isHostElementName(arg.ref.symbol))
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(call, arg, fileGraph) };
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
- for (const { rawOwnerComponentId, viaPrefix } of resolvedOwners) {
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
- const ownerResolutions = resolveOwnerChain(ownerRef, graph, helperIndex, createCycleGuard());
1042
- for (const res of ownerResolutions) {
1043
- const rawOwnerComponentId = ownerIdFromResolution(graph, res.ownerDecl);
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