Skip to content

fix(cpp): extract out-of-line member definitions - #577

Merged
zzet merged 1 commit into
mainfrom
fix/cpp-out-of-line-member-definitions
Aug 13, 2026
Merged

fix(cpp): extract out-of-line member definitions#577
zzet merged 1 commit into
mainfrom
fix/cpp-out-of-line-member-definitions

Conversation

@zzet

@zzet zzet commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Summary

Found while reviewing #576. That PR made std::chrono::duration_cast(x) and boost::asio::post(ex) emit call edges at any qualification depth — but in a typical translation unit those calls sit in out-of-line member bodies, and those definitions extract as nothing at all.

void ns::Cls::run() {} declares itself with a qualified_identifier, which no function_definition pattern in qCppAll admitted (every one required declarator: (identifier)). No method node was emitted, and every call in the body went with it — a deferred call needs an enclosing function range to attach to. Probed against main before this change, a .cpp whose class lives in a header extracts only the file node and its imports edge:

#include "cls.h"

void Cls::depth2() { boost::asio::post(ex); }     // no node, no call edge
void ns::Cls::depth3() { std::chrono::duration_cast<int>(v); }   // same

Changes

  • Each function_definition pattern now admits [(identifier) (qualified_identifier)] in the declarator slot.
  • emitOutOfLineMember reads the qualifier structurally and lets the trailing segment decide the shape:
    • type owner → a method keyed on the owner (demo.cpp::Cls.run), member_of the class when this file declares it. Keying on the owner is what keeps void A::run() and void B::run() in one file from colliding on a single ID and dropping the second.
    • all-namespace qualifier → a free function carrying the qualifier as scope_ns, since void ns::helper() {} is not a member. A class/struct emitted from this file is proof of a type; otherwise the same Capitalized-name heuristic isCapitalizedCppType already uses decides.
    • Destructors, operators, pointer/reference returns, and out-of-line template members (template <typename T> void Holder<T>::put(T)) reach the same walk.
  • Folds cppQualifiedCallName (added by fix(cpp): emit nested qualified call edges #576) and lastIdentifier into that single walk — a qualified callee and a qualified declarator are the same shape. lastIdentifier scanned direct children only, so once a name nested it returned the first segment rather than the last: ns::Cls::bar gave ns. One consequence of the shared walk is that a qualified operator call (ns::operator+(a, b)) now emits an edge instead of being dropped; covered by a test.

Testing

  • go test ./internal/parser/languages -run Cpp -count=1 — all pass, including 7 new cases (depth 2/3/4 members, same-name members in one file, namespace-qualified free function, local-class member_of, destructor/operator/template member, qualified operator call, and a regression case pinning free functions and inline methods unchanged).
  • go test -race ./internal/parser/languages -count=1
  • go test ./internal/parser/... ./internal/resolver/... ./internal/indexer/... ./internal/semantic/lsp/... ./internal/search/rerank/... -count=1 — the packages carrying C++ fixtures outside the extractor.
  • go build ./..., go vet ./internal/parser/languages, golangci-lint run ./internal/parser/languages/... (0 issues), git diff --check.

A definition written outside its declaring scope — `void ns::Cls::run() {}`
in the .cpp for a header's class — declares itself with a
qualified_identifier, a shape no function_definition pattern admitted. The
definition emitted no node at all, and every call in its body went with it:
a deferred call needs an enclosing function range to attach to. In a
translation unit whose class lives in a header, that is the whole file.

Admit a qualified declarator in each function_definition pattern and read
the qualifier structurally. The trailing segment decides the shape: a type
owner yields a method keyed on the owner (member_of the class when this
file declares it, so `A::run` and `B::run` no longer collide), and an
all-namespace qualifier yields a free function carrying the qualifier as
scope_ns. Destructors, operators, pointer and reference returns, and
out-of-line template members reach the same walk.

Folds cppQualifiedCallName and lastIdentifier into that one walk — a
qualified callee and a qualified declarator are the same shape. lastIdentifier
scanned direct children only, so once a name nested it returned the first
segment rather than the last (`ns::Cls::bar` gave `ns`).
@zzet
zzet merged commit 662b980 into main Aug 13, 2026
10 checks passed
@zzet
zzet deleted the fix/cpp-out-of-line-member-definitions branch August 15, 2026 08:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant