Skip to content

fix(cpp): a range-based for loop over auto elements has the element type of its range - #2492

Merged
colbymchenry merged 1 commit into
mainfrom
claude/dazzling-bouman-557717
Oct 11, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/dazzling-bouman-557717

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

A C++ call on the variable of a range-based for loop over auto elements now has the element type of the range the loop iterates, read from the range where that can be done reliably.

#2439 typed a loop variable when the loop names its type (for (ConformanceTestSuite *suite : suites)). A loop over auto elements names none, so receiver inference kept scanning as for an auto local with an unreadable initializer: it reached an earlier declaration of the same name (often a different variable, sometimes in another function) or fell through to Strategy 3's guess by the receiver's name, with #2413's shadowed mark set. facebook/rocksdb's db/db_impl/db_impl_write.cc loops for (auto cfd : cfds) over a const autovector<ColumnFamilyData*>& parameter.

What the range is read from

cppAutoLoopElement reads the range expression from the loop header (cppRangeExpression), works out its written type (cppExpressionType), then its element (cppRangeElementOf). Types are kept as written, template arguments and all, with the scope, file and line they are written at, because the index's returnType keeps only the last name (FieldNumberOrder returns vector).

  • A name: the receiver scan itself, from the loop's line. It must be the calling function's own declaration or a member of its class (isCppCallersDeclaration), not shadowed. A name the method doesn't declare is read as a data member of its class from the class's own lines (versions_ in db_impl_write.cc, declared in db_impl.h, which no same-stem header scan reaches). An auto local's initializer is read in turn (const auto& blob_files = vstorage->GetBlobFiles();).
  • *x, x.m, x->m, this: a pointer's pointee or a standard holder's held type; a data member read from the class's lines outside its nested functions and classes (C++ data members are not indexed).
  • A call: f(…), ns::f(…), Class::f(…) looked up from the caller's scopes outwards; x.f(…) / x->f(…) on the object's class, own method or inherited (fix(cpp): a -> call on a smart pointer or optional reaches the type it holds #2440's cppMethodOf). The return type is read from the declaration's text.
  • Element: the first template argument of a standard or abseil sequence container (vector, set, deque, Span, InlinedVector, flat_hash_set, …) or of protobuf's RepeatedPtrField/RepeatedField and rocksdb's autovector; for any other project class, what its begin() returns, dereferenced (a pointer, a container's ::iterator, or an iterator class's operator*). That is how for (auto cfd : *versions_->GetColumnFamilySet()) reaches ColumnFamilyData*.
  • Aliases are followed with their template arguments through a new cppAliasedTypeWritten (cpp-type-aliases.ts), from where the alias is declared: rocksdb's const BlobFiles& is std::vector<std::shared_ptr<BlobFileMetaData>>.
  • Classes are looked up with fix(cpp): a -> call on a smart pointer or optional reaches the type it holds #2440's cppClassWritten. Clang/abseil nullability annotations (const FieldDescriptor* const absl_nonnull) are dropped from element types.

Not read (the scan goes on as before): a map's elements (a pair, whose first/second are recorded as bare calls), structured bindings (13 loops in protobuf, 43 in rocksdb), an element a template argument decides, an alias each #if arm declares differently (rocksdb's port::Thread), subscripts (files_[level]), and members a header declares below the inline method that loops over them.

The element then goes through the same declaration record as any declared receiver (noteCppDeclaration, cppDeclaredType), so the existing rules apply unchanged: the typed path, #2413's gate for a standard-library element (std::string, std::thread), and #2440's held-type lookup for -> on a std::unique_ptr/std::shared_ptr/std::optional element.

Smart-pointer elements: folded in through #2440

-> on a smart-pointer element is the gap #2413 left and #2440 closed for declared receivers. With #2440 merged, a smart-pointer element is recorded as its holder text, so #2440's matchCppHeldTypeCall resolves -> on it with the C++ lookup, and . on it (p.reset()) is the holder's own member, which #2413's gate silences. An earlier version of this change left -> on smart-pointer elements alone while #2440 was open: without the held-type lookup, typing the holder lost lucky-right edges in rocksdb's range_del_aggregator.cc (iter->Valid() over std::vector<std::unique_ptr<TruncatedRangeDelIterator>>).

