@component-compass/reference-graph 0.1.23 → 0.1.25

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