Skip to content

fix(cpp): a call on a declared pointer, value or reference reaches the class its type names, not a namesake - #2493

Merged
colbymchenry merged 1 commit into
mainfrom
claude/intelligent-chebyshev-73b6fe
Oct 11, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/intelligent-chebyshev-73b6fe

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

A C++ call on a variable, parameter or member the calling function or its class declares now resolves on the class C++ name lookup finds for the declared type, not on any class sharing the type's last name.

Iterator* iter; iter->Valid() became resolveMethodOnType('Iterator', 'Valid'), which takes any class named Iterator that declares Valid. facebook/rocksdb has 22 classes named Iterator, and the one the code means (ROCKSDB_NAMESPACE::Iterator) inherits Valid/Next/key from IteratorBase, so nested namesakes like MemTableRep::Iterator took the calls. google/leveldb's Iterator* calls went to SkipList::Iterator the same way.

This routes the raw-pointer / value / reference typed path through the lookup #2440 added for smart-pointer held types (cppClassWritten + cppMethodOf).

What changed

matchCppDeclaredTypeCall (name-matcher.ts) runs right after C++ receiver inference, before the last-name typed match:

cppClassWritten (cpp-supertypes.ts) now tries the file's usings before the global scope. protobuf's upb generator writes using google::protobuf::Descriptor; inside namespace upb::generator, and the PHP extension declares a global C struct Descriptor in a header; the lookup took the C struct, which has no methods. C++ finds the using's name at the namespace's level first, and a TU can't see both.

Validation

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

repo retargeted added removed metadata-only
google/leveldb 261 2 0 0
fmtlib/fmt 0 0 0 0 (dump byte-identical)
redis (C control) 0 0 0 0 (dump byte-identical)
facebook/rocksdb 1,096 46 24 0
protocolbuffers/protobuf 439 2 2,300 0

No extends/implements edge moved in any repo, and no #2440 held-type site moved.

Retargeted / added, grouped by (old owner → new owner), each pattern checked at the source:

  • rocksdb: MemTableRep::Iterator → IteratorBase 1,081; gtest's CartesianProductGenerator2::Iterator → each generator's own Iterator 8; TraceRecord::Handler → the inherited nested TraceRecordResult::Handler 4; a class's own nested Statistics / HandleImpl 3; added: an anonymous-namespace State reaching its BitFields base's Get 46.
  • protobuf: hpb_generator::Context → compiler::java::Context 186 / compiler::rust::Context 180 / upb::generator::reflection::Context 16; compiler::Parser → TextFormat::Parser 7; hpb::SourceLocation → io::Printer::SourceLocation 5 (through using SourceLocation = io::Printer::SourceLocation;); objectivec's own FieldGeneratorMap / FileGenerator, io::ErrorCollector, upb::Arena, TextFormat::Printer, RepeatedFieldProxyBase, the unittest's own SimpleErrorCollector.
  • leveldb: SkipList::Iterator → leveldb::Iterator 261; added WriteBatch::Iterate's handler->Put/Delete → WriteBatch::Handler 2.
  • An independent checker (plain scope walk + extends edges) agreed on all but 24 sites it can't model (global-scope files using ROCKSDB_NAMESPACE, inherited nested classes, aliases); those were read by hand.

Removed (class found, no such method):

  • protobuf 2,300: 2,272 on the descriptor classes (FieldDescriptor, Descriptor, FileDescriptor, …), whose accessors the index doesn't hold (declared behind PROTOBUF_FUTURE_ADD_EARLY_NODISCARD), so field->full_name() was guessed onto upb::FieldDefPtr, generated Field, Type, … at 0.65–0.8. The rest: Symbol (macro-declared accessors), upb C structs, and JavaGenerator/python::Generator plugin calls that went to another generator's setter. Every removed target's owner is unrelated to the receiver's class and its bases.
  • rocksdb 24: MultiScanException& ex; ex.what() → the Java bindings' KVException::what (23; what is std::runtime_error's), and m_owners->erase() on a std::set-based TxnidVector → PreparedHeap::erase.

Decisions measured along the way

  • Unique-name fallback → as before. When the class was found only as the one class of that name the file can see, it either agreed with main (82 sites, confidence churn only) or was a nested namesake standing in for a class the index doesn't name (rocksdb's out-of-line InlineSkipList<C>::Node is indexed <anonymous>, so Node matched FlushScheduler::Node; protobuf's generated FieldOptions matched TailCallTableInfo::FieldOptions). Main's guesses there were right 4 of 6 times in protobuf.
  • Template parameters → as before. Main's last-name match is often right by luck (template <typename BinaryHeap> instantiated with the class BinaryHeap).
  • A base the index doesn't link → as before. protobuf's class FlatAllocator : public decltype(ApplyTypeList<FlatAllocatorImpl>(…)) (95 right FlatAllocatorImpl::PlanArray edges kept) and rocksdb's WritePreparedTxnDB : public WritePreparedTxnDBBase, an alias each #if USE_COROUTINES arm declares differently (13 sites, 11 of main's guesses wrong, 2 right; kept as before).
  • using before the global scope recovered the one right edge the first version lost (message->FindFieldByNumber in the upb generator).

