Files
codegraph/__tests__/cpp-string-literal-declarations.test.ts
T
Colby Mchenry c19f0284f8 fix(cpp): a declaration inside a string literal is not a receiver's declaration (#2531)
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>
2026-10-12 22:17:33 +00:00

183 lines
6.0 KiB
TypeScript

/**
* A declaration written inside a C++ string literal is text, not code, so
* receiver inference doesn't take it for the receiver's declaration.
*
* protocolbuffers/protobuf's upb generator emits C code from raw string
* templates: `GenerateRepeatedSetters(upb::FieldDefPtr field, …)` writes
* `upb_MiniTableField field = $field_init$;` inside an `R"cc( … )cc"`, then
* calls `field.ctype()` on its own parameter. Reading back from the call
* found the template's line first, took `field` for a `upb_MiniTableField` —
* a C struct with no methods — and the call got no edge (before #2493, a
* guess by the method's name).
*
* A raw string can span many lines and hold quotes and comment markers, so
* its lines are found by reading the file once, as the compiler does. An
* ordinary string's contents are blanked on the line being read.
*/
import { describe, it, expect, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
import type { Node } from '../src/types';
const roots: string[] = [];
afterEach(() => {
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
});
async function indexed(files: Record<string, string>): Promise<CodeGraph> {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-strdecl-'));
roots.push(root);
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
return CodeGraph.init(root, { index: true });
}
function callable(cg: CodeGraph, qualifiedName: string): Node {
const node = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName);
if (!node) throw new Error(`no function or method ${qualifiedName}`);
return node;
}
/** `calls` callees of a function or method, as `qualifiedName`. */
function calls(cg: CodeGraph, caller: string): string[] {
return cg
.getCallees(callable(cg, caller).id)
.filter((r) => r.edge.kind === 'calls')
.map((r) => r.node.qualifiedName)
.sort();
}
const lines = (...l: string[]): string => [...l, ''].join('\n');
/** upb's field wrapper, and the C struct a generated template declares. */
const UPB = {
'upb/reflection/def.hpp': lines(
'namespace upb {',
'class FieldDefPtr {',
' public:',
' int ctype() const { return 0; }',
' const char* name() const { return ""; }',
'};',
'} // namespace upb',
),
'upb/mini_table/field.h': lines(
'typedef struct upb_MiniTableField {',
' int number;',
'} upb_MiniTableField;',
),
};
describe('a declaration inside a string literal is not the receiver\'s', () => {
it('reproduction: upb\'s generator declares `field` in the C code a raw string emits', async () => {
const cg = await indexed({
...UPB,
'upb_generator/c/generator.cc': lines(
'#include "upb/reflection/def.hpp"',
'#include "upb/mini_table/field.h"',
'namespace upb {',
'namespace generator {',
'void Emit(const char* text) {}',
'void GenerateRepeatedSetters(upb::FieldDefPtr field) {',
' Emit(R"cc(',
' UPB_INLINE void $0_resize(void* msg, size_t size) {',
' upb_MiniTableField field = $1;',
' // "a quote" and ) in the template',
' upb_Message_Resize(msg, &field, size);',
' }',
' )cc");',
' if (field.ctype() == 1) Emit(field.name());',
'}',
'} // namespace generator',
'} // namespace upb',
),
});
try {
expect([...new Set(calls(cg, 'upb::generator::GenerateRepeatedSetters'))]).toEqual([
'upb::FieldDefPtr::ctype',
'upb::FieldDefPtr::name',
'upb::generator::Emit',
]);
} finally {
cg.close();
}
});
it('an ordinary string that reads like a declaration', async () => {
const cg = await indexed({
'src/runner.h': lines(
'namespace demo {',
'class Runner {',
' public:',
' void Run() {}',
'};',
'class Stub {',
' public:',
' void Run() {}',
'};',
'class Printer {',
' public:',
' void Print(const char* text) {}',
'};',
'} // namespace demo',
),
'src/generator.cc': lines(
'#include "src/runner.h"',
'namespace demo {',
'void Generate(Printer* printer, Runner* runner) {',
' printer->Print("Stub* runner = nullptr;");',
' runner->Run();',
'}',
'} // namespace demo',
),
});
try {
expect(calls(cg, 'demo::Generate')).toEqual(['demo::Printer::Print', 'demo::Runner::Run']);
} finally {
cg.close();
}
});
});
describe('what a literal on the line does not hide', () => {
it('a declaration after a character literal holding a quote, or after a comment holding `R"(`', async () => {
const cg = await indexed({
'src/runner.h': lines(
'namespace demo {',
'class Runner {',
' public:',
' void Run() {}',
'};',
'class Stub {',
' public:',
' void Run() {}',
'};',
'inline Runner* MakeRunner() { return nullptr; }',
'} // namespace demo',
),
'src/quotes.cc': lines(
'#include "src/runner.h"',
'namespace demo {',
'void Quote(Stub* runner) {',
' char q = \'"\'; int n = 1\'000; Runner* runner = MakeRunner();',
' runner->Run();',
'}',
'void Comment(Stub* runner) {',
' /* R"( not a raw string */ Runner* runner = MakeRunner();',
' runner->Run();',
'}',
'} // namespace demo',
),
});
try {
expect(calls(cg, 'demo::Quote')).toEqual(['demo::MakeRunner', 'demo::Runner::Run']);
expect(calls(cg, 'demo::Comment')).toEqual(['demo::MakeRunner', 'demo::Runner::Run']);
} finally {
cg.close();
}
});
});