Skip to content

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

Merged
colbymchenry merged 6 commits into
mainfrom
claude/modest-bun-b7b741
Oct 7, 2026
Merged

colbymchenry merged 6 commits into
mainfrom
claude/modest-bun-b7b741

Conversation

@colbymchenry

@colbymchenry colbymchenry commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

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

Validation

Site-keyed before/after edge diffs (diff-sites.mjs):

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

🤖 Generated with Claude Code

colbymchenry and others added 3 commits October 7, 2026 05:40
…s own class's method

normalizeCppTypeName cut each `<…>` at its first `>`, so a declared type
with template arguments nested in its template arguments kept debris:
`autovector<std::pair<int, FileMetaData*>>` read as `autovector >` and
`Striped<CacheAlignedWrapper<port::Mutex>>` as `Striped >`. No class has
such a name, so the call was never resolved on the receiver's class. A
capitalized debris name made isUndeclaredTypeName take the type for an
outside one and silence the call (rocksdb's `mutex_.Get(key)`), and a
lowercase one fell to a guess by the receiver's words (rocksdb's
`files_marked_for_compaction_.clear()` reached `CompactionInputFiles::clear`).

Template arguments are now stripped with their nesting by
cpp-type-aliases' depth-tracking helper, exported as
stripCppTemplateArguments; cpp-supertypes' identical private copy now
imports it. A `>` that closes no `<` is the end of a declaration begun on
an earlier line (`std::unique_ptr<BlobContents>>>& blob_reqs` under
`autovector<std::pair<…,`): a depth strip alone would read the argument's
type there, so such text names no type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… what is above it

#2413 marks the receiver as shadowed when the scan back to its
declaration passes one whose type it can't read, so a member or earlier
variable of the same name found further up doesn't decide the call.
A declaration whose type began on an earlier line is such a one now that
its last line reads as no type, where it used to stop the scan with a
type no class has.

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