fix(cpp): a line starting with * is read as code, and block comments and string literals are blanked before declarations are read (#2552)

The line-at-a-time C++ declaration scans took any line starting with `*` for
a comment continuation, skipping rocksdb's `const auto& el =` /
`*reinterpret_cast<std::shared_ptr<EventListener>*>(jhandle);`, and read code
commented out in `/* … */` and the text of string literals (protobuf's
`[[deprecated("Please use SpaceUsedLong() instead")]]`) as declarations.
Block comments are now found in the per-file pass that finds raw strings, and
every scan blanks them, with string contents, before reading a line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-14 06:31:57 +00:00
committed by GitHub
parent d94a92ea89
commit d4b9c2b4e9
4 changed files with 254 additions and 46 deletions
+1
View File
@@ -111,6 +111,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In C++, a call whose receiver's type can't be read no longer links to a method of a class nested in the class the receiver's name suggests, or of a class whose name only contains that name. Before, such a guess took any method of the call's name whose qualified name contained the class's name: protocolbuffers/protobuf's `descriptor->file()` in a template reached `Descriptor::ExtensionRange::file`, and facebook/rocksdb's `CompositeEnv::IsInstanceOf(name)` reached `CompositeEnvWrapper::IsInstanceOf`. A call written through a class, like `PessimisticTransaction::PopSavePoint()` in an override, now links to the method that class inherits, here `TransactionBaseImpl::PopSavePoint`. Re-index C++ projects after upgrading.
- In C++, an alias that names a class through a class derived from the one that declares it, like protocolbuffers/protobuf's `using ConformanceRequestSetting = BinaryAndJsonConformanceSuite::ConformanceRequestSetting;`, now leads to that class, `ConformanceTestSuite::ConformanceRequestSetting`. Before, an alias was followed only to a class declared under exactly the name it spells, so objects created through this one linked to the alias rather than the class. Re-index C++ projects after upgrading.
- In C++, a name written in a googletest `TEST_F` or `TEST_P` body now finds the test fixture's nested types, those of its base classes included, before anything in the surrounding namespace, as C++ does for the member function such a body is: facebook/rocksdb's `new TestUserDefinedIndexFactory()` links to the class nested in the fixture's base. A class declared inside a test body, or inside any function, is now only found from that function, so the local classes many RocksDB tests each declare under one name, like `FakeWF` or `MyManager`, no longer stand in for one another, and a call like `fwf->SetIOError(true)` reaches the `FakeWF` its own test declares. Re-index C++ projects after upgrading.
- In C++, a declaration continued on a line that starts with `*`, like facebook/rocksdb's `const auto& el =` over `*reinterpret_cast<std::shared_ptr<EventListener>*>(jhandle);`, is now read, so calls such as `el->OnFlushCompleted(…)` link to `EventListener`'s methods; such a line used to be skipped as part of a comment. Code commented out in a `/* … */` block, and the text of a string literal, like protocolbuffers/protobuf's `[[deprecated("Please use SpaceUsedLong() instead")]]`, are no longer read as declarations, which had sent a call to the wrong method or to none. Re-index C++ projects after upgrading.
- Framework support now switches on when an app's `package.json` sits three or more folders deep, as with an ASP.NET solution's Angular or React app in `src/Web/ClientApp/`, or a React app in a workspace folder like `web/ui/mantine-ui/`. Before, only the project root and the first two folder levels were read, so frameworks such as Angular, React Router, Vue Router, SvelteKit and TanStack Router could go undetected there: an Angular app's routes, its `routerLink` and `router.navigate(…)` navigation and its template's event bindings were missing, so a handler used only from a `(click)` binding had no callers, and a React app's `navigate('/login')` calls didn't reach their route. Re-index projects laid out this way after upgrading.
- In a repository that mixes languages, a web framework's naming conventions now link only code written in that framework's own languages. Before, Express's also linked Go and Python code: in etcd a Go method's `*zap.Logger` result type linked to the method itself, and in Harbor a Python `self.jaeger.validate()` call linked to another class's `validate`. Links that cross languages on purpose, like React Native and Expo native modules, Swift and Objective-C, Drupal's `routing.yml`, SvelteKit's `$lib/…` imports in `.ts` files and a Razor page's `@model`, are unchanged.
- In C and C++, a call written from the global scope, like `::leveldb::RepairDB(…)` or `::hpb::CreateMessage(…)`, now links to the function of exactly that name, and one through a namespace alias, like protobuf's `::_pbi::…` calls, to the function in the aliased namespace. Before, these calls were dropped unless the repository also held Swift and Objective-C files, and even then a name spelled out in full, like `::google::protobuf::internal::TSanWrite(…)`, linked to nothing, while `::operator delete(…)` linked to one class's own `operator delete`. A global call never links to a function or method of the same name inside a namespace or class, such as a wrapper `open()` that calls the C library's `::open(…)`, and `::std::…` and `::testing::…` calls stay unlinked unless that library's source is part of your project. Re-index C and C++ projects after upgrading.
+209
View File
@@ -0,0 +1,209 @@
/**
* The C++ declaration scans read source a line at a time and took any line
* starting with `*` for the middle of a block comment, and any other line
* for code. So a declaration continued onto a line starting with `*` was
* missed — `Widget` above `*widget_;` — and code commented out inside
* `/* … *\/` without leading stars was read as a declaration. Block comments
* are now found by reading the file once, as raw strings are, and blanked,
* with the contents of string literals, in every scan that reads lines.
*/
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';
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-block-comments-'));
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 });
}
const lines = (...text: string[]): string => [...text, ''].join('\n');
/** What a function's or method's calls reach, as `qualifiedName @confidence`. */
function callees(cg: CodeGraph, qualifiedName: string): string[] {
const from = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName)!;
return cg.getOutgoingEdgesFrom([from.id], ['calls'])
.map((e) => `${cg.getNode(e.target)?.qualifiedName} @${(e.metadata as { confidence?: number } | undefined)?.confidence}`)
.sort();
}
/** The same, with how each call was resolved. */
function resolutions(cg: CodeGraph, qualifiedName: string): string[] {
const from = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName)!;
return cg.getOutgoingEdgesFrom([from.id], ['calls'])
.map((e) => {
const meta = e.metadata as { confidence?: number; resolvedBy?: string } | undefined;
return `${cg.getNode(e.target)?.qualifiedName} @${meta?.confidence} ${meta?.resolvedBy}`;
})
.sort();
}
const SHAPES = lines(
'class Widget {',
' public:',
' void Draw() {}',
'};',
'class Canvas {',
' public:',
' void Draw() {}',
'};',
);
describe('C++ declarations around block comments', () => {
it('reads a declaration continued on a line that starts with `*`', async () => {
const cg = await indexed({
'shapes.h': SHAPES,
'painter.h': lines(
'#include "shapes.h"',
'class Painter {',
' public:',
' void Paint() { widget_->Draw(); }',
' private:',
' Widget',
' *widget_;',
'};',
),
});
try {
expect(callees(cg, 'Painter::Paint')).toEqual(['Widget::Draw @0.9']);
} finally {
cg.close();
}
});
it('reads no declaration from code commented out in a block comment', async () => {
const cg = await indexed({
'shapes.h': SHAPES,
'run.cc': lines(
'#include "shapes.h"',
'void Run(Canvas* c) {',
' /*',
' Widget* c = nullptr;',
' */',
' c->Draw();',
'}',
'void Doc(Canvas* w) {',
' /*',
' * Widget* w = nullptr;',
' */',
' w->Draw();',
'}',
),
});
try {
expect(callees(cg, 'Run')).toEqual(['Canvas::Draw @0.9']);
expect(callees(cg, 'Doc')).toEqual(['Canvas::Draw @0.9']);
} finally {
cg.close();
}
});
it("reads no member, base, using-declaration or local from a class's or function's comments", async () => {
const cg = await indexed({
'others.h': lines(
'class HolderBase {',
' public:',
' void GetMessage() {}',
'};',
'class Old {',
' public:',
' void OnlyOld() {}',
'};',
'class Base {};',
'class Other {};',
'template <typename T> class TBase {',
' public:',
' void Insert(int x) {}',
'};',
),
'holders.h': lines(
'#include "others.h"',
'class Holder : public HolderBase {',
' public:',
' /*',
' LOCAL_VAR_ACCESSOR(Message*, Message);',
' */',
' void Run() { GetMessage(); }',
'};',
'class Derived : public Base,',
' /*',
' public Old,',
' */',
' public Other {',
'};',
'template <typename T> class TDerived : public TBase<T> {',
' public:',
' /*',
' using TBase<T>::Insert;',
' */',
' void Add() { Insert(1); }',
'};',
'class Db {',
' public:',
' void Flush() {}',
' void Run() {',
' /*',
' auto Flush = [] {};',
' */',
' Flush();',
' }',
'};',
'void UseDerived(Derived* d) { d->OnlyOld(); }',
),
});
try {
// No macro of `Holder`'s might declare `GetMessage`: C++ lookup goes
// on to the base's.
expect(resolutions(cg, 'Holder::Run')).toEqual(['HolderBase::GetMessage @0.9 instance-method']);
// `Derived`'s bases are `Base` and `Other`, the two the index links,
// neither with an `OnlyOld`: the call is not guessed onto `Old`'s.
expect(resolutions(cg, 'UseDerived')).toEqual([]);
// No using-declaration brings the dependent base's `Insert` in: only
// its name, which nothing else declares, links it.
expect(resolutions(cg, 'TDerived::Add')).toEqual(['TBase::Insert @0.9 exact-match']);
// `Flush` is no local: C++ lookup finds the member.
expect(resolutions(cg, 'Db::Run')).toEqual(['Db::Flush @0.9 instance-method']);
} finally {
cg.close();
}
});
it('reads no local from a string literal', async () => {
// protobuf's `[[deprecated("Please use SpaceUsedLong() instead")]]` and
// rocksdb's `"Unknown code(%d): "` read as declarations of a local.
const cg = await indexed({
'status.h': lines(
'class Status {',
' public:',
' int code() const { return 0; }',
' [[deprecated("Please use code() instead")]] int Code() const { return code(); }',
' void ToString() const;',
'};',
),
'status.cc': lines(
'#include "status.h"',
'void Status::ToString() const {',
' char tmp[30];',
' snprintf(tmp, sizeof(tmp), "Unknown code(%d): ", code());',
'}',
),
});
try {
expect(resolutions(cg, 'Status::Code')).toEqual(['Status::code @0.9 instance-method']);
expect(resolutions(cg, 'Status::ToString')).toEqual(['Status::code @0.9 instance-method']);
} finally {
cg.close();
}
});
});
+11 -7
View File
@@ -10,11 +10,13 @@
* found by reading the file once, as the compiler does: comments, ordinary
* string and character literals, and raw strings with their delimiters.
* An ordinary string can't span lines, so its contents are blanked on the
* line being read.
* line being read. Block comments are found in the same pass and blanked
* too: a line inside one is no code whatever it starts with, and a line
* outside one is code even when it starts with `*` (`*out = value;`).
*/
import type { ResolutionContext } from './types';
/** Column ranges, `[start, end)`, a raw string literal takes on a line (its quotes and delimiters included). */
/** Column ranges, `[start, end)`, a raw string literal or a block comment takes on a line (its quotes, delimiters and comment markers included). */
type Ranges = Array<[number, number]>;
const RAW_STRING_LINES = new WeakMap<ResolutionContext, Map<string, Map<number, Ranges>>>();
@@ -27,17 +29,17 @@ export function clearCppRawStringMemos(context: ResolutionContext): void {
const isWordChar = (c: string | undefined): boolean => c !== undefined && /[A-Za-z0-9_$]/.test(c);
/**
* The 1-based lines of a C or C++ file that raw string literals cover, each
* with the columns they take. Empty for a file with none.
* The 1-based lines of a C or C++ file that raw string literals or block
* comments cover, each with the columns they take. Empty for a file with none.
*/
export function cppRawStringLines(file: string, context: ResolutionContext): Map<number, Ranges> {
export function cppNonCodeLines(file: string, context: ResolutionContext): Map<number, Ranges> {
let memo = RAW_STRING_LINES.get(context);
if (!memo) RAW_STRING_LINES.set(context, (memo = new Map()));
const hit = memo.get(file);
if (hit) return hit;
const lines = new Map<number, Ranges>();
const text = context.readFile(file);
if (text && text.includes('R"')) {
if (text && (text.includes('R"') || text.includes('/*'))) {
let line = 1;
let column = 0;
const n = text.length;
@@ -74,7 +76,9 @@ export function cppRawStringLines(file: string, context: ResolutionContext): Map
advance(end < 0 ? n : end);
} else if (c === '/' && next === '*') {
const end = text.indexOf('*/', i + 2);
advance(end < 0 ? n : end + 2);
const to = end < 0 ? n : end + 2;
mark(i, line, column, to);
advance(to);
} else if (c === '"') {
// `R"`, `u8R"`, `uR"`, `UR"`, `LR"`, not part of a longer name.
const prefix = /(?:u8|[uUL])?R$/.exec(text.slice(Math.max(0, i - 3), i));
+33 -39
View File
@@ -16,7 +16,7 @@ import { isDartLocallyBound } from './dart-local-scope';
import { breakVbTie, isVbMemberInScope, isVbNestedTypeInScope, isVbTypeQualifiedBy, matchVbTypedCall, preferVbProject, sameVbProject } from './vbnet-receivers';
import { cppAliasedClass, cppAliasedTypeName, cppAliasedTypeWritten, cppDeclaresLocalAlias, cppParentScope, cppScopesWithin, cppTemplateArguments, cppTemplateParameters, cppTypeSegments, isCppPointerType, resolveCppAliasedType, stripCppTemplateArguments, type CppAliasedType } from './cpp-type-aliases';
import { clearCppNamespaceMemos, cppMacroNamespaceFrames, cppMacroNamespaceHeads, cppNamespaceAliases } from './cpp-namespaces';
import { clearCppRawStringMemos, cppLiteralsBlanked, cppRawStringLines } from './cpp-raw-strings';
import { clearCppRawStringMemos, cppLiteralsBlanked, cppNonCodeLines } from './cpp-raw-strings';
import { cppClassWritten, type CppNameSite } from './cpp-supertypes';
import { cppIncludedFile, cppIncluders } from './cpp-includers';
import { isTestPath } from '../search/query-utils';
@@ -9433,7 +9433,7 @@ function cppDataMemberIn(decl: Node, name: string, context: ResolutionContext):
const declarator = buildDeclaratorRegex(escaped);
const end = Math.min(decl.endLine ?? decl.startLine, lines.length);
const nested = cppNestedSpans(decl, end, context);
const rawLines = cppRawStringLines(decl.filePath, context);
const rawLines = cppNonCodeLines(decl.filePath, context);
const depths = cppClassParenDepths(decl, lines, end, rawLines, context);
for (let l = decl.startLine; l <= end; l++) {
if (!word.test(lines[l - 1]!) || nested.some(([start, stop]) => l >= start && l <= stop)) continue;
@@ -9455,7 +9455,6 @@ function cppDataMemberIn(decl: Node, name: string, context: ResolutionContext):
}
if (!m || code!.slice(m.index + m[0].length).startsWith('(')) continue;
let depth = depths[from - decl.startLine]!;
if (depth < 0) continue;
for (const c of code!.slice(from === decl.startLine ? decl.startColumn : 0, m.index)) {
if (c === '(') depth++;
else if (c === ')') depth--;
@@ -9473,16 +9472,15 @@ function cppDataMemberIn(decl: Node, name: string, context: ResolutionContext):
/**
* How many parentheses are open where each line of the C++ class
* declaration `decl`, which ends on line `end`, begins, or -1 for one that
* begins inside a block comment. Comments are followed across lines, not
* guessed as cppCodeOf does: it takes a code line that starts with a
* dereference, `*first_key, scratch);`, for one.
* declaration `decl`, which ends on line `end`, begins. Its block comments
* and literals are blanked first (cppNonCodeLines): a parenthesis in one
* opens nothing.
*/
function cppClassParenDepths(
decl: Node,
lines: readonly string[],
end: number,
rawLines: ReturnType<typeof cppRawStringLines>,
rawLines: ReturnType<typeof cppNonCodeLines>,
context: ResolutionContext,
): number[] {
let memo = CPP_CLASS_PAREN_DEPTHS.get(context);
@@ -9491,27 +9489,14 @@ function cppClassParenDepths(
if (!depths) {
depths = [];
let depth = 0;
let comment = false;
for (let l = decl.startLine; l <= end; l++) {
depths.push(comment ? -1 : depth);
depths.push(depth);
const text = cppLiteralsBlanked(lines[l - 1] ?? '', l, rawLines);
for (let i = l === decl.startLine ? decl.startColumn : 0; i < text.length; i++) {
const c = text[i]!;
if (comment) {
if (c === '*' && text[i + 1] === '/') {
comment = false;
i++;
}
} else if (c === '/' && text[i + 1] === '/') {
break;
} else if (c === '/' && text[i + 1] === '*') {
comment = true;
i++;
} else if (c === '(') {
depth++;
} else if (c === ')' && depth > 0) {
depth--;
}
if (c === '/' && text[i + 1] === '/') break;
else if (c === '(') depth++;
else if (c === ')' && depth > 0) depth--;
}
}
memo.set(decl.id, depths);
@@ -10794,9 +10779,11 @@ function cppMemberMacroArguments(decl: Node, context: ResolutionContext): string
if (hit) return hit;
const words: string[] = [];
const lines = context.getFileLines?.(decl.filePath) ?? context.readFile(decl.filePath)?.split(/\r?\n/) ?? [];
const nonCode = cppNonCodeLines(decl.filePath, context);
let depth = 0;
for (let i = decl.startLine - 1; i < Math.min(lines.length, decl.endLine); i++) {
const code = cppCodeOf(i === decl.startLine - 1 ? lines[i]!.slice(decl.startColumn) : lines[i]!) ?? '';
const blanked = cppLiteralsBlanked(lines[i]!, i + 1, nonCode);
const code = cppCodeOf(i === decl.startLine - 1 ? blanked.slice(decl.startColumn) : blanked) ?? '';
const call = depth === 1 ? /^\s*[A-Z][A-Z0-9_]*\s*\(([^;{}]*)\)\s*;?\s*$/.exec(code) : null;
if (call) words.push(...call[1]!.split(/\W+/).filter((w) => w.length >= 3));
for (const c of code) {
@@ -10822,7 +10809,8 @@ function cppBaseSpecifiers(decl: Node, context: ResolutionContext): string[] {
if (hit) return hit;
const specs: string[] = [];
const lines = context.getFileLines?.(decl.filePath) ?? context.readFile(decl.filePath)?.split(/\r?\n/);
const head = lines?.slice(decl.startLine - 1, decl.startLine + 11) ?? [];
const nonCode = cppNonCodeLines(decl.filePath, context);
const head = (lines?.slice(decl.startLine - 1, decl.startLine + 11) ?? []).map((l, k) => cppLiteralsBlanked(l, decl.startLine + k, nonCode));
if (head.length > 0) {
head[0] = head[0]!.slice(decl.startColumn);
const text = head.map((l) => cppCodeOf(l) ?? '').join('\n');
@@ -10853,7 +10841,9 @@ function cppUsingDeclares(decl: Node, base: Node, name: string, context: Resolut
if (!usings) {
usings = [];
const lines = context.getFileLines?.(decl.filePath) ?? context.readFile(decl.filePath)?.split(/\r?\n/) ?? [];
const body = lines.slice(decl.startLine - 1, decl.endLine ?? decl.startLine).map((l) => cppCodeOf(l) ?? '').join('\n');
const nonCode = cppNonCodeLines(decl.filePath, context);
const body = lines.slice(decl.startLine - 1, decl.endLine ?? decl.startLine)
.map((l, k) => cppCodeOf(cppLiteralsBlanked(l, decl.startLine + k, nonCode)) ?? '').join('\n');
for (const m of body.matchAll(/\busing\s+(?!namespace\b)((?:::\s*)?[A-Za-z_][\w:]*(?:\s*<[^;{}]*>)?)\s*::\s*([A-Za-z_]\w*)\s*;/g)) {
usings.push([cppLastSegment(stripCppTemplateArguments(m[1]!).trim()), m[2]!]);
}
@@ -10889,9 +10879,10 @@ function isCppLocalName(name: string, caller: Node, ref: UnresolvedRef, context:
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const word = new RegExp(`\\b${escaped}\\b`);
const declarator = buildDeclaratorRegex(escaped);
const nonCode = cppNonCodeLines(caller.filePath, context);
for (let i = Math.max(0, caller.startLine - 1); i < Math.min(lines.length, ref.line); i++) {
if (!word.test(lines[i]!)) continue;
const code = cppCodeOf(lines[i]!);
const code = cppCodeOf(cppLiteralsBlanked(lines[i]!, i + 1, nonCode));
if (!code || !word.test(code)) continue;
if (cppRebindsReceiver(code, escaped)) return true;
// `std::function<void()> done;` reads as a declaration once its template arguments are gone.
@@ -10905,13 +10896,16 @@ function isCppLocalName(name: string, caller: Node, ref: UnresolvedRef, context:
}
/**
* A C++ source line as code, for reading declarations: null for a line of a
* comment (`// …`, ` * …`), else the line with its comments blanked, columns
* kept. Receiver inference read leveldb's `// … non-null imm_` as a
* declaration of `imm_` with the type `null`.
* A C++ source line as code, for reading declarations: null for a `// …`
* line, else the line with its comments blanked, columns kept. Receiver
* inference read leveldb's `// … non-null imm_` as a declaration of `imm_`
* with the type `null`. A line inside a block comment is blank by the time
* it gets here (cppLiteralsBlanked, from cppNonCodeLines): a line starting
* with `*` is not one by that alone, as `*widget_;` continuing a declaration
* is not.
*/
function cppCodeOf(line: string): string | null {
if (/^\s*(?:\/\/|\*)/.test(line)) return null;
if (/^\s*\/\//.test(line)) return null;
let code = line.replace(/\/\*.*?\*\//g, (c) => ' '.repeat(c.length));
// A block comment opening here runs past the line.
const open = code.indexOf('/*');
@@ -10969,7 +10963,7 @@ function inferCppReceiverType(
// A declaration in a string literal is text: protobuf's upb generator
// writes `upb_MiniTableField field = $field_init$;` in the raw string it
// emits, above calls on its own `upb::FieldDefPtr field` parameter.
const rawLines = cppRawStringLines(ref.filePath, context);
const rawLines = cppNonCodeLines(ref.filePath, context);
// Past the calling function's first line, C++ looks the name up among the
// members of the function's class before anything outside it.
const caller = context.getNodeById?.(ref.fromNodeId);
@@ -11072,7 +11066,7 @@ function inferCppReceiverType(
? context.getFileLines(headerPath)
: (context.readFile(headerPath)?.split(/\r?\n/) ?? null);
if (!headerLines) continue;
const headerRawLines = cppRawStringLines(headerPath, context);
const headerRawLines = cppNonCodeLines(headerPath, context);
for (let i = 0; i < headerLines.length; i++) {
const text = headerLines[i]!;
@@ -11242,7 +11236,7 @@ function cppAutoInitializerStatement(
lines: readonly string[],
index: number,
receiverName: string,
rawLines: ReturnType<typeof cppRawStringLines>,
rawLines: ReturnType<typeof cppNonCodeLines>,
): string | null {
const escaped = receiverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const start = new RegExp(`\\b${escaped}\\b\\s*=(?!=)`).exec(line);
@@ -11257,7 +11251,7 @@ function cppStatementFrom(
text: string,
lines: readonly string[],
index: number,
rawLines: ReturnType<typeof cppRawStringLines>,
rawLines: ReturnType<typeof cppNonCodeLines>,
): string | null {
for (let next = index + 1; ; next++) {
const end = cppStatementEnd(text);
@@ -11289,7 +11283,7 @@ function cppStructuredBindingType(
callLineIndex: number,
ref: UnresolvedRef,
context: ResolutionContext,
rawLines: ReturnType<typeof cppRawStringLines>,
rawLines: ReturnType<typeof cppNonCodeLines>,
): CppWrittenType | null {
if (!line.includes('[')) return null;
for (const m of line.matchAll(CPP_STRUCTURED_BINDING)) {