fix(cpp): a `->` call on a smart pointer or optional reaches the type it holds
Summary
A C++ call made with -> on a receiver declared as a std::unique_ptr<T>, std::shared_ptr<T> or std::optional<T> now links to T's method, or to a method of a class T derives from.
Before, receiver inference read the declared type as unique_ptr, found no method on it, and the call fell through to Strategy 3's guess by method name and receiver words:
- facebook/rocksdb:
std::unique_ptr<Iterator> iter; iter->Valid()reachedArenaWrappedDBIter::Valid.std::shared_ptr<FileSystem> fs; fs->GetFileSize(…)reached the test doubletest::StringFS::GetFileSize.std::unique_ptr<Transaction> writer; writer->Put(…)reachedSstFileWriter::Put. - protocolbuffers/protobuf:
std::unique_ptr<FieldGeneratorBase> generator; generator->GenerateMembers(…)reachedMapFieldGenerator::GenerateMembers. The Kotlin generator'sstd::unique_ptr<FileGenerator>reachedjava::FileGenerator.
This is the follow-up #2413 left open. It uses that PR's CppReceiverDeclaration (raw, callers, shadowed) and cppMemberOperator. It applies only when all of these hold:
- the declaration is the caller's own and not shadowed;
- it is not an alias and not a pointer;
- the call is
->; - the holder is
std::-qualified, asisCppLibraryTypereads it.
Why the held type is looked up as C++ does, not by its last name
The plan was resolveMethodOnType on T's last segment. Measured on rocksdb, that sends about 3,800 calls to the wrong class with a 0.9 typed edge (2,998 retargets, 611 additions, 154 confidence bumps):
Iteratornames 22 classes in rocksdb:MemTableRep::Iterator,SkipList::Iterator,InlineSkipList::Iteratorand gtest's generatorIterators among them.- The
Iteratorthese calls mean declares none ofValid/Next/Seek/key; it inherits them fromIteratorBase. - So
getMethodMatches('Iterator', 'Valid')findsMemTableRep::Iterator::Valid, and the supertype walk never runs.
So T goes through #2407's C++ lookup, now shared as cppClassWritten, from where the call is written:
- the calling function first (a class local to it);
- its class and that class's bases;
- the enclosing namespaces;
- the file's
usingdeclarations, directives and namespace aliases; - aliases followed;
- failing all that, the only class of that name the file can see (that edge gets 0.85).
The method is then taken from that class, or from its bases through the class's own extends edges. A new node-keyed getSupertypeNodes reads those edges; the name-keyed getSupertypes unions every same-named class's bases.
Each shape, decided by measurement
| held type | outcome | rocksdb | protobuf | fmt |
|---|---|---|---|---|
| a project class with the method, its own or inherited | typed edge at 0.9, or 0.85 through the unique-name fallback | 7,680 retargeted · 2,500 added · 3,509 same target, now 0.9 | 62 · 54 · 86 | 1 · 3 · 1 |
a template parameter (std::unique_ptr<IterType>) | no edge | 25 removed | 0 | 0 |
a library type (std::deque<…>, std::vector<…>), or an alias of one (using VectorWalPtr = std::vector<…>) | no edge | 9 removed | 0 | 0 |
| a project class that has no such method, nor do its bases | no edge; a class template falls through, since a specialization may declare it (as in #2399) | no such call had an edge before | 0 | 0 |
| a type the project doesn't declare, or an alias the lookup can't follow | unchanged | 38 guesses kept | 12 guesses kept | 0 |
-
Types the project doesn't declare keep their guesses. In the probe, those guesses were about half right:
- right: generated
unittest::TestAllTypes→Message::SpaceUsedLong;std::unique_ptr<Rep> rep_→ methods of an out-of-linestruct BlockBasedTableBuilder::Rep, which the index names<anonymous>; - wrong:
ZeroCopyInputStream→ a test stream, whilePROTOBUF_EXPORThid the class. #2449 now indexes it, so those 24 calls resolve through this PR toZeroCopyInputStream/ZeroCopyOutputStream.
- right: generated
-
An alias the lookup can't follow keeps its guess. rocksdb's
port::Threadisstd::threadinport_posix.handWindowsThreadinport_win.h. Only an alias of a library type or of a template parameter is silenced. -
Iterators: unchanged. No receiver in the five repos is declared as
std::…<T>::iteratorand called with->. All 31->calls on an iterator go through aliases:- gmock's
ExpectationSet::const_iterator; - rocksdb's
ColumnFamilySet::iterator, which is a project class.
Reading the element type through an alias needs the alias's template arguments, which
cpp-type-aliases.tsdoesn't keep. Left as a follow-up. - gmock's
-
->on a raw pointer to a smart pointer: unchanged.std::unique_ptr<T>* p; p->reset()calls the holder's own member, and #2413's gate already silences it. #2413's gate log has 549 such calls in rocksdb and 1 in protobuf. All arereset/get/release/swap/has_value/value/emplace. A test pins it.
Validation
Setup.
- Every arm ran
codegraph init -ywith the native kernel, on this Windows box. - Base is main
b8b3eb97, which includes #2413, #2421, #2426, #2434, #2439, #2443, #2445, #2446 and #2449; the other arm is this branch merged with it. Both use one kernel built from that source. - The A/B was re-run on each C++ change main took while this PR was open:
ed199e60,9a1d4bd9(#2449),16344af8(#2434, protobuf only),e47cb258(#2443, #2445, #2446) andb8b3eb97(#2439).- #2449 added 29 protobuf sites: the
ZeroCopy*Streamcalls above,msg->GetReflection()turning from confidence bumps into additions where main stopped guessing, andDynamicMessageFactory's override (below). It also dropped one rocksdb removal: main no longer guessesWindowsThread::joinfor astd::optional<std::thread>. - #2439 reads typed range-
forloop variables, so 8 rocksdb calls on smart-pointer loop variables now take this path. Seven arefor (const std::shared_ptr<Cache>& cache : …)in db_block_cache_test.cc, which go from 0.8 to 0.9 on the sameCache::*targets. The eighth isfor (const std::shared_ptr<TableFactory>& tf : …) tf->Name(), newly linked toCustomizable::Name. - The other C++ commits left every changed site as it was.
- #2449 added 29 protobuf sites: the
- Edges are diffed per call site (kind, source, line, col, refName), and each changed site is joined to a probe log of that call's declaration and held type.
- The same A/B on the earlier base (
d8a7f868+ #2413) gave identical changed sites once qualified names are compared without the namespaces #2426 restores. The one exception is protobuf'sstd::unique_ptr<TestMapField>. On the old base its alias led to aMapFieldwhose namespace the index had lost, so the call kept its guess. With #2426 the alias is followed, and the sameMapFieldBase::GetRepeatedFieldtarget goes from 0.65 to 0.9.
What didn't change.
- leveldb and redis (the C control): dumps byte-identical.
- No extends, implements or synthesized edge changed in any repo, the cpp-override edges included, so the
matchCppSupertyperefactor changes nothing on real data. - Every changed site is a
->call on a caller-ownedstd::holder.
Removed (rocksdb 34; protobuf and fmt none), every one read:
- 25 template-parameter calls:
StressTest::TestIterateImpl'sstd::unique_ptr<IterType>→ArenaWrappedDBIter::*;BlockBasedTable::PutDataBlockToCache'sstd::unique_ptr<TBlocklike>→Block::own_bytesandBlockBasedTable::ApproximateMemoryUsage.
- 6 calls on library types:
std::unique_ptr<std::deque<BlockHandleInfo>>→Block::size/BlockBuilder::empty/autovector::emplace_back;std::optional<std::vector<WriteBatch>>→autovector::emplace_back.
- 3
std::unique_ptr<VectorWalPtr>calls →CompactionInputFiles::size.
Retargeted and added.
-
An independent check re-derived each target from the declaration's spelling, the caller's scopes and the
extendsedges. It agrees for rocksdb's 6,913 + 2,450 class sites and for every class site in fmt and protobuf; the 29 protobuf sites #2449 added and the 8 rocksdb range-for sites #2439 added were read by hand. -
The ~1,000 it can't place go through:
- aliases (
InternalIterator→InternalIteratorBase,LogFile→WalFile,MergerMaxIterHeap→BinaryHeap,UserDefinedIndexBuilder→IndexFactoryBuilder); using namespace ROCKSDB_NAMESPACE;(std::unique_ptr<DB>in tools and examples);- or the unique-name fallback (a fixture's nested
TestItem,CountingFileReadListener).
I grouped them by declaration and target and read each group.
- aliases (
-
Read by hand:
- all of protobuf's 62 retargets and 54 additions;
- all 5 fmt changes;
- in rocksdb: 40 random retargets, 25 random additions, 38 retargets and 37 additions each with a different held type, and every held type resolved through a base class (36).
All were right. The old targets were implementations, test doubles or namesakes:
ModelDB::CompactRangefor astd::unique_ptr<DB>;CuckooTableReader::NewIteratorfor astd::unique_ptr<TableReader>;ChargedCache::Lookupfor astd::shared_ptr<SecondaryCache>;BlobFileBuilder::Finish;KeyGenerator::Nextfor astd::unique_ptr<LockTracker::KeyIterator>.
-
DynamicMessageFactory: on mained199e60, protobuf'sstd::unique_ptr<DynamicMessageFactory>reached the base virtualMessageFactory::GetPrototype, because the index stored the class's own override under a namePROTOBUF_NONNULLmangled. #2449 fixed the name, and the call now reachesDynamicMessageFactory::GetPrototypeitself.
Cost. Process CPU time (all threads) for a full index, interleaved runs on the earlier base:
- rocksdb: main 165.8 / 164.2 / 164.4 s, this branch 163.6 / 159.7 / 156.7 s;
- protobuf: main 422.0 / 409.4 s, this branch 399.4 / 393.6 s.
No measurable cost; fewer calls reach the by-name search.
Code
src/resolution/name-matcher.ts:matchCppHeldTypeCallandcppMethodOf, called frommatchMethodCallright after #2413's gate.src/resolution/cpp-supertypes.ts:matchCppSupertype's lookup is nowcppClassWritten(written, site), parameterized by where the name is written: scope, file and line, template parameters, and the class it can't be.matchCppSupertypeis a thin wrapper over it, with unchanged behavior.src/resolution/cpp-namespaces.ts(new):cppNamespaceAliasesandcppMacroNamespaceFrames, moved byte for byte out of name-matcher.ts. That way cpp-supertypes.ts no longer imports name-matcher, and name-matcher imports cpp-supertypes without a cycle.src/resolution/types.tsandindex.ts:getSupertypeNodes(id), memoized per supertype generation likegetSupertypes.src/resolution/cpp-type-aliases.ts:cppTemplateArguments.
Tests
-
New file:
__tests__/cpp-smart-pointer-receiver.test.ts, 8 tests. It covers:- rocksdb's reproduction, with
MemTableRep::IteratorandArenaWrappedDBIteras decoys; shared_ptr,optional, a parameter, and a member the class declares in its header, with decoys named after each receiver;- a nested class found first from inside the class that declares it;
- an alias held type, a
using namespace, and a class local to the function; - template parameters, library types and std aliases getting no edge;
- a class lacking the method;
- what stays as it was: a generated message, a class template's specialization, and
->on astd::unique_ptr<T>*.
Against main, 6 of the 8 fail. The other 2 pin unchanged behavior.
- rocksdb's reproduction, with
-
All 19 C/C++ test files pass on the merged branch (216 tests), among them #2407's
cpp-base-class-lookupagainst the refactored lookup, and #2426's and #2421's new suites.tsc --noEmitis clean. -
Full suite on the merged branch. This box ran at 100% CPU, with other sessions' suites and indexing alongside.
- The parallel run passed 6,549 of 6,674 tests (82 skipped). All 43 failures were timeouts (41) or
EBUSYtemp-dir removals (2), in 17 load-sensitive files such assync,wal-deferral,worktree-detection,mcp-*,git-index-currencyandfunction-ref. - Rerun one at a time with
--testTimeout=120000, those 17 files pass: 938 tests, 0 failed, 1 skipped.
- The parallel run passed 6,549 of 6,674 tests (82 skipped). All 43 failures were timeouts (41) or
Overlap
- #2426 (merged) is why protobuf's
TestMapFieldcall now gets its typed edge, as described above. #2449 (merged) made theZeroCopy*StreamandDynamicMessageFactorycalls resolvable. #2443 (nested template arguments), #2445 (::-qualified names) and #2439 (range-forloop variables) touch the same files. Each sync conflicted only on name-matcher's import line or CHANGELOG, and the A/B on top of them is unchanged apart from #2439's eight range-forsites.
Follow-ups (not in this PR)
- Main's typed path for raw pointers has the same namesake problem.
Iterator* it; it->Valid()resolves by the last segment, so in rocksdb 1,364 of the 44,915 calls that path types reachMemTableRep::Iteratorrather than the class C++ lookup finds. Routing that path throughcppClassWrittenandcppMethodOfis the fix. - An alias of a smart pointer gets no edge today, because #2399's rule silences a capitalized alias that names no class. In rocksdb that's about 90
->calls: 60 throughScopedArenaPtr<InternalIterator>, an alias template ofstd::unique_ptr, and 28 through block_test.cc'stypedef std::unique_ptr<DataBlockIter> IterPtr;. Following it needs the alias's template arguments. - Iterators through aliases: 31 calls (above).
absl::StatusOr<T>andabsl::optional<T>also hand->toT. protobuf has about 75 such calls, most unlinked today.- The declarator regex spans two templated parameters on one line. In
f(std::shared_ptr<A> a, std::unique_ptr<B> b),b's type reads asstd::shared_ptr<A> a, std::unique_ptr<B>. That's 31->calls in the probes, 1 of them on a smart pointer.
🤖 Generated with Claude Code