Skip to content

Out-of-line member template definition gets a different EntityId scope than its in-class declaration (header parser; template_graph fixed)

Index: Known gaps — closed and moot

Status (2026-10-25): CLOSED. Fixed by extract/headers/clang/redeclarations.py; see the closed entry in Known gaps — closed and moot. The investigation below is kept for history. Earlier status: Status (re-verified 2026-10-09 at 456989f): PARTIAL.

struct A { template<class T> void f(T); }; template <class T> void A::f(T) {} -- confirmed by direct compilation (clang -Xclang -ast-dump=json) that clang emits TWO FunctionTemplateDecl nodes for f: one lexically nested inside A's own CXXRecordDecl (the in-class declaration), and one at the ENCLOSING namespace's own lexical level (a sibling of struct A, not a child of it) carrying a parentDeclContextId pointing back at A's own node id -- clang's own signal for "this out-of-line definition's real semantic owner is A, even though it isn't lexically nested inside it." _ClangAstParser._walk computes both scope/scope_path purely from LEXICAL nesting, with no parentDeclContextId handling anywhere in this codebase, so the out-of-line definition gets scope=() while the in-class declaration gets scope=(Record("A"),) -- parse_functions parses BOTH nodes into separate Function entries with disagreeing EntityIds for what is really one entity. The review further confirmed parse_variables has the analogous gap for an out-of-line class-template static data member definition. Not fixed: unlike this phase's other fixes (each a small, local addition to one already-threaded parameter), closing this properly needs a NEW general-purpose facility this codebase doesn't have yet -- a typed, ScopePath-valued sibling of the existing dumper_clang_expr._index_decl_id_qualified_names (which already indexes every decl id to a FLAT qualified-name string for a different consumer, but a flat string cannot be losslessly converted back into a typed ScopePath -- collapsing Record-vs-Namespace-vs-InlineNamespace is exactly the ambiguity ScopePath was built to prevent). That index would need building once per parse, threading through both parse_functions and parse_variables, and reasoning through further edge cases this investigation did not exhaustively enumerate (an out-of-line member of a NESTED class, an out-of-line member of a class-template SPECIALIZATION, and whether castxml's own context resolution has the identical gap for parity). A correct fix needs that index plus verifying each edge case against a real compilation before landing, not a narrow patch for only the one reported shape.