@component-compass/reference-graph 0.1.24 → 0.1.26

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