fix(cpp): a receiver declared with nested template arguments calls its own class's method
Summary
C++ receiver inference reduces a receiver's declared type to its last name in normalizeCppTypeName. The function cut each <…> at its first > (.replace(/<[^>]*>/g, ' ')), so nested template arguments left debris that names no class:
| declared as | read before | read now |
|---|---|---|
autovector<std::pair<int, FileMetaData*>> | autovector > | autovector |
Striped<CacheAlignedWrapper<port::Mutex>> | Striped > | Striped |
formatter<remove_cvref_t<T>, Char> | formatter , Char> | formatter |
A call on such a receiver was never looked up on its class. What happened next depended on the debris:
- Capitalized:
isUndeclaredTypeNametook it for an outside type and dropped the call. rocksdb'smutex_.Get(key)on aStriped<…<…>>had no edge. - Lowercase: the call fell to Strategy 3's guess by the receiver's words. rocksdb's
files_marked_for_compaction_.clear()reachedCompactionInputFiles::clear.
Changes:
normalizeCppTypeNamenow strips template arguments by depth with the helpercpp-type-aliases.tsalready had, exported asstripCppTemplateArguments.cpp-supertypes.tshad an identical private copy; it now imports the shared one.- A
>that closes no<(cppTypeBeganAbove) means the type began on an earlier line, as with rocksdb'sstd::unique_ptr<BlobContents>>>& blob_reqsunderautovector<std::pair<BlobRangeReadRequest*,. A depth strip alone would read the inner argument's type there. So that text names no type, and the scan back to the declaration sets #2413'sshadowedflag on it, the way #2413 treatsauto x = Make();. A same-named member found further up then doesn't decide the call. Before this change, such a line stopped the scan with a debris type, which #2413's library-type check already declines.
Validation
Site-keyed before/after edge diffs (diff-sites.mjs):
- Arms: main
ed199e60vs this branch. They share a kernel built from the same source, and theirdistdiffers only inname-matcher.js,cpp-type-aliases.jsandcpp-supertypes.js. - The A/B was first run on main
d8a7f868, before #2413, #2426 and #2421 landed. It was redone after syncing. All 99 changed sites are identical in both runs; one target only gained its namespace through #2426. - Re-run on main
cdb994b4, after syncing in #2449's C/C++ extractor change (attribute macros), plus #2430 (Go) and #2442 (JSX). The changed sites in leveldb, fmt, rocksdb and protobuf are identical, site by site. redis wasn't re-run: it's C, and receiver inference runs only for C++. - Post-merge check, squash
cdf06812against its parentce78b63d. That parent adds #2434'sfoo_unittest.cctest naming, plus #2447, #2448, #2444, #2441, #2436, #2433, #2437 and #2431. The changed sites in the four C++ repos are again identical. #2447 landed between the last pre-merge check and the merge, and the squash applied exactly the tested patch (samegit patch-id).
| repo | removed | added | retargeted | metadata-only |
|---|---|---|---|---|
| google/leveldb | 0 | 0 | 0 | 0 |
| fmtlib/fmt | 2 | 0 | 0 | 0 |
| protocolbuffers/protobuf | 0 | 6 | 0 | 0 |
| facebook/rocksdb | 0 | 50 | 11 | 31 |
| redis (C control) | 0 | 0 | 0 | 0 |
A probe arm logged every receiver whose type now reads differently: leveldb 8, fmt 27, protobuf 243 and rocksdb 1,351 call sites. Most are std containers (vector, unique_ptr, map…) that no project class is named after, so their outcome is unchanged. Every changed edge site maps to one of those logged receivers.
rocksdb (all 92 sites checked). Receivers are declared as autovector<…<…>> (85 sites) or Striped<CacheAlignedWrapper<port::Mutex>> (7). Each declaration that was read is the receiver's own: 70 are in the calling function's body or parameters and 22 are members of the caller's class. Each new target is that class's own method.
- Added (50):
- 43
autovector::{size, empty, back, push_back, clear, begin, end}calls. These had no edge before: each method has several owners, and the receiver's words name none of them. - 7
Striped::Getcalls (mutex_,loader_mutex_). These were silenced before by the capitalizedStriped >.
- 43
- Retargeted (11): from Strategy 3 guesses at 0.65 to
autovector's methods at 0.9. Nine guesses wereCompactionInputFiles::{clear,size,empty}(the receivers arefiles_marked_for_compaction_,bottommost_files_…), one wasLazyWideColumnsBatch::sizeand oneFileChecksumList::size. All 11 old targets were wrong. - Metadata-only (31): the target stays
autovector::emplace_back, now as a typed edge (0.9) instead of a guess for being the only method of that name (0.7).
protobuf (6 added).
-
4 right:
hpb::StatusOr<std::unique_ptr<int>>inhpb/status_test.ccnow reacheshpb::StatusOr::ok/value. These were silenced before. -
2 wrong, both in
json/internal/parser.cc:absl::StatusOr<absl::Span<char>> decoded; decoded.ok()→hpb::StatusOr::ok;absl::StatusOr<absl::optional<int32_t>> x; x->has_value()→google::protobuf::Option::has_value, a 0.7 guess for being the only method of that name.
Both come from a problem main already has with types that have no nested arguments. The normalizer keeps only the last name, so
absl::StatusOris taken for the project'shpb::StatusOr. On main,absl::StatusOr<int32_t> value; value.ok()(parser.cc:341, also descriptor.cc:5163, option_interpreter.cc:1086, compiler/plugin.cc:107…) already links tohpb::StatusOr::ok. Nestedabsl::StatusOrreceivers now join them. Of the 33 such receivers that now read asStatusOr, these 2 changed edges; the rest callstatus(),size()orvalue_or(), whichhpb::StatusOrlacks and no guess picks. A follow-up task is filed to type a namespace-qualified receiver by C++ name lookup.
fmt (2 removed). Both are value_formatter_.format(*it, ctx) in ranges.h's formatter<join_view<…>, Char>, where value_formatter_ is declared formatter<remove_cvref_t<value_type>, Char>.
- The old edges were word-overlap guesses (
detail::native_formatter::format, 0.65). They were right only for built-in element types, whose formatter specialization derives fromnative_formatter. - The receiver now reads
formatter, like fmt's receivers declared with flatformatter<…>types. Its only indexedformatistest/format-test.cc'sformatter<incomplete_type>::format.isVisibleAcrossFilesdrops that one, since a header can't see it, so no edge remains. - On main, every such flat receiver in fmt's headers (
impl_.format,underlying_.parse,formatter_.format…) already ends unresolved this way. fmt's partial specializations are indexed as<anonymous>, so nothing reachesnative_formatterby name.
Stray > rule. It fired at 2 receiver sites across the four C++ repos, rocksdb blob_reqs and protobuf deps. Neither changed an edge.
Tests
__tests__/cpp-nested-template-receiver.test.ts (6 integration tests and 1 unit block):
- Red phase on main's normalizer: 3 tests fail and reproduce rocksdb's behavior:
- the
CompactionInputFiles::clearguess; - only a
push_back@0.7guess, withsizemissing; - no
Striped::Getedge.
- the
- Multi-line test: passes on main and fails under a plain depth strip, which reads
CachableEntry::size. - Member-hiding test: pins the composition with #2413's
shadowedflag. Without it, astd::string blob_reqsmember hidden by the parameter would silence the call. - Full suite on the merged branch (Windows, shared box at 100% CPU), in three passes:
- Parallel run: 6,394 / 6,672 passed. All 194 failures were timeouts or environment races, none in C++ tests.
- Serial rerun of the 73 affected files with 180 s timeouts: 2,129 / 2,145 passed. 5 still failed: function-ref's #1820 (Python, its own 60 s timeout), MCP staleness banner, status freshness
EBUSY, writer lock, and the MPEG-TS 2 s threshold. - Those 5 run alone: all pass.
- The C++ suites from #2413, #2426 and #2421 pass on the merged branch. #2426's kernel parity cases needed the kernel rebuilt from the merged source.
- After syncing #2449, #2430 and #2442, these all pass serially on the merged tree (123/123): this PR's file, #2449's
cpp-attribute-macrosandkernel-ccpp-parity,cpp-brace-scopes,go-embedding-no-override,jsx-child-disambiguation, and the C++ receiver and base-class suites. tscpasses, including a type-check of the new test file.
🤖 Generated with Claude Code