Skip to content

fix(cpp): a call on an auto variable or a chained call reaches the class C++ finds, not a namesake - #2532

Merged
colbymchenry merged 1 commit into
mainfrom
claude/cpp-typed-receiver-lookup
Oct 13, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/cpp-typed-receiver-lookup

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

#2493 made a call on a declared pointer, value or reference resolve on the class C++ name lookup finds for its type. Three kinds of typed receivers still went by the type's last name, so a namesake class could win:

  1. auto locals. The type came from inferCppAutoInitializerType, which reads only new X, X(…), Class::fn(…) and make_unique<X>, and keeps only the last name. When it couldn't read the initializer, the scan went on to an earlier declaration of the same name, often in another function. In facebook/rocksdb, auto iter = std::unique_ptr<Iterator>(…) and auto it = db_->NewIterator(…) read an earlier test's Iterator* iter, and iter->SeekToFirst() reached the nested MemTableRep::Iterator (342 edges on main).
  2. Chained calls (a->b()->c()). The inner call's return type was its last name: protobuf's Java generator reached the C# generator's EnumGenerator, and a chain returning absl::Status reached upb::Status.
  3. Types from outside the project. These got a project namesake's method: protobuf's absl::Status status; status.ok() reached upb::Status::ok (the fix(cpp): a call on a declared pointer, value or reference reaches the class its type names, not a namesake #2493 follow-up), and fmt's std::ostream& os; os.flush() reached fmt's own ostream::flush.

What changed (name-matcher.ts)

  • auto locals (cppAutoLocalType) take the type of their initializer as written where it is declared (cppExpressionType), and calls on them go through matchCppDeclaredTypeCall from that site. cppExpressionType now also reads:

    • casts and smart pointers written out: static_cast<T*>(…), std::unique_ptr<T>(…);
    • new T(…), std::make_unique<T> / make_shared<T>, and a construction T(…) of a class C++ lookup finds;
    • a bare call to the caller's class's own or inherited method (auto* h = micro_rep();).

    The initializer is read up to its ;, across lines: auto* op = opts->merge_operator continues on the next line as ->CheckedCast<Foo>(). A type that depends on a template parameter (T*, or a library template of one like std::shared_ptr<T>) is left to the old path; a smart pointer to one of the project's class templates (std::unique_ptr<ExternalTableReaderBase<Mode>>) is not. Without the template arguments substituted, overloads like protobuf's DownCastMessage<T> (const T& / T* / std::shared_ptr<T>) can't be told apart.

  • Chains (matchCppChainOnWrittenType, tried before the last-name path): c is looked up on the class C++ finds for b's written return type, through a pointer or smart pointer. The inner receiver's operator is read from the source, since the extractor spells -> as .. When the type can't be read or placed, the last-name path decides as before.

  • Outside types get no project method:

  • Small fixes found by the A/B:

    • A template <…> header followed by macros or specifiers still names its parameters (collectTemplateHeader).
    • A trailing annotation macro (T* PROTOBUF_NONNULL) is no part of a written return type.
    • Memoized auto locals and loop elements are worked out with a fresh depth budget. A deep chain that read a local first left it untyped for that local's other calls (cfd->current() in rocksdb's compaction test).
    • The record's element field is now declared, since it also holds an auto local's type.

Validation

Site-keyed edge diffs (diff-sites.mjs), main c19f0284 vs this branch, both built from the same tree (kernel included):

repo removed added retargeted metadata-only
google/leveldb 0 35 0 0
fmtlib/fmt 3 106 11 35
facebook/rocksdb 202 3,056 757 518
protocolbuffers/protobuf 169 475 18 71

Nodes and files are identical between the arms.

