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
co-authored by Claude Opus 5.5
parent d94a92ea89
commit d4b9c2b4e9
4 changed files with 254 additions and 46 deletions
+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)) {