Tests

__tests__/cpp-declared-type-receiver.test.ts (8 cases):

  • 3 fail on main (the rocksdb reproduction; a reference parameter, a value with a base method and a header member; no guess for a class that only inherits from std::runtime_error), all pass here;
  • 5 boundary guards pass on both: a nested class found first from inside a class that inherits it; using before a global C struct (fails if the reorder is reverted — mutation-checked); a template parameter and a class another source file defines keep main's edges; a decltype(…) base keeps main's edge (fails if the base check is removed — mutation-checked); a declaration read from an earlier function stays as before.

All 309 tests in the C++ resolution suites pass. Full suite: 6,922 passed, 0 failed, 82 skipped (551 files).

CPU (process CPU time of a full index, interleaved runs, cpu-bench.sh): rocksdb main 144.1 / 141.7 / 143.3 s vs this branch 139.7 / 145.9 / 146.8 s; protobuf main 350.0 / 324.4 s vs 341.7 / 300.0 s. No measurable cost.

Follow-ups (not in this PR)

  • fix(cpp): a -> call on a smart pointer or optional reaches the type it holds #2440's held-type path returns no edge for a held class lacking the method without the base-completeness check added here; worth applying the same cppBasesKnown there.
  • Receiver inference reads declarations inside raw-string code templates (protobuf's upb generator: upb_MiniTableField field = $field_init$; inside R"cc(…)cc"), 8 sites here.
  • A qualified outside type (absl::Status s; s.ok() → upb::Status::ok) still falls through to the last-name match; that verdict was split off to a separate session's work, by agreement.

🤖 Generated with Claude Code

…e class its type names, not a namesake

Receiver typing resolved a C++ call on a variable, parameter or member by
the declared type's last name: `Iterator* iter; iter->Valid()` became
resolveMethodOnType('Iterator', 'Valid'), which takes any class named
`Iterator` that declares `Valid`. facebook/rocksdb has 22 such classes,
and the `Iterator` the code means inherits `Valid`, `Next` and `key` from
`IteratorBase`, so nested namesakes like `MemTableRep::Iterator` took the
calls (google/leveldb's went to `SkipList::Iterator`).

A declaration the calling function or its class writes, called with the
operator it takes (`->` on a pointer, `.` on a value or reference), now
goes through the same C++ lookup #2440 uses for smart pointers'
held types (cppClassWritten from the caller's scopes, then the file's
`using`s, aliases followed) and cppMethodOf (the class's own method, else
one through its own base edges):

- An alias #2399 followed from the caller (function-local typedefs
  included) is the class it names, by qualified name. An `auto` loop's
  element type (#2492) is looked up where its range's type is written.
- A class found only as the one class of that name the file can see, a
  template parameter's type, a library type, or a type nothing visible
  declares: the call goes on as before.
- A class with no such method gets no edge, unless it is a class template
  or a base on the way is one the index doesn't link (a `decltype(…)`
  base, an alias each `#if` arm declares differently), which may declare
  it: then as before.
- Declarations read from elsewhere, or past one the scan couldn't read,
  are unchanged.

cppClassWritten now tries the file's `using`s before the global scope:
protobuf's upb generator writes `using google::protobuf::Descriptor;`
inside its namespace, and the PHP extension declares a global C struct
`Descriptor` in a header.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit fc0da38 into main Oct 11, 2026
@colbymchenry
colbymchenry deleted the claude/intelligent-chebyshev-73b6fe branch October 11, 2026 16:07
colbymchenry added a commit that referenced this pull request Oct 12, 2026
…only when its bases are known (#2530)

matchCppHeldTypeCall (#2440) returned null (no edge) for a `->` call on a
`std::unique_ptr`/`std::shared_ptr`/`std::optional` whose held class
lacks the method, unless that class is a class template. #2493 found the
gap in the same rule for raw pointers and values: a base the index can't
link may declare the method. protobuf's `class FlatAllocator : public
decltype(ApplyTypeList<FlatAllocatorImpl>(…))` has no extends edge to
`FlatAllocatorImpl`, and rocksdb's `WritePreparedTxnDB` derives from an
alias each `#if` arm declares differently.

Both paths now share one verdict, cppLacksMethod: no class template, and
cppBasesKnown (every base on the way is linked to a class, or is a library
type). Otherwise the call goes on as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry added a commit that referenced this pull request Oct 13, 2026
…class C++ finds, not a namesake (#2532)

#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 added a commit that referenced this pull request Oct 13, 2026
…ype's method, wherever the class is declared (#2533)

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 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>
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