Skip to content

fix(cpp): a call on a data member of the caller's class reaches its type's method, wherever the class is declared - #2533

Merged
colbymchenry merged 1 commit into
mainfrom
claude/cpp-member-receivers
Oct 13, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/cpp-member-receivers

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

C++ receiver inference reads back through the call's file, then the header of the same name (foo.cc → foo.h). A data member of the calling method's class declared anywhere else was never read, and calls on it were guessed by the receiver's name.

facebook/rocksdb declares DBImpl in db_impl.h, while its methods live in db_impl_write.cc, db_impl_compaction_flush.cc, db_impl_open.cc and more. On main before #2532 (c19f0284), 684 of the 686 calls on x_ receivers in those files were name guesses at 0.65/0.7 (versions_->GetColumnFamilySet() at 0.7), and 385 more were unresolved.

Follows #2532 (merged as 79cd737b), which this was built and measured on.

What changed (name-matcher.ts)

  • Member lookup at the caller's boundary. Where the backward scan leaves the calling function, the name is looked up as a data member of the function's class before anything outside it, as C++ does: block scopes, then the class and its bases, then namespaces. This is cppCallersMember:

    • the class's own declaration lines, wherever the class is declared (cppDataMemberIn, memoized per declaration and name);
    • of several classes sharing a qualified name, the declaration in the caller's own translation unit (fix(cpp): a call a method makes with no receiver links to its class's own or inherited method #2479's cppScopeDeclarations);
    • then the classes it derives from, through their base edges, skipping a base that depends on the template's parameters (a bare name isn't looked up there);
    • a gtest TEST_F(Fixture, …) / TEST_P body is a method of a class derived from its fixture, so db_ there is the fixture's member (cppTestFixture).

    An earlier function's local of the same name, which the old scan reached first, no longer wins over the member.

  • Same record, same rules. The member goes into the declaration record as the caller's own, with its class as the lookup site (declared). So fix(cpp): a call on a declared pointer, value or reference reaches the class its type names, not a namesake #2493's lookup, the library-type gate, fix(cpp): a -> call on a smart pointer or optional reaches the type it holds #2440's held-type path (std::unique_ptr<VersionSet> versions_) and fix(cpp): a call on an auto variable or a chained call reaches the class C++ finds, not a namesake #2532's chains all apply.

  • What stays as before:

    • a member of a template parameter's type (TComparator cmp_);
    • a member of an alias of one (using Shard = typename ClockCache::Shard;, or protobuf's using MapFieldBase = std::conditional_t<kIsMutable, …>, which made isCppTemplateDependentType count std traits too);
    • a name the scan saw an unreadable declaration of in the function (auto mutex_ = Make();).
  • Reading members correctly:

    • Only declarations outside parentheses count, and not a name an argument list follows. Method declarations have no nodes, so their parameters are in the class's lines: rocksdb's DB::CreateColumnFamilies(…, std::vector<…>* column_families) read as a member of DBImpl.
    • Block comments are followed across lines. cppCodeOf takes a code line that starts with a dereference (*first_key, scratch);) for a comment, which left the paren depth open for the rest of the class.
    • String literals are blanked.
  • cppExpressionType's own member fallback (from fix(cpp): a range-based for loop over auto elements has the element type of its range #2492, for loop ranges) is now redundant and was removed.

Validation

Site-keyed edge diffs (diff-sites.mjs) against #2532's tip (978709dc; main's 79cd737b has the same tree), both arms built the same way, kernel included:

repo removed added retargeted metadata-only
google/leveldb 7 40 51 48
fmtlib/fmt 10 37 36 46
facebook/rocksdb 695 2,454 7,303 5,519
protocolbuffers/protobuf 266 162 405 753

Against c19f0284, main before #2532 (both PRs together): leveldb −7 / +75 / 51 / 48, fmt −13 / +143 / 47 / 81, rocksdb −897 / +5,510 / 8,060 / 6,035, protobuf −435 / +637 / 423 / 824. Nodes and files are identical.

