inheritors_of can report a same-named class from another namespace as an inheritor
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: NoneNot 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
- Resolve the base name against namespace/package/import context before treating the relationship as resolved — the real fix, and the one worth doing.
- 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/Typekinds) 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 caveateShop 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_qualifiedas a bare-name key (spring_resolver,temporal_resolver,refactor.py, plus this query path). It also records thatGraphStore._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/IMPLEMENTSare 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