fix(cpp): a receiver declared with nested template arguments calls its own class's method

Merged
CO
8d ago
+321-21

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 asread beforeread 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: isUndeclaredTypeName took it for an outside type and dropped the call. rocksdb's mutex_.Get(key) on a Striped<…<…>> had no edge.
  • Lowercase: the call fell to Strategy 3's guess by the receiver's words. rocksdb's files_marked_for_compaction_.clear() reached CompactionInputFiles::clear.

Changes:

  • normalizeCppTypeName now strips template arguments by depth with the helper cpp-type-aliases.ts already had, exported as stripCppTemplateArguments. cpp-supertypes.ts had 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's std::unique_ptr<BlobContents>>>& blob_reqs under autovector<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's shadowed flag on it, the way #2413 treats auto 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 ed199e60 vs this branch. They share a kernel built from the same source, and their dist differs only in name-matcher.js, cpp-type-aliases.js and cpp-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 cdf06812 against its parent ce78b63d. That parent adds #2434's foo_unittest.cc test 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 (same git patch-id).
reporemovedaddedretargetedmetadata-only
google/leveldb0000
fmtlib/fmt2000
protocolbuffers/protobuf0600
facebook/rocksdb0501131
redis (C control)0000

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::Get calls (mutex_, loader_mutex_). These were silenced before by the capitalized Striped >.
  • Retargeted (11): from Strategy 3 guesses at 0.65 to autovector's methods at 0.9. Nine guesses were CompactionInputFiles::{clear,size,empty} (the receivers are files_marked_for_compaction_, bottommost_files_…), one was LazyWideColumnsBatch::size and one FileChecksumList::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>> in hpb/status_test.cc now reaches hpb::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::StatusOr is taken for the project's hpb::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 to hpb::StatusOr::ok. Nested absl::StatusOr receivers now join them. Of the 33 such receivers that now read as StatusOr, these 2 changed edges; the rest call status(), size() or value_or(), which hpb::StatusOr lacks 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 from native_formatter.
  • The receiver now reads formatter, like fmt's receivers declared with flat formatter<…> types. Its only indexed format is test/format-test.cc's formatter<incomplete_type>::format. isVisibleAcrossFiles drops 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 reaches native_formatter by 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::clear guess;
    • only a push_back@0.7 guess, with size missing;
    • no Striped::Get edge.
  • 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 shadowed flag. Without it, a std::string blob_reqs member hidden by the parameter would silence the call.
  • Full suite on the merged branch (Windows, shared box at 100% CPU), in three passes:
    1. Parallel run: 6,394 / 6,672 passed. All 194 failures were timeouts or environment races, none in C++ tests.
    2. 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.
    3. 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-macros and kernel-ccpp-parity, cpp-brace-scopes, go-embedding-no-override, jsx-child-disambiguation, and the C++ receiver and base-class suites.
  • tsc passes, including a type-check of the new test file.

🤖 Generated with Claude Code