Skip to content

fix(cpp): a declaration inside a string literal is not a receiver's declaration - #2531

Merged
colbymchenry merged 2 commits into
mainfrom
claude/cpp-raw-string-declarations
Oct 12, 2026
Merged

colbymchenry merged 2 commits into
mainfrom
claude/cpp-raw-string-declarations

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

C++ receiver inference no longer takes text inside a string literal for the receiver's declaration. Follow-up to #2493, which found the raw-string case in protobuf's upb generator.

Why

inferCppReceiverType reads back from a call for the first line that declares the receiver. It blanks comments (cppCodeOf) but not string literals, so a declaration-shaped text inside one won:

  • Raw strings. protobuf's upb generator emits C code from R"cc( … )cc" templates. GenerateRepeatedSetters(upb::FieldDefPtr field, …) writes upb_MiniTableField field = $field_init$; inside one, then calls field.ctype() on its own parameter. The scan found the template's line, took field for the C struct upb_MiniTableField (no methods), and the call got no edge (before fix(cpp): a call on a declared pointer, value or reference reaches the class its type names, not a namesake #2493, a guess).
  • Ordinary strings. rocksdb's log messages read as declarations: "… in WriteBatch handler; …" made handler a WriteBatch; "… Starting remote compaction (…" gave compaction the type remote; "…(rejected; output buffer … of input)" gave input the type of. Those calls fell to guesses by the method's name, or to nothing.

What changed

  • New src/resolution/cpp-raw-strings.ts:
    • cppRawStringLines(file) reads a file once, only when it holds R", the way the compiler does: comments, ordinary string and character literals, and raw strings (R, u8R, uR, UR, LR; delimiters up to 16 characters). It keeps, per line, the column ranges each raw string literal takes. Memoized per file, cleared with the other name-matcher memos.
    • cppLiteralsBlanked(text, line, rawLines) blanks those ranges plus the contents of the line's ordinary string and character literals, columns kept. A character literal holding a quote ('"'), a digit separator (1'000) and a quote or R"( inside a /* … */ don't hide the code after them.
  • inferCppReceiverType runs each candidate line through it before cppCodeOf, in the calling file and in its header.

Validation

Main 80e4b459 vs this branch, full index of each repo (kernel included), dumps compared by natural keys, every changed call site joined to where main's scan found the receiver's declaration:

repo changed call sites
google/leveldb 0 (byte-identical)
fmtlib/fmt 0 (byte-identical)
redis (C control) 0 (byte-identical)
facebook/rocksdb 89
protocolbuffers/protobuf 25

rocksdb, 89 — all improvements:

  • 45 keep the same target, now reached through the receiver's real declaration (0.9) rather than a guess by name (0.65–0.8): compaction->… on a Compaction*, handler->… on a WriteBatch::Handler*.
  • 35 added, all right. 16 handler->PutCF(…)-style calls reach WriteBatch::Handler. 17 chained calls like compaction->column_family_data()->GetID() reach ColumnFamilyData / Version now that compaction's type is read. 2 limiter->GetName() reach ConcurrentTaskLimiter.
  • 6 retargeted from wrong guesses to the declared Slice: input.size() was CompactionInputFiles::size, key.ToString() was a stress test's KeyGenerator::ToString.
  • 3 removed wrong guesses: gtest's TestPartResultArray::size and FilePath::c_str on two std::strings, and DBIter::ValueColumnsState::value on a std::optional<Slice>.

protobuf, 25:

  • 12 added + 1 confidence: the upb generator's calls on its own upb::FieldDefPtr parameters (ext.message_type(), ext.name(), …), formerly shadowed by declarations in its raw-string templates.
  • 4 removed wrong guesses: file->name() → CodeGeneratorResponse_File::name, triggered by the string "…expected prefixes file (".
  • 5 keep the same target at higher confidence.
  • 3 new wrong guesses (0.65). The real declaration is an auto local whose initializer the scan can't type: auto oneof = message->oneof_decl(i); and const auto* value = enum_->value(j);. Before, a declaration inside a string literal above it stopped the scan (no edge, right by accident). Now the scan reaches the auto, keeps going as it always has for an unreadable auto, and the call falls to the existing name guess (upb::OneofDefPtr::name, EnumValue::options). That fallback predates this change.

Tests

__tests__/cpp-string-literal-declarations.test.ts (3 cases):

  • the upb reproduction (raw string with quotes and ) inside);
  • an ordinary string that reads like a declaration (printer->Print("Stub* runner = nullptr;")).

Both fail on main and pass here. A guard passes on both: a declaration after '"', a digit separator, or a /* R"( … */ on the same line is still read. A naive line blanker fails it.

The C++ resolution suites pass: 313 tests, 0 failures.

CPU (process CPU time of a full index, interleaved runs): rocksdb main 128.0 / 123.7 s vs this branch 136.4 / 121.3 s; protobuf 278.8 / 288.2 s vs 286.7 / 273.4 s. No measurable cost: a file is read for raw strings only when it contains R", once.

🤖 Generated with Claude Code

colbymchenry and others added 2 commits October 12, 2026 16:00
…eclaration

C++ receiver inference reads back from a call for a line that declares
the receiver, blanking comments (cppCodeOf) but not string literals. Text
inside one was taken for the declaration:

- protobuf's upb generator emits C code from raw strings:
  `GenerateRepeatedSetters(upb::FieldDefPtr field, …)` writes
  `upb_MiniTableField field = $field_init$;` inside an `R"cc( … )cc"`,
  then calls `field.ctype()` on its own parameter. The scan found the
  template's line first, and the call got no edge (a C struct has no
  methods).
- rocksdb's log messages read as declarations: `"… in WriteBatch handler;
  …"` made `handler` a `WriteBatch`, and `"… remote compaction (…"` gave
  `compaction` the type `remote`, so calls fell to guesses by the method's
  name.

New cpp-raw-strings.ts reads a file once, when it holds `R"`, as the
compiler does (comments, string and character literals, raw strings with
their delimiters), and keeps the column ranges each raw string takes per
line. The receiver scan, in the file and its header, blanks those and the
contents of the line's ordinary string and character literals before
cppCodeOf. A character literal holding a quote (`'"'`), a digit separator
(`1'000`) and an `R"(` inside a comment don't hide the code after them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit c19f028 into main Oct 12, 2026
@colbymchenry
colbymchenry deleted the claude/cpp-raw-string-declarations branch October 12, 2026 22:17
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