inheritors_of can report a same-named class from another namespace as an inheritor

Author: merlincat11Created Aug 30, 2026Updated Sep 16, 2026
Labelsbug

Split out of #939 (review discussion) to keep that PR scoped to the parser bug in #935.

Problem

inheritors_of resolves the requested node, and when exact qualified-target lookup finds nothing it falls back to the node's plain name. That fallback matches edges by bare target plus source language, which does not establish namespace or package identity.

A/GenericBase.cs:  namespace A; class GenericBase<T> {}
B/GenericBase.cs:  namespace B; class GenericBase<T> {}
B/Child.cs:        namespace B; class Child : GenericBase<string> {}

Querying the A declaration by its qualified graph name returns B.Child, with no caveat attached:

$ code-review-graph query inheritors_of /repo/A/GenericBase.cs::GenericBase
count: 1  results: ['Child']   confidence: None

Not specific to generics

Reproduces on main with plain class Child : PlainBase {} and two same-named PlainBase declarations. This is the bare-name fallback, not the generic handling.

Why it matters

#935 objected to the graph making unjustified affirmative claims about inheritance. A false positive asserted without qualification is the same failure pointing the other way.

Options

  1. Resolve the base name against namespace/package/import context before treating the relationship as resolved — the real fix, and the one worth doing.
  2. Attach a caveat when the fallback matched an ambiguous bare name, so the result is returned but not asserted. A prototype existed in #939 (restricting the ambiguity count to Class/Type kinds) and was removed to keep that PR scoped.

Option 2 is a mitigation; option 1 is the fix.


Status

#966 (open) implements option 2. Ambiguous bare-name matches are retained and marked inferred_by: "bare_name", in both standard and detail_level="minimal" output, scoped to inheritors_of so tests_for is unaffected. The false positive in the repro above is still returned — it is caveated rather than asserted. This issue stays open for option 1.

Negative result: option 1 does not work at the query layer

The first implementation of #966 (e7f5f05) attempted option 1 at query time, resolving the base name from file-level namespace and import metadata: same file, an IMPORTS_FROM edge to the declaring file, or a namespace the declaring file declares, walking outwards to enclosing namespaces.

Measured against dotnet/eShop (547 C# files; 8 genuinely ambiguous inheritance targets; ground truth read from the namespaces and using directives in source):

returned correct precision recall
main 22 11 0.50 1.00
query-time resolution (e7f5f05) 11 11 1.00 1.00
shipped mitigation (fd1bad7) 22 11 0.50 1.00

The precision gain was real and exact — including correctly keeping HybridApp.Services.CatalogService as an implementor of WebAppComponents.Services.ICatalogService via its local using.

It was still withdrawn, because the same rule silently deletes correct inheritors whenever the using lives outside the child's own file:

Lib/Base.cs          namespace Lib;   public class Widget { }
Other/Base.cs        namespace Other; public class Widget { }
App/GlobalUsings.cs  global using Lib;
App/Child.cs         namespace App;   public class AppWidget : Widget { }

query inheritors_of /repo/Lib/Base.cs::Widget  ->  []
  "Found 0 result(s)" — delivered as a real absence, no caveat

eShop itself ships ten GlobalUsings.cs files, so this is mainstream .NET layout rather than a corner case. File-level metadata cannot see project-wide scope, so a query-time resolver converts this issue's false positive into a confident false negative — worse under the "prefer a false negative over a confident false positive" standard, because it is not presented as a negative at all.

Where option 1 belongs

Build time, not query time, and not standalone:

  • #964 supplies the identity substrate: namespaces in C# qualified names, explicit global usings collected across the inferred project, lexical scope. Without it any resolver repeats the failure above.
  • #943 supplies somewhere to put a resolved target without breaking the five consumers that read INHERITS.target_qualified as a bare-name key (spring_resolver, temporal_resolver, refactor.py, plus this query path). It also records that GraphStore._resolve_bare_endpoints (graph.py:965) is already a generic, kind-parameterized bare-endpoint resolver with the right ambiguity semantics, already carrying the C# namespace-to-file bridge — INHERITS/IMPLEMENTS are simply not registered with it.

With those in place this issue closes by reading the resolved target, leaving bare-name matching as the caveated fallback only.

Source: tirth8205/code-review-graph