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:
co-authored by
Claude Opus 4.8
parent
222af6b87c
commit
763ee9c825
+1
-1
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
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);
|
||||
if (component) {
|
||||
return component.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user