When impl Error for MapperError {} names a supertype that lives outside the
repository, the reference does not stay unresolved. It resolves to whichever local
symbol happens to be called Error — in the repro below, MapperError's own
enum variant — and the graph then asserts an implementation relationship that
does not exist in the source.
Everything downstream believes it: codegraph_explore, impact radius, "who
implements this trait", and any consumer rendering a class diagram all draw the
fabricated realization.
Environment
- codegraph: 1.5.0 (npm), reproduced on
main @ c6aaa20
- Backend: node:sqlite (WAL)
- Platform: Linux x64
- Extraction path is irrelevant — this lives in
src/resolution/, which has no
kernel counterpart, so the native kernel and the wasm/TS walker behave identically
Repro
The trigger is a bare name brought in by use. This matters, because the
fully-qualified spelling does not reproduce.
src/lib.rs:
use std::error::Error;
pub enum MapperError {
Error,
Missing,
}
impl Error for MapperError {}
codegraph init .
sqlite3 .codegraph/codegraph.db "
SELECT e.kind, s.kind, s.name, t.kind, t.name,
json_extract(e.metadata,'$.confidence'),
json_extract(e.metadata,'$.resolvedBy')
FROM edges e JOIN nodes s ON s.id=e.source JOIN nodes t ON t.id=e.target
WHERE e.kind IN ('extends','implements');"
Expected
No edge. std::error::Error is not in the repository, so there is nothing to point
at; the reference belongs in unresolved_refs as failed.
Actual
implements | enum MapperError | enum_member Error | 0.9 | exact-match
Does NOT reproduce
impl std::error::Error for MapperError {} // no `use` — stays unresolved, correct
0 inheritance edges
std::error::Error | implements | failed
resolveOne's fast pre-filter looks the whole dotted string up as a symbol name,
misses, and drops the ref before the name-matcher runs. Only the bare-name form gets
that far. Worth stating explicitly, because it makes the bug look absent when probed
the obvious way.
Also reproduces in Svelte / Vue / Astro
An SFC imports inside its <script> block (Astro: the --- frontmatter) with
ordinary ES module syntax, so the same shape applies:
src/models.ts
export class Serializable { a = 1; }
src/Box.svelte (identical in .vue and .astro)
<script lang="ts">
import { Serializable } from 'some-npm-pkg';
export class SfcBox implements Serializable { n = 1; }
</script>
<div>hi</div>
Actual, all three languages:
implements | class AstroBox @src/Box.astro | class Serializable @src/models.ts
implements | class SfcBox @src/Box.svelte | class Serializable @src/models.ts
implements | class VueBox @src/Box.vue | class Serializable @src/models.ts
Note when reproducing this one: the decoy has to be a single same-named local
symbol. With two competing decoys (an enum_member and a type_alias)
findBestMatch declines and no edge forms, which makes the bug look absent.
Impact (measured)
A 2,669-node Rust/TypeScript application (Tauri desktop app, 149 files):
| target kind |
edges |
correct? |
trait |
43 implements + 7 extends |
yes |
enum_member |
7 |
no |
type_alias |
4 |
no |
11 of 61 inheritance edges (18%) are fabricated. All 11 originate from
use std::error::Error; + impl Error for … across 11 different error types, and
every one lands on a different file's Error symbol.
Cause
Two places in src/resolution/name-matcher.ts, neither of which constrains kind:
matchByExactName builds its candidate pool from getNodesByName and filters by
language gate, kind !== 'import', and lexical reachability. No kind filter.
scoreCandidate treats node kind as a bonus, never a constraint — and awards no
bonus at all for extends/implements; only calls, instantiates and
decorates have one.
With exactly one same-named node in the repo, the single-candidate shortcut adopts it
unconditionally at confidence 0.9 without ever calling findBestMatch. With several,
findBestMatch ranks by path proximity, where an enum variant competes on equal
footing with a trait.
The part that makes the obvious fix useless
Filtering candidates by kind does not remove the false edge — it moves it.
Restricting extends/implements candidates to kinds that can be a supertype (so
enum_member is no longer eligible), measured on the same fixture:
| target kind |
before |
after kind filter alone |
enum_member |
7 |
0 |
type_alias |
4 |
11 |
trait (correct) |
50 |
50 |
The same 11 false edges, now pointing at an unrelated local pub type Error = ….
And it is worse for a consumer: type_alias is a legal supertype in TypeScript
(class X implements SomeAliasedObjectType), so it cannot be rejected by kind
downstream, whereas enum_member could.
The second missing constraint is locality: a name the file imports from outside
the repository has no in-repo referent at all, so no candidate of any kind is correct.
I have a fix ready and will open a PR referencing this issue.
When
impl Error for MapperError {}names a supertype that lives outside therepository, the reference does not stay unresolved. It resolves to whichever local
symbol happens to be called
Error— in the repro below,MapperError's ownenum variant — and the graph then asserts an implementation relationship that
does not exist in the source.
Everything downstream believes it:
codegraph_explore, impact radius, "whoimplements this trait", and any consumer rendering a class diagram all draw the
fabricated realization.
Environment
main@c6aaa20src/resolution/, which has nokernel counterpart, so the native kernel and the wasm/TS walker behave identically
Repro
The trigger is a bare name brought in by
use. This matters, because thefully-qualified spelling does not reproduce.
src/lib.rs:Expected
No edge.
std::error::Erroris not in the repository, so there is nothing to pointat; the reference belongs in
unresolved_refsasfailed.Actual
Does NOT reproduce
resolveOne's fast pre-filter looks the whole dotted string up as a symbol name,misses, and drops the ref before the name-matcher runs. Only the bare-name form gets
that far. Worth stating explicitly, because it makes the bug look absent when probed
the obvious way.
Also reproduces in Svelte / Vue / Astro
An SFC imports inside its
<script>block (Astro: the---frontmatter) withordinary ES module syntax, so the same shape applies:
src/models.tssrc/Box.svelte(identical in.vueand.astro)Actual, all three languages:
Note when reproducing this one: the decoy has to be a single same-named local
symbol. With two competing decoys (an
enum_memberand atype_alias)findBestMatchdeclines and no edge forms, which makes the bug look absent.Impact (measured)
A 2,669-node Rust/TypeScript application (Tauri desktop app, 149 files):
traitimplements+ 7extendsenum_membertype_alias11 of 61 inheritance edges (18%) are fabricated. All 11 originate from
use std::error::Error;+impl Error for …across 11 different error types, andevery one lands on a different file's
Errorsymbol.Cause
Two places in
src/resolution/name-matcher.ts, neither of which constrains kind:matchByExactNamebuilds its candidate pool fromgetNodesByNameand filters bylanguage gate,
kind !== 'import', and lexical reachability. No kind filter.scoreCandidatetreats node kind as a bonus, never a constraint — and awards nobonus at all for
extends/implements; onlycalls,instantiatesanddecorateshave one.With exactly one same-named node in the repo, the single-candidate shortcut adopts it
unconditionally at confidence 0.9 without ever calling
findBestMatch. With several,findBestMatchranks by path proximity, where an enum variant competes on equalfooting with a trait.
The part that makes the obvious fix useless
Filtering candidates by kind does not remove the false edge — it moves it.
Restricting
extends/implementscandidates to kinds that can be a supertype (soenum_memberis no longer eligible), measured on the same fixture:enum_membertype_aliastrait(correct)The same 11 false edges, now pointing at an unrelated local
pub type Error = ….And it is worse for a consumer:
type_aliasis a legal supertype in TypeScript(
class X implements SomeAliasedObjectType), so it cannot be rejected by kinddownstream, whereas
enum_membercould.The second missing constraint is locality: a name the file imports from outside
the repository has no in-repo referent at all, so no candidate of any kind is correct.
I have a fix ready and will open a PR referencing this issue.