Prechádzať zdrojové kódy

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>
Colby Mchenry 1 týždeň pred
rodič
commit
763ee9c825

+ 1 - 1
CHANGELOG.md

@@ -17,7 +17,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 ### New Features
 
 - Same-named symbols across a monorepo's apps are no longer conflated. In a NestJS-style workspace with one `UserService` per app, `codegraph_callers`, `codegraph_callees`, and `codegraph_impact` now report **one section per distinct definition** — each app's callers and blast radius under its own file-labeled heading — instead of a single merged list, and accept a `file` argument to focus exactly the definition you mean (like `codegraph_node` already did). Impact in particular no longer overstates a change's blast radius by merging unrelated same-named classes. Thanks @Igorgro. (#764)
-- Fixed a related source of cross-package wrong edges: PascalCase **type references from plain `.ts` files were being resolved as React components**, which could link a file's own type alias to an arbitrary same-named class in another package (on one large monorepo this produced over a thousand wrong cross-package reference edges; 96% are now gone, and the remainder are genuine shared-model imports). Component resolution now applies only to references from JSX-capable files and never guesses between multiple candidates without a positional signal. Re-index a project to benefit. (#764) (TypeScript, React)
+- Fixed a related source of cross-package wrong edges: PascalCase **type references from plain `.ts` files were being resolved as React components**, which could link a file's own type alias to an arbitrary same-named class in another package (on one large monorepo this produced over a thousand wrong cross-package reference edges; 96% are now gone, and the remainder are genuine shared-model imports). Component resolution now applies only to references from JSX-capable files and never guesses between multiple candidates without a positional signal. The **Svelte and Vue component resolvers had the same arbitrary-pick flaw** (Vue resolved the first same-named `.vue` file found anywhere in the tree) and now follow the same rule: same-directory first, otherwise only an unambiguous name resolves. Re-index a project to benefit. (#764) (TypeScript, React, Svelte, Vue)
 - TypeScript and JavaScript **class fields are now reported as properties instead of methods**. A plain field like `public fonts: Fonts;` previously extracted as a method, misrepresenting class shape and letting calls to same-named functions resolve to data fields (a boolean field named `isArray` was soaking up `Array.isArray(...)` call edges). Fields holding arrow functions or function expressions (`onClick = () => {…}`, including wrapped ones like `onScroll = throttle(() => {…})`) correctly remain methods and their bodies are still analyzed. Field initializers are analyzed too, so `history = createHistory()` records its call — and JavaScript class fields, which previously produced no symbol at all, now appear in the graph. Re-index a project to benefit. (#808) (TypeScript, JavaScript)
 - Callback registration through `this` now resolves precisely in TypeScript and JavaScript: `window.addEventListener("online", this.onOfflineStatusToggle)` or an API object like `{ mutateElement: this.mutateElement }` produces a reference edge to the **enclosing class's own method** — never a same-named method on an unrelated class, and never a data field. Builds on the callback-registration support below. (#808) (TypeScript, JavaScript)
 - Callback-registration coverage deepened across four more shapes: a `this.<member>` registration whose method lives on a **base class** now resolves through the inheritance chain (`bus.on("submit", this.handleSubmit)` in a subclass links to the parent's `handleSubmit`); Java and Kotlin **method references to other classes** (`Handlers::onMessage`, `OtherClass::handle`) resolve across files, with `this::` and `super::` scoped to the defining class and references through a variable deliberately left out; and Swift bare callback names now match only the **enclosing type's** methods (implicit `self`), eliminating a class of wrong edges where a parameter like `request` linked to a same-named method on an unrelated type. (Java, Kotlin, Swift, TypeScript, JavaScript)

+ 5 - 1
src/resolution/frameworks/svelte.ts

@@ -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
src/resolution/frameworks/vue.ts

@@ -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;
+
+  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;
+  };
 
-  // Check same directory first for better specificity
+  // 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;
 }
 
 /**