Skip to content

fix(cpp): a call through a namespace links no class's method by its name - #2730

Merged
colbymchenry merged 1 commit into
mainfrom
claude/cpp-namespace-qualifier-calls
Oct 21, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/cpp-namespace-qualifier-calls

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Follow-up to #2729.

What was wrong

A C++ call written through a namespace, x::m(), reached matchMethodCall's name strategies whenever no declaration of x::m was found by qualified name. Those strategies can only return a method of some class:

  • Strategy 2: a class named like the qualifier, capitalized (internal → a class Internal);
  • Strategy 3: the one method of that name in the project (0.7), or the best receiver-word guess (0.65).

A namespace holds functions and types, never methods, so for these calls every such pick is another class's member. Worse, they run before matchCppMacroNamespaced, the step that reads the namespaces a macro opens or an alias names, so a guess there hid the right declaration. On main 009e437:

  • protocolbuffers/protobuf: absl::StripPrefix(…) ×12 → the Rust generator's MultiCasePrefixStripper::StripPrefix; absl::Status(…) ×8 → upb::Status's constructor; absl::Cleanup(…) → internal::cleanup::ChunkList::Cleanup; hpb's backend::CreateMessage / backend::ClearMessage → hpb::internal::PrivateAccess::CreateMessage and DynamicExtensionInfoHelper::ClearMessage.
  • fmtlib/fmt: fmt::ptr(p) ×9 → a test scanner's detail::scan_buffer::ptr(); fmt::scan<…>(…) ×21 → a method of the test's scanner<num> specialization; fmt::day/month/year(…) ×10 → a test's date:: classes; fmt::underlying(…) → a ranges formatter's method. fmt's own ptr, scan, underlying and its day/month/year aliases sit in FMT_BEGIN_NAMESPACE.

The change

isCppNamespaceQualifier (name-matcher.ts) holds when the qualifier is lowercase, no C or C++ type or alias node has its name (cppTypesNamed), and the calling file declares it as neither a template parameter nor an alias. That last part reads cppNamesBroughtIn from #2729, now split into type names and namespace names, since a namespace the file opens confirms it is one rather than exempting it. When it holds, Strategy 2 and both Strategy 3 returns give no edge, and the later steps run as before.

One exception: the constructor of a class the call names in that namespace (isCppConstructorThrough). Its owner, right inside the written namespace, is m itself, or a specialization of the class template m, which the index holds without a name. protobuf's internal::SourceWrapper<absl::Cord>(&data) constructs struct SourceWrapper<absl::Cord> in message_lite.cc, whose template is only declared, and keeps its edge. A method of another template's specialization is no constructor: fmt's template <> struct scanner<num> has a scan, but fmt::scan<int>(…) is fmt's scan function.

The check runs only where one of those strategies would return.

Validation

Arms on main 009e437: main3 and ns3 (this branch). They share one kernel, built from main's kernel sources, which this branch doesn't touch. Diffs by natural keys over nodes, every edge and every unresolved ref, plus a call-site diff:

repo removed retargeted added
protocolbuffers/protobuf 21 2 0
fmtlib/fmt 2 41 2
google/leveldb byte-identical
facebook/rocksdb byte-identical
redis/redis (C control) byte-identical

Nodes are identical everywhere. leveldb, rocksdb and redis were indexed with this change before the constructor exception was added, and were byte-identical then. The exception only applies where the guard fires, which it never did there.

Every changed site checked:

  • protobuf, removed 21, all wrong: absl::StripPrefix ×12, absl::Status ×8 and absl::Cleanup ×1. These call abseil, which isn't in the repo; they now link to nothing.
  • protobuf, retargeted 2, wrong → right: hpb's backend::CreateMessage<T>(arena) and backend::ClearMessage(message) now reach hpb::internal::backend::upb::CreateMessage / ClearMessage. They go through the alias hpb/multibackend.h declares in its first #if arm, namespace backend = hpb::internal::backend::upb; (the other arm names the cpp backend). That is qualified-name 0.8, from the step that follows namespace aliases, which the guess used to pre-empt.
  • fmt, retargeted 41, wrong → right:
    • fmt::scan<…>(…) ×21 → the scan function in test/scan.h;
    • fmt::ptr(p) ×6 → ptr in format.h, and ×3 on smart pointers (std-test.cc) → the overload in std.h;
    • fmt::day/month/year(…) ×10 → fmt's day, month, year aliases in chrono.h. chrono.h declares them as using day = std::chrono::day; when the standard library has them, and as fmt's own class day in the #else arm, which the index doesn't hold;
    • fmt::underlying(e) → underlying in format.h.
  • fmt, 2 calls → 2 instantiates: fmt::bytes(…) and fmt::string_buffer(…) went to the constructors bytes::bytes and string_buffer::string_buffer, and now reach fmt's struct bytes and class string_buffer, as a construction does.

