fix(resolution): Svelte/Vue component resolvers get the #764 ambiguity rule (#814)

Follow-up to #813: the React resolver's blind components[0] fallback was
the demonstrated wrong-edge source, but the Svelte and Vue resolvers had
the same flaw in their own shape:

- svelte: resolveComponent fell back to components[0] across the whole
  repo when no same-directory match existed — an arbitrary pick among
  same-named components in a multi-app monorepo.
- vue: resolveComponent returned the FIRST basename-matching .vue file
  found anywhere in the tree; its same-directory pass below was
  unreachable dead code. apps/a/Button.vue vs apps/b/Button.vue was a
  file-enumeration-order coin flip.

Both now follow the #764 rule: same-directory first, otherwise only an
UNAMBIGUOUS name resolves — ambiguity falls through to the name-matcher's
proximity scoring instead of guessing.

Safety: zero-delta A/B on the README's own framework benchmark repos
(sveltejs/realworld — the 100% Svelte coverage repo — and nuxt/movies,
93.5% Vue coverage) plus the excalidraw control: node counts identical,
zero calls or references edges changed. Single-app repos have unique
component names, so the rule only bites where the old behavior was
already a coin flip. Full suite 1398 passed.

Also verified the #813 per-definition tool grouping is language-agnostic
(probed Go same-named functions across packages — grouped identically to
the TS fixture).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-11 16:31:18 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 222af6b87c
commit 763ee9c825
3 changed files with 27 additions and 30 deletions
+5 -1
View File
@@ -220,7 +220,11 @@ function resolveComponent(
const sameDir = components.filter((n) => n.filePath.startsWith(fromDir));
if (sameDir.length > 0) return sameDir[0]!.id;
return components[0]!.id;
// No positional signal: only an UNAMBIGUOUS name may resolve — picking
// components[0] chose an arbitrary same-named component in a multi-app
// monorepo (#764). Ambiguity falls through to the name-matcher, whose
// proximity scoring decides.
return components.length === 1 ? components[0]!.id : null;
}
/**
+21 -28
View File
@@ -279,39 +279,32 @@ function resolveComponent(
fromFile: string,
context: ResolutionContext
): string | null {
const allFiles = context.getAllFiles();
const vueFiles = allFiles.filter((f) => f.endsWith('.vue'));
// Check for exact name match (Button -> Button.vue)
for (const file of vueFiles) {
// Collect ALL basename matches first. The previous version returned the
// FIRST `Button.vue` found anywhere in the tree (its same-directory pass
// below was unreachable), so a multi-app monorepo with one `Button.vue`
// per app resolved to an arbitrary one (#764).
const matches: string[] = [];
for (const file of context.getAllFiles()) {
if (!file.endsWith('.vue')) continue;
const fileName = file.split(/[/\\]/).pop() || '';
const componentName = fileName.replace(/\.vue$/, '');
if (componentName === name) {
const nodes = context.getNodesInFile(file);
const component = nodes.find((n) => n.kind === 'component' && n.name === name);
if (component) {
return component.id;
}
}
if (fileName.replace(/\.vue$/, '') === name) matches.push(file);
}
if (matches.length === 0) return null;
// Check same directory first for better specificity
const componentIn = (file: string): string | null => {
const nodes = context.getNodesInFile(file);
const component = nodes.find((n) => n.kind === 'component' && n.name === name);
return component ? component.id : null;
};
// Same directory first for specificity
const fromDir = fromFile.substring(0, fromFile.lastIndexOf('/'));
for (const file of vueFiles) {
if (file.startsWith(fromDir)) {
const fileName = file.split(/[/\\]/).pop() || '';
const componentName = fileName.replace(/\.vue$/, '');
if (componentName === name) {
const nodes = context.getNodesInFile(file);
const component = nodes.find((n) => n.kind === 'component');
if (component) {
return component.id;
}
}
}
}
const sameDir = matches.filter((f) => f.startsWith(fromDir));
if (sameDir.length > 0) return componentIn(sameDir[0]!);
return null;
// No positional signal: only an UNAMBIGUOUS basename may resolve;
// ambiguity falls through to the name-matcher's proximity scoring.
return matches.length === 1 ? componentIn(matches[0]!) : null;
}
/**