Removed, every group read:

  • rocksdb (695): calls on members of library types whose name guesses reached project classes:

    • result_map_.find (std::unordered_map) → toku::omt::find (104);
    • saved_value_.capacity (std::string) → autovector (102);
    • cf_name_.c_str → gtest's FilePath (78);
    • compaction_queue_.empty (std::deque), std::set / std::optional / std::thread / std::mutex / std::counting_semaphore / std::array members, smart pointers' own get / reset.

    Plus edits_.size() (10). Main's 0.9 there came from another class's autovector member above the method, while this class's own std::vector<VersionEdit> edits_ is declared below it.

  • protobuf (266):

  • fmt (10) and leveldb (7): std::vector / std::string members, in fmt's vendored gtest and in leveldb's DBIter.

Retargeted, every group read, all wrong→right:

  • rocksdb:
    • 4,309 gtest bodies' db_->… (ModelDB at 0.65 → DB at 0.9);
    • 251 + 124 + 104 iterator members (→ IteratorBase, InternalIteratorBase, CompactionIterator), and 80 MemTableRep::Iterator → IteratorBase (auto it = db_->NewIterator(…) in gtest bodies, the original namesake);
    • 264 write_batch_ → WriteBatchWithIndex;
    • 368 TransactionDB / StackableDB, 299 env_ → SpecialEnv's EnvWrapper;
    • the wrapper classes in env.h / file_system.h, each with its own target_ (0.9→0.9: the old scan took the nearest target_ above, another wrapper's);
    • ShortenedIndexBuilder, CompactionProxy, ThreadStatusUpdater and so on.
  • protobuf: 340 Java generators' context_->… → compiler::java::Context (137 of them a 0.9 namesake hpb_generator::Context); TextFormat::FieldValuePrinter, GzipOutputStream::sub_stream_, ParserImpl::error_collector_.
  • leveldb: IteratorWrapper::iter_ → Iterator, MemTableIterator::iter_ → SkipList::Iterator, db_ → DB.

Added: calls on typed members, and chains through them (db_->CompactRange(…).PermitUncheckedError()).

Metadata-only: the same target, now at 0.9.

The A/B rounds also caught three mistakes before this version: parameters read as members, the comment guess, and an alias check that stopped at a mid-name alias (MemTable::Table::Iterator). Each now has a test.

Tests: __tests__/cpp-member-receiver.test.ts, 10 cases:

  • 4 fail on fix(cpp): a call on an auto variable or a chained call reaches the class C++ finds, not a namesake #2532's code: the rocksdb reproduction; a member below an inline method plus one a base declares; a gtest fixture member; the class's own member, not a nested class's.
  • 6 guard behavior that must not change: the caller's translation unit's class; a dependent base; an unreadable local; a method's parameter or a method; a member of a template parameter's type; an alias of one.
  • 14 mutations, one per guard or branch, each fail a test.
  • Full suite: 6,973 passed, 0 failed, 80 skipped.

CPU (rocksdb, full index, process CPU time, interleaved; main c19f0284 vs this branch on top of #2532): main 151.3 / 154.6 / 153.1 s vs 147.1 / 141.0 / 140.6 s. The member lookup is memoized per caller and name, and per class declaration; no measurable cost.

Not in this PR

  • A local declared T* const name = … isn't read by the declarator pattern. That's older than this change: rocksdb's Iterator* const iter = iter_guard.get();.
  • Bare calls in a gtest body to the fixture's methods (auto iter = NewIterator(…)) still go through the name strategies. Most of the 111 remaining rocksdb MemTableRep::Iterator edges come from these; the other 42 are memtable.cc's own, and right. There were 342 before fix(cpp): a call on an auto variable or a chained call reaches the class C++ finds, not a namesake #2532 and 181 after it.

🤖 Generated with Claude Code

…ype's method, wherever the class is declared

Receiver inference read back through the call's file and the header of the
same name only, so a member of a class declared elsewhere — rocksdb's
DBImpl is declared in db_impl.h, its methods in db_impl_write.cc,
db_impl_compaction_flush.cc, db_impl_open.cc and more — was never read, and
calls on it (`versions_->GetColumnFamilySet()`, `mutex_.Lock()`) were
guessed by the receiver's name.

- Where the scan leaves the calling function, the name is looked up as a
  data member of the function's class before anything outside it
  (cppCallersMember): the class's own declaration lines wherever it is
  declared (the declaration in the caller's own translation unit, of
  classes sharing a name), then the classes it derives from through their
  base edges, skipping a base that depends on the template's parameters.
  A gtest `TEST_F` / `TEST_P` body is a method of a class derived from its
  fixture, so `db_` there is the fixture's.
- The member goes into the declaration record as the caller's own, with
  its class as the lookup site, so #2493's lookup, the library-type gate and
  #2440's held-type path apply. A member of a template parameter's type,
  or of an alias of one, and a name the scan saw an unreadable declaration
  of, stay as before. (isCppTemplateDependentType now also counts a std
  trait of a parameter: protobuf's `using MapFieldBase =
  std::conditional_t<kIsMutable, …>`.)
- cppDataMemberIn reads only declarations outside parentheses and not
  followed by an argument list (method declarations have no nodes, so their
  parameters are in the class's lines), skips nested functions and classes,
  follows block comments across lines, and blanks string literals.
- A member declared below an inline method that uses it, and a member of
  the class itself rather than of a class nested in it, are found the same
  way; cppExpressionType's own member fallback became redundant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit df3ce97 into main Oct 13, 2026
@colbymchenry
colbymchenry deleted the claude/cpp-member-receivers branch October 13, 2026 00:46
colbymchenry added a commit that referenced this pull request Oct 13, 2026
…e gives its type to calls on it (#2536)

Receiver inference reads a C++ declaration as a type followed by the
name. What may stand between them was read as the type: in rocksdb's
`Iterator* const iter = iter_guard.get();` the type was `const`, which
names nothing, so the scan went on to an earlier function's `iter`, and
in protobuf's `io::EpsCopyOutputStream* PROTOBUF_NONNULL stream` the
type was a class named `PROTOBUF_NONNULL`. A name an annotation follows
(leveldb's `MemTable* imm_ GUARDED_BY(mutex_);`) was not read at all.

- The declarator regex (and the range-for type tail) reads past a cv-
  qualifier or `__restrict` before or after a `*` or `&` (`T* const`,
  `T const&`), the nullability annotations after one (abseil's and
  clang's, and the ALL-CAPS macros c-cpp.ts already blanks for the
  parser), and an ALL-CAPS annotation macro's call after the name.
- matchCppDeclarator hands the readers the type without those
  annotations (cppDeclaratorType); cv-qualifiers stay, since what reads
  a type reads past them. An ALL-CAPS word counts as an annotation only
  after a `*` or `&`, so a type's own name stays (`OS_HANDLE* const h`),
  and template arguments are kept as written.
- The call's own file, its same-named header, a class's members and a
  range-for's variable are read that way; the local-name check for bare
  calls reads past the qualifiers too.
- #2533's test that a method's parameter is no member used `T* const`
  locals, which the scan couldn't read; it now declares no local, since
  one the scan reads decides first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry added a commit that referenced this pull request Oct 13, 2026
…xture's method (#2537)

The index holds a gtest `TEST_F(Fixture, Name)` / `TEST_P` body as a
function named `TEST_F`; gtest makes it a method of a class derived from
`Fixture`. #2533 gave such a body the fixture's data members
(cppTestFixture), not its methods. rocksdb's tests call `DBTestBase`'s
`Put(…)`, `Flush()`, `Key(i)` and `NewIterator(…)` bare, and those calls
were guessed by name, often reaching a namesake in another class at 0.4
(`ColumnFamilyTestBase::Put`); `auto iter = NewIterator(…)` gave `iter`
no type, so `iter->SeekToFirst()` reached the nested
`MemTableRep::Iterator`.

- cppImplicitThisMethod is #2479's lookup, taken out of
  matchCppImplicitThisCall: the caller's class, its bases, the classes
  around it, with cppTestFixture as the owner of a gtest body.
  matchCppImplicitThisCall keeps the local-shadowing check for the link.
- cppExpressionType types a bare call by the same lookup, as on `this`,
  instead of the class's first declaration. Of two classes sharing a name
  in different test files it reads the caller's own, and in a template a
  dependent base a using-declaration names still counts, as before
  (protobuf's `using RepeatedFieldWrapper<T>::MutableRepeatedField;`).
  `this` in a gtest body points to the fixture.
- A `->` call on a value of a type from outside the project no longer
  takes the type's last name for the class it is in
  (isCppArrowThroughOutsideType); the name strategies decide.
  matchCppDeclaredTypeCall steps aside for `->` on a value before #2532's
  outside-type check, so typing JsonTest's `auto m = ToProto<Any>(…)` (an
  `absl::StatusOr<Any>`) had sent `m->value()` to protobuf's own
  `hpb::StatusOr::value` at 0.9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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