From 374b3b4209d93d8be5bc77ca487ba027e76e0758 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Tue, 8 Sep 2026 15:49:10 -0500 Subject: [PATCH] fix(resolution): constrain inheritance/import reference target kinds (#1536, #1537) (#1796) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(resolution): gate extends/implements to real supertypes An inheritance reference bound to whatever local symbol shared its name. The name-matcher scores node kind as a bonus, never a filter, and awards no bonus at all for inheritance refs, so `use std::error::Error;` + `impl Error for MapperError {}` resolved to the local `MapperError::Error` VARIANT — an implementation relationship absent from the source. Two changes, both needed. Filtering by kind alone was measured and it only RELOCATES the false edge: with enum members excluded, the same 7 refs moved onto an unrelated local `type Error` alias, which is a legal supertype kind and therefore harder for a consumer to reject. 1. Eligibility before ranking. `matchByExactName` restricts its candidate pool to kinds that can BE a supertype, so a legitimate trait outranks a same-named variant instead of merely losing its edge. `resolveOne` is wrapped by a gate that applies the same set to every other strategy at one seam — filtering inside the name-matcher would have missed the framework, import, chain and CFML paths. 2. Locality. A name imported from outside the repository has no in-repo referent at all, so no candidate is correct. Only oracles that cannot be wrong are consulted: Rust `use` paths rooted at a stdlib crate, and `isExternalImport` for ES modules. Generalizing the Rust side to "the module path doesn't resolve to a file" was tried and reverted — a crate re-exporting a sibling's modules (`pub use pupil_core::ports;`) has no directory to walk, and that version deleted 13 real trait implementations. Measured on a Rust/Tauri project (2,682 nodes): the 11 false inheritance edges are gone, all 59 real trait relationships are preserved, and node count is unchanged. On this repository as a control, the only edge removed is a class recorded as extending a function. Synthesized-edge counts are identical in both. Co-Authored-By: Claude Opus 5 * fix(resolution): an import never resolves to a member of a type `import * as path from 'node:path'` is unresolvable — the module is external — so the name-matcher fell back to finding any node called `path`, and a common word like path/url/join/get matches a class property or interface method somewhere in almost any repo. Nothing in any supported language lets an import bind to a member that only exists inside a type; you import the type. Same shape as the inheritance gate that precedes it: eligibility applied to the candidate pool before ranking, plus the resolveOne gate as the backstop for every other strategy. On this repository as a control: 19 imports pointing at methods and 4 at properties are gone (all of them coincidences — `Walker::join`, `Telemetry::events`), 3 refs now find the module constant they actually name, node count unchanged. Co-Authored-By: Claude Opus 5 * fix(resolution): classify SFC script imports as ES module specifiers `isExternalImport` had a TS/JS branch listing typescript/tsx/javascript/jsx/ arkts, so for Svelte, Vue and Astro it fell through every branch and returned false — "not external" — for `import { Foo } from 'some-npm-pkg'`. An SFC imports inside its `\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 bee88ff..d0ddfe5 100644 --- a/src/resolution/import-resolver.ts +++ b/src/resolution/import-resolver.ts @@ -316,6 +316,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 `