Overlap with #2440

The held-type reading moved into cppStdHeldType, next to #2440's CPP_STD_HOLDERS, and matchCppHeldTypeCall now calls it (same behavior). The class lookup, the class-kind set and the base-class method walk are #2440's (cppClassWritten, CPP_CLASS_KINDS, cppMethodOf); cppMemberOwner from #2479 gives the calling method's class.

Validation

Setup. Base is main 8a5ba3d0 (with #2439, #2440, #2479); the other arm is this branch. Both arms ran codegraph init -y with one native kernel built from main's source, on this Windows box. Edges are diffed per call site (kind, source, line, col, refName). A probe logged every auto loop the scan met with its range and the element read.

repo removed added retargeted confidence only
google/leveldb 0 0 0 0 (dumps byte-identical)
fmtlib/fmt 0 1 3 0
facebook/rocksdb 68 173 104 231
protocolbuffers/protobuf 13 25 7 59

Loops read: rocksdb 528 of the 649 auto loops the scan met, protobuf 158 of 255.

Removed (every one was a wrong guess):

  • rocksdb, 67 calls on a standard-library element: std::string .c_str() (34, all to googletest's testing::internal::FilePath::c_str), .find() (14, to toku::omt::find), .size() (9), std::thread .join()/.joinable() (9, to port::WindowsThread), std::vector<size_t> .empty() (1). One table.reset() on a std::unique_ptr<BlockBasedTable> element, which was TableIterator::reset.
  • protobuf: std::stringstream::str and std::vector::size (1 each), and 11 calls on elements of the generated FieldDescriptorProto, OneofDescriptorProto and DescriptorProto. The index has no class node for these (only their .pb.cc constructors), so the existing rule for a type the project doesn't declare drops the guess. All 11 were wrong: upb::OneofDefPtr::name, google::protobuf::Field::name, a C# generator's has_default_value, and FieldDescriptor::type / json_name at 0.9 through an earlier const FieldDescriptor* field.

Retargeted (every one wrong → right):

  • rocksdb, 68 smart-pointer elements through fix(cpp): a -> call on a smart pointer or optional reaches the type it holds #2440: listener->OnFlushCompleted(…) over std::shared_ptr<EventListener> went from the stress test's DbStressListener to EventListener; std::shared_ptr<BlobFileMetaData> elements from SharedBlobFileMetaData; std::unique_ptr<Iterator> elements from MemTableRep::Iterator / ArenaWrappedDBIter to IteratorBase; job->Run() from CompactionJob::Run to FlushJob::Run.
  • rocksdb, 36 others: column.name() from LazyWideColumn::name to WideColumn::name; blob_file.GetBlobFileNumber() over GetBlobFileGarbages() from BlobFileAddition to BlobFileGarbage; mem->ApproximateMemoryUsage() over autovector<ReadOnlyMemTable*> to the declared ReadOnlyMemTable; edit_list.empty() over autovector<autovector<VersionEdit*>> to autovector::empty.
  • protobuf, 7: the Objective-C generator's loops over std::unique_ptr<MessageGenerator>, EnumGenerator, OneofGenerator and ExtensionGenerator lists, which had gone to other generators' methods.
  • fmt, 3: vendored googletest's test_suite_info->GetTestSuiteName() over std::vector<ParameterizedTestSuiteInfoBase*>, from the derived template's method at 0.65 to the base's at 0.9.

Added: rocksdb 126 calls on loop variables plus 47 chained calls whose receiver is one (cfd->mem()->GetFirstSequenceNumber(), through matchCppCallChain); protobuf 22 plus 3; fmt 1 (gmock's m.GetDescriber() → MatcherBase::GetDescriber). All checked. 4 are wrong: rocksdb's column_family_test.cc loops over a std::vector<Iterator*> and calls SeekToFirst() / Next(), which reach MemTableRep::Iterator. That is the typed path's lookup by the type's last name, the namesake case #2440 measured for 1,364 declared Iterator* x receivers; a declared Iterator* it gets the same edge on main.

Confidence only: all on the same target, up to 0.9 (rocksdb 114 from 0.7, 92 from 0.65, 25 from 0.8; protobuf 42 from 0.7, 17 from 0.65).

The re-resolve harness used while iterating (both arms' resolvers over one index) agreed with the full A/B site for site; the full A/B adds only the chained calls the harness didn't re-resolve.

Time. rocksdb, indexed interleaved base/fix/base/fix on an idle box: 35 s / 36 s / 36 s / 35 s. Each loop's element is worked out once per calling function and loop.

Tests

__tests__/cpp-auto-range-for-receiver.test.ts, fixtures cut from rocksdb and protobuf. The assertions check the typed path's confidence, since a guess can land on the right method at 0.65/0.7. 7 of the 8 tests fail on main; the eighth, an element a template argument decides, is a guard that passes on both.

  • rocksdb's for (auto cfd : cfds) over an autovector parameter, where the old scan reached another function's ColumnFamilyHandle* cfd;
  • *versions_->GetColumnFamilySet(): a member another header declares, a smart pointer's ->, and a class iterated through its begin() and its iterator's operator*;
  • a free function's written return type, and a nullability annotation in an element;
  • an auto local's initializer, an alias with template arguments, and a std::shared_ptr element;
  • a data member of another object, and loops inside loops;
  • a std::string element's own member links to nothing;
  • a std::unique_ptr element: -> reaches the held type, .reset() nothing;
  • an element a template argument decides gets no typed edge.

Ran: tsc --noEmit; the new file; every cpp-*, c-cpp-*, c-calls-* and kernel-ccpp-parity suite plus extraction.test.ts (998/998, kernel rebuilt from main's source); the full suite, 6,913 passed and 82 skipped, with one failure, daemon-pid-reuse.test.ts's EBUSY rmdir in teardown on Windows, which passed when rerun alone.

🤖 Generated with Claude Code

… type of its range

#2439 typed a range-for variable when the loop names its type. A loop
over `auto` elements names none, so receiver inference kept scanning as
for an `auto` local with an unreadable initializer: it reached an earlier
declaration of the same name, often another variable's, or fell through
to the guess by the receiver's name. facebook/rocksdb's db_impl_write.cc
loops `for (auto cfd : cfds)` over a `const autovector<ColumnFamilyData*>&`.

The loop's range is now read from its header and typed as written,
template arguments and all, since the index keeps only a return type's
last name:
- a name the calling function declares, or a data member of its class
  read from the class's own lines (`versions_` from db_impl_write.cc);
- an `auto` local's initializer;
- `*x`, `x.m`, `x->m`, `this`, and calls whose declaration writes their
  return type, free or on the object's class (own or inherited);
- aliases followed with their template arguments (cppAliasedTypeWritten);
- the element: a standard or abseil sequence container's first template
  argument, protobuf's RepeatedPtrField and rocksdb's autovector's, or what
  a project class's begin() returns, dereferenced (ColumnFamilySet's
  iterator's operator* gives ColumnFamilyData*). Nullability annotations
  are dropped.

The element goes through the same declaration record as any declared
receiver, so the typed path, #2413's gate for a standard-library element
and #2440's held-type lookup for `->` on a smart-pointer element apply
unchanged. Classes are looked up with #2440's cppClassWritten, methods
through its cppMethodOf, and matchCppHeldTypeCall now reads its held type
through the shared cppStdHeldType.

A/B on main 8a5ba3d, site-keyed: leveldb identical; fmt +1 / 3
retargeted; rocksdb -68 / +173 / 104 retargeted / 231 confidence only;
protobuf -13 / +25 / 7 / 59. Every removal and retarget was a wrong
guess; 4 of rocksdb's additions reach MemTableRep::Iterator through the
typed path's last-name lookup, as a declared `Iterator* it` does on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit 01ab45d into main Oct 11, 2026
@colbymchenry
colbymchenry deleted the claude/dazzling-bouman-557717 branch October 11, 2026 15:19
colbymchenry added a commit that referenced this pull request Oct 11, 2026
…e class its type names, not a namesake (#2493)

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