Kept by the constructor exception: protobuf's internal::SourceWrapper<absl::Cord>(&data) ×4, and gmock's internal::RefMatcher<T&>(x) in fmt's vendored googletest, which constructs class RefMatcher<T&>. Kept because the qualifier is a type: rocksdb's hash_table::Insert/Find/Erase/GetMutex ×4 through typedef HashTable<T*, Hash, Equal> hash_table;.

Tests

__tests__/cpp-namespace-qualifier.test.ts, 8 cases. 5 fail on main:

  • fmt's ptr;
  • fmt's day alias;
  • abseil;
  • each strategy's pick: Strategy 2's internal::Fill → a class Internal, the word guess log::Flush → LogWriter, and python::Escape through a namespace the calling file opens;
  • fmt::scan<int> beside scanner<num>::scan.

3 guards pass on main: the SourceWrapper specialization's constructor, rocksdb's hash_table typedef, and one case that combines a function's alias, an alias a header declares, a template parameter's default and a lowercase class.

A mutation run (runtime-false edits, so the setup build still compiles) pins every part:

mutation fails
main's sources the 5 red cases
guard off the 5 red cases
the calling file's types ignored the alias / template-parameter case
type and alias nodes ignored the alias / template-parameter case (the header alias)
a namespace the file opens exempted, as #2729 does for capitalized names each strategy's pick, scan
Strategy 2 unguarded each strategy's pick
unique pick unguarded fmt's ptr, day, abseil, each strategy's pick, scan
word guess unguarded each strategy's pick
specialization constructors not kept SourceWrapper
a specialization's head not checked scan

Full suite on 009e437 plus this change, with the kernel built, CODEGRAPH_KERNEL_EXPECT=1 and --maxWorkers=4: 682 files (6 all-skipped), 8,110 tests passed, 82 skipped, 0 failed. That includes #2729's cpp-undeclared-qualifier.test.ts, whose cppNamesBroughtIn this change splits.

Cost

None measurable. Index wall times, main3 / this branch: protobuf 174 s / 150 s, rocksdb 56 s / 54 s, the small repos within a second. The check runs only where Strategy 2 or 3 is about to return a pick for a C++ x::m() call. Its scan of the calling file is #2729's, memoized per file, and a specialization's head is read only for an unnamed owner.

🤖 Generated with Claude Code

A C++ call written through a namespace, `x::m()`, reached matchMethodCall's
name strategies when no declaration of `x::m` was found by qualified name.
They can only return a method of some class, which a namespace never holds:
the capitalized-receiver class (`internal` → a class `Internal`), the one
method of the name in the project, or the best receiver-word guess. So
protocolbuffers/protobuf's `absl::StripPrefix(name, …)` linked to its Rust
generator's `MultiCasePrefixStripper::StripPrefix` and `absl::Status(…)` to
`upb::Status`'s constructor. fmtlib/fmt's `fmt::ptr(p)` linked to a test
scanner's `ptr()` method, and `fmt::day(…)` to a test's `date::day`, ahead of
the step that reads the namespaces a macro opens and finds fmt's own `ptr`
function and `day` alias in `FMT_BEGIN_NAMESPACE`.

Those strategies now return nothing when the qualifier is a namespace
(isCppNamespaceQualifier): a lowercase name that no C or C++ type or alias
node has, and that the calling file declares as neither a template parameter
nor an alias (cppNamesBroughtIn, now split into type names and namespace
names, since a namespace the file opens confirms it is one). rocksdb's
`hash_table::Insert(…)` through `typedef HashTable<…> hash_table;` keeps its
edge. So does the constructor of a class the call names in that namespace
(isCppConstructorThrough): protobuf's `internal::SourceWrapper<absl::Cord>(&data)`
constructs `struct SourceWrapper<absl::Cord>`, a specialization the index
holds without its name, whose template is only declared. A method of
another template's specialization is no constructor: fmt's
`fmt::scan<int>(…)` is fmt's `scan` function, not `scanner<num>::scan`.
The check runs only where one of those strategies would return.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit 81fd007 into main Oct 21, 2026
@colbymchenry
colbymchenry deleted the claude/cpp-namespace-qualifier-calls branch October 21, 2026 05:56
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