Skip to content

Design: separate base-type extraction, canonical declaration identity, and resolution for INHERITS edges #943

Description

@merlincat11

Raised out of #939, which is parked in favour of doing this deliberately. #935 is the motivating bug and should be a test case for whatever design lands.

Problem

INHERITS.target_qualified currently serves three incompatible roles at once:

role example
raw source reference Models.GenericBase<string>
canonical declaration reference Models.GenericBase + arity
resolved graph target /repo/GenericBase.cs::GenericBase

#935 is a symptom: a generic base is stored as GenericBase<string>, which never matches
the node GenericBase, so inheritors_of returns zero and asserts the absence.

#939 attempted to fix this by normalizing the stored target to the erased declaration
name. That makes a shared field lossy, and the field has more than one consumer. Two
new false positives resulted, both verified against main.

Verified regressions from the erase-in-place approach

These are the constraints any design must satisfy. Both were reproduced by building the
same source with main and with the #939 branch.

1. Spring resolution picks the wrong implementation.

public interface Store<T> { T get(); }

@Component
public class StringStore implements Store<String> { public String get() {...} }

@Service
public class Consumer {
    @Autowired Store<Integer> store;
    public void run() { store.get(); }
}

spring_resolver.py builds its implementation map directly from INHERITS.target_qualified
and resolves when a key has exactly one implementor. INJECTS already reduces
Store<Integer> to Store, so once INHERITS also erases to Store the two meet:

main : CALLS Consumer.run -> Store.get          (conservative)
#939 : CALLS Consumer.run -> StringStore.get    (wrong implementation)

This rewrites a CALLS edge, so the error propagates into impact radius, flows and review
context. Generic parameters are deliberately autowiring qualifiers in Spring, so
Store<String> and Store<Integer> are distinct candidates.

2. C# generic arity is part of declaration identity.

// I0.cs
public interface I { }
// I1.cs
public interface I<T> { }
// Child.cs
public class Child : I<int> { }

Querying the non-generic I0.cs::I:

main : 0 results
#939 : ['Child']        (Child implements I<int>, not I)

inheritors_of falls back from the qualified node to node.name filtered only by
language, and extra.constructed_types is not consulted on that path. This is within one
namespace, so it is distinct from #940.

Requirements for a design

  • Keep the raw constructed spelling losslessly. A stored edge should not destroy
    information because the current query layer cannot use it yet.
  • Give the canonical declaration reference enough identity to be correct for C#: at
    minimum the name plus generic arity (I, I<>, I<,>, or metadata-style I`1).
    Java and C# should not necessarily share one normalization rule.
  • Make resolution consult that canonical form, rather than having a lookup fallback do
    bare-name matching that the stored format was reshaped to satisfy.
  • Audit every consumer of INHERITS.target_qualified before changing its meaning.
    Known consumers today: tools/query.py (inheritors_of), spring_resolver.py
    (implementation map). This list must be re-derived, not trusted.
  • Prefer a false negative over a confident false positive. [Bug]: C# and Java. Inheritance through a generic base class is dropped, and inheritors_of reports it as a confirmed absence #935's actual complaint was
    the graph asserting a relationship it had not established.

Prior art in this repo

  • CPP_IDENTITY_VERSION in incremental.py handles a changed persisted identity format
    by rebuilding with the real parser. That is the right mechanism if a stored format
    changes, in preference to a SQL data migration that reimplements parser logic (see the
    v10 attempt in fix(parser): resolve generic inheritance targets #939's history).
  • Tree-sitter already exposes the structure needed to extract a base name without
    scanning for angle brackets: C# generic_nameidentifier + type_argument_list,
    Java generic_typetype_identifier + type_arguments. A character scanner gets
    class C : I</* < */ string> {} wrong, because < also occurs in trivia.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions