diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d8ab6f79..d0a0dec2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,10 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes +- An inheritance relationship is no longer invented between a type and an unrelated symbol that merely shares a name with its supertype. When a class or struct implemented something defined outside your project — a trait from the Rust standard library, an interface from an npm package — the graph would attach that relationship to whatever local symbol happened to have the same name: an enum variant, a type alias, even a function. On a Rust project of moderate size this fabricated a dozen implementation relationships, which then showed up in diagrams and in answers about who implements what. A supertype that isn't in your project is now simply left unresolved, and when several same-named symbols compete, only ones that can actually be a supertype are considered — so the real trait wins instead of losing to a same-named variant. This covers Svelte, Vue and Astro components too, whose `\n
hi
\n'], + ['vue', 'src/Box.vue', '\n\n'], + ['astro', 'src/Box.astro', '---\n$IMPORT$\nexport class SfcBox implements Serializable {\n n = 1;\n}\n---\n
\n'], + ])('drops an npm supertype in a %s single-file component', async (_lang, file, body) => { + // An SFC imports inside its \n
hi
\n` + ); + const { edges } = await load(); + expect(has(edges, 'SfcBox', 'Serializable', 'class')).toBe(true); + }); + + it('does not resolve an import to a type member that shares its name', async () => { + // `import * as path from 'node:path'` is unresolvable — the module is + // external — so the name-matcher looked for any node called `path` and + // found a class property. No language lets you import a type's member. + write('src/types.ts', `export class Request {\n path = '';\n url = '';\n}\n`); + write( + 'src/run.ts', + `import * as path from 'node:path';\n\nexport function run() {\n return path.join('a', 'b');\n}\n` + ); + const cg = await CodeGraph.init(dir, { silent: true }); + await cg.indexAll(); + const db = (cg as any).db.db; + const rows: { tgt: string; tgtKind: string }[] = db + .prepare( + `SELECT t.name tgt, t.kind tgtKind + FROM edges e JOIN nodes t ON t.id = e.target + WHERE e.kind = 'imports'` + ) + .all(); + cg.close?.(); + expect(rows.filter((r) => r.tgtKind === 'property' || r.tgtKind === 'field')).toEqual([]); + }); + + it('keeps class extends class and class implements interface', async () => { + write( + 'src/base.ts', + `export interface Runner { run(): void }\n` + + `export class Base { run(): void {} }\n` + + `export class Child extends Base implements Runner { run(): void {} }\n` + ); + const { edges } = await load(); + expect(has(edges, 'Child', 'Base', 'class')).toBe(true); + expect(has(edges, 'Child', 'Runner', 'interface')).toBe(true); + }); +}); diff --git a/src/resolution/import-resolver.ts b/src/resolution/import-resolver.ts index a32a97916..e65ae40f6 100644 --- a/src/resolution/import-resolver.ts +++ b/src/resolution/import-resolver.ts @@ -287,6 +287,21 @@ const C_CPP_STDLIB_HEADERS = new Set([ 'version', ]); +/** + * Languages whose imports are ES-module specifiers, extracted by + * `extractJSImports` and therefore classified by the same bare-specifier / + * alias / workspace rules. Svelte, Vue and Astro belong here: an SFC imports + * inside its `