Removed, every site triaged:

  • rocksdb (202):
    • 192 were name guesses on receivers of library types, now known: std::vector / std::string / std::optional / std::unique_ptr::get, e.g. blob_files.empty() on BlobFiles reached CompactionInputFiles::empty.
    • 9 are the same edge, kept under the chain's ref at the same position (With(e1).With(e2), mock->Inner()->Inner()), checked mechanically.
    • 1 is a loss: auto* state = new IteratorState(…) where WriteUnpreparedTxnDB::IteratorState is defined out of line and indexed <anonymous>, so its typed receiver gets no method. A declared IteratorState* state behaves the same way on main, and the name guess happened to be right.
  • protobuf (169):
  • fmt (3): os.flush() on std::ostream&, and 2 same-edge-kept sites.

Retargeted, every group read, all wrong→right:

  • rocksdb:
    • 361 ArenaWrappedDBIter → IteratorBase and 160 MemTableRep::Iterator → IteratorBase (auto iter = std::unique_ptr<Iterator>(…));
    • 51 SharedBlobFileMetaData → BlobFileMetaData;
    • port::Mutex → InstrumentedMutex (static_cast<InstrumentedMutex*>(arg)), ProtectionInfoKVOC → ProtectionInfoKVOS (a class template's return type), TableReader, Transaction, FileSystem, Env and so on.
  • protobuf: _GetDescriptor(self)->index() in each pyext namespace now reaches that namespace's descriptor class, rather than Descriptor's index; micro_rep() → MicroRep, not LargeRep.

Added:

  • 3,004 are chains that resolve through the written return type (rocksdb 2,512, protobuf 411, fmt 46, leveldb 35): rocksdb it->key().data() → Slice (the namesake's key() returned const char*), target()->…, dbfull()->…; protobuf GetDescriptor(self)->FindFieldByName(…).
  • The other 668 are calls on typed auto locals.
  • Every added or retargeted site whose target class shares its name with another class was listed. All come from exact lookups, and the examples read right.

Tests: __tests__/cpp-auto-local-receiver.test.ts, 12 cases:

  • 9 fail on main; the other 3 guard behavior that must not change:
    • an initializer split over lines;
    • template-dependent returns after macros;
    • a project namespace's type the file doesn't see.
  • 17 mutations, one per guard or branch, each fail a test.
  • All 306 tests in the 25 C/C++ test files pass. Full suite: 6,963 passed, 0 failed, 80 skipped.

CPU (rocksdb, full index, process CPU time, interleaved): main 132.1 / 142.1 / 146.9 s vs this branch 130.6 / 148.7 / 130.1 s. No measurable cost.

Not in this PR

  • gtest TEST_F / TEST_P bodies don't have the fixture's members: auto iter = db_->NewIterator(…) there stays untyped. The follow-up PR (data members declared in another header) adds fixture members.
  • Bare calls in those bodies (auto iter = NewIterator(…)) would need the fixture's methods: about 50 rocksdb MemTableRep::Iterator edges remain because of them.

🤖 Generated with Claude Code

…class C++ finds, not a namesake

#2493 resolved calls on declared pointers, values and references by C++
lookup. Calls whose receiver type came from an initializer or a return
type still went by the type's last name:

- An `auto` local now has the type its initializer has, as written where
  that is declared: a cast or smart pointer written out
  (`std::unique_ptr<Iterator>(…)`, `static_cast<InstrumentedMutex*>(arg)`),
  `new` / `std::make_unique` / a construction of a class, or what a
  function or method is declared to return (`txn->GetIterator(…)`), with
  the initializer read to its `;` across lines. Its calls then go through
  matchCppDeclaredTypeCall from that site. A type that depends on a
  template parameter (`T*`, `std::shared_ptr<T>`) is left to the old path.
- A chained call `a->b()->c()` looks `c` up on the class C++ finds for the
  written return type of `b`, through a smart pointer too
  (matchCppChainOnWrittenType), before the last-name path.
- A type from outside the project gets no project method: a std/abseil
  library type (#2413's gate, now applied before the last-name match, so a
  namesake class like fmt's own `ostream` no longer wins), and a
  capitalized type under a namespace the project declares no type in
  (`absl::Status`, which reached `upb::Status::ok`).
- A bare call in a method types an `auto` local from the class's own or
  inherited method; a `template <…>` header followed by macros or
  specifiers still names its parameters; a trailing annotation macro
  (`T* PROTOBUF_NONNULL`) is no part of a return type; and memoized
  `auto` types are worked out with a fresh depth budget, so a deep chain
  that reads a local first no longer leaves it untyped for its other calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit 79cd737 into main Oct 13, 2026
@colbymchenry
colbymchenry deleted the claude/cpp-typed-receiver-lookup branch October 13, 2026 00:42
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>
colbymchenry added a commit that referenced this pull request Oct 15, 2026
…hares its last name, and rocksdb::DB is RocksDB's own (#2567)

A C++ receiver declared with a namespace-qualified type is from outside
the project only when no class of the project can be what that name names.
Three cases read that wrong on main:

- A namespace a macro stands for. RocksDB declares its classes in
  `namespace ROCKSDB_NAMESPACE` under `#define ROCKSDB_NAMESPACE rocksdb`,
  so the index names them `ROCKSDB_NAMESPACE::DB`, and #2532's outside rule
  (cppProjectScopeNames) never saw `rocksdb`: `rocksdb::DB* db; db->Put(…)`
  got no edge. cppProjectScopeNames now adds the name a single-name macro
  stands for, in both directions (cppNameMacros, the map
  cppNamespaceMacros already builds).
- A lowercase outside type beside a project namesake. isCppLibraryType
  (#2413) took a non-std lowercase type as the library's only when no
  project type had its name at all, so `absl::flat_hash_set<int> seen;
  seen.insert(…)` reached a project's own `util::flat_hash_set::insert`.
  It is now also the library's when every project type of that name is a
  namesake (cppFindsOnlyNamesakes).
- A capitalized type whose last name finds only namesakes, on the paths
  #2493's call-site rules leave to the last-name match: a declaration read
  outside the calling function (protobuf's `absl::Status _status` in its
  RETURN_IF_ERROR macro body reached `upb::Status::ok`; `auto bytes =
  ::hpb::Serialize(…); bytes.value()` reached `hpb::StatusOr::value`), and
  a namespace the project uses (its generated
  `DescriptorProto::ExtensionRange`, which the index doesn't hold, reached
  `Descriptor::ExtensionRange::options`). That call now gets no edge, as
  isUndeclaredTypeName rules where no class shares the name; `->` on a
  value still goes to the name strategies, and a template parameter's
  member is left alone.

cppFindsOnlyNamesakes: every type of the last name, and every owner of a
method of the called name (the index can hold a class's methods without
the class), has a qualified name that ends unlike the written one, neither
the other's tail; namespace aliases are expanded and macro-named
namespaces read both ways. A name a project type qualifies, an unqualified
name and an unbalanced `<>` are never judged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry added a commit that referenced this pull request Oct 15, 2026
… global type (#2568)

protobuf's hpb declares `class Arena { … backend::Arena arena_; }`, where
`namespace backend = hpb::internal::backend::upb;` and that namespace says
`using Arena = ::upb::Arena;`. cppTypeName dropped the leading `::`, so the
alias's target was looked up as `upb::Arena` from the alias's own scope,
whose nearest `upb` is that very namespace: the alias named itself, the
lookup ended on no class, and `arena_.Fuse(other.arena_)` fell back to the
type's last name, linking `hpb::Arena::Fuse` (and `IsFused`, `RefArena`) to
itself instead of `upb::Arena::Fuse`.

TypeName now records a leading `::` (`global`), and a global name is looked
up from the global scope alone: when an alias's target is followed (follow,
and the scope expand looks the target up from and hands on), in a class's
base clause (baseClassesOf), and in cppAliasedTypeWritten, which #2532's
`auto` locals and chains walk alias by alias.

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