diff --git a/CHANGELOG.md b/CHANGELOG.md index 92cf17b48c..b192a2baf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -183,6 +183,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In C++, a function is now named by its declarator, never by something written in the clauses after its parameters. nlohmann/json's constructor `basic_json(CompatibleType&& val) noexcept(noexcept(JSONSerializer::to_json(…)))` was missing from the index, replaced by a method `JSONSerializer::to_json` that doesn't exist and that other `JSONSerializer::to_json(…)` calls linked to; a method declared with `throw(std::bad_alloc)` was indexed as `std::bad_alloc`, and a function with a trailing `requires std::integral` as `std::integral`. Re-index C++ projects after upgrading. - In C and C++, a header or source file named like a test, such as protocolbuffers/protobuf's `conformance_test.h` and `test_runner.h`, now counts as part of the code that `#include`s it, directly or through other headers. Before, calls from that code into it were dropped, so protobuf's conformance suites' `suite_.ReportFailure(…)` and `RunValidInputTest(…)` calls, and its unit tests' calls to the shared `TestUtil` helpers, linked to nothing. A test file that only tests include is still kept out of reach of the rest of your code. Re-index C and C++ projects after upgrading. - In C++, a class, struct or method now keeps the namespaces and the class it is declared in when the parser misreads something earlier in the file, such as an unknown macro in front of a member or in a class header. Before, everything after the misread code could lose them, or land inside a class it isn't in: in protocolbuffers/protobuf, `FieldDescriptor` was indexed without `google::protobuf::`, and in RocksDB, the `Opts` struct declared inside a cache table class was indexed outside it, so finding a class's base classes and the methods called on its objects fell back to guessing by name. Methods that had come loose from their class are its members again, and classes the misread code had hidden, like leveldb's POSIX file and environment classes, are now indexed. Re-index C++ projects after upgrading. +- In C, an `#if` group whose branches aren't whole statements, like an `else if (…) { … }` arm kept behind `#ifdef`, an `if (…)` whose body comes after the `#endif`, or a function's first line that differs per build, no longer turns into functions named `if` that the rest of the function, or of the file, is filed under. Such a group is now read with all its branches in a row when that is still C, and as one build of the file when it isn't, so functions that were missing or nested inside another, like ST's `LL_SetFlashLatency` or tre's `tre_parse`, are indexed with their calls. A block macro written in capitals, like `ATOMIC_BLOCK(…) { … }`, is now read like a lowercase iterator macro. Re-index C projects after upgrading. Thanks @danusha2345. - In C++, a call on a variable, parameter or member declared as a standard-library or other outside type with a lowercase name, like `std::string`, `std::vector`, `std::set` or `absl::flat_hash_set`, no longer links to a project method that only shares the method's name, even one of a project class in another namespace with the type's name, like your own `util::flat_hash_set`. A variable declared outside the calling function, like another function's or a file's `std::string text`, no longer links its calls to such a class either. In google/leveldb, `saved_key_.clear()` on a `std::string saved_key_;` reached `Slice::clear`, and in protocolbuffers/protobuf, `proto.append(…)` on a `std::string` reached an unrelated `LeftoverBuffer::append`. The same goes for `->` through a raw pointer like `std::string* out`, while `->` through a smart pointer, iterator or optional still links to the type it holds. Comments also no longer pass for a variable's declaration, so more calls now link to the method of the type the variable is really declared with. A call on a member reached through other members, like facebook/rocksdb's `options.cf_paths.emplace_back(…)` on a `std::vector`, `props->rep.db_id.size()` on a `std::string` or `bbto.filter_policy.reset(…)` on a `std::shared_ptr`, no longer links to a project method of that name either: those had reached `autovector::emplace_back`, `BlobContents::size` and `FSWritableFilePtr::reset`. The same goes for a member of an outside type like `absl::Status`. Re-index C++ projects after upgrading. - In C++, a class written with attribute macros the parser can't read now keeps its members. That covers several macros between `class` and the class name, like protocolbuffers/protobuf's generated `class PROTOBUF_EXPORT PROTOBUF_FUTURE_ADD_EARLY_WARN_UNUSED Any final`, a macro with arguments there, like RocksDB's `struct ALIGN_AS(64U) HandleImpl`, or one before a template specialization, and macros on a member: in front of it (`PROTOBUF_FUTURE_ADD_EARLY_NODISCARD`), between a pointer and its name (`PROTOBUF_NONNULL`), after its parameters or its name (`ABSL_ATTRIBUTE_LIFETIME_BOUND`, google/leveldb's `LOCKS_EXCLUDED(mu_)` and `GUARDED_BY(mu_)`), or alone on a line above a comment ({fmt}'s `FMT_BEGIN_EXPORT`). Before, protobuf's generated message classes were missing and their members, like `GetDescriptor`, `GetReflection` and `unknown_fields`, were indexed as functions outside any class, and leveldb's annotated methods were indexed under the annotation's name, so calls to them linked to nothing. Re-index C++ projects after upgrading. - In C++, a call on a range-based `for` loop's variable, like `suite->SetVerbose(verbose)` inside `for (ConformanceTestSuite *suite : suites)`, now links to the method of the type the loop declares. Before, the loop's declaration was never read, so the call was matched by the variable's name alone: in RocksDB, `log->file()` on a `log::Writer` went to `BlobLogWriter::file`, and a loop variable named like a member of its class was taken for that member. The variable counts only inside its loop. Re-index C++ projects after upgrading. diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index a481cb8a45..2814a4a2c3 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -13796,6 +13796,246 @@ describe('C/C++ kernel-port preParse blanks (R7a)', () => { } }); + it('blankCUnbalancedConditionalBranches opens a damaged #if group, one branch kept only when they do not balance, offsets kept', async () => { + const { blankCUnbalancedConditionalBranches } = await import('../src/extraction/languages/c-cpp'); + const src = [ + 'void f(int id)', + '{', + ' if (id == 1) {', + ' a(id);', + ' }', + '#if defined(USE_V) || \\', + ' defined(USE_W)', + ' else if (id == 2) { // "{" in a comment', + ' b(id);', + ' }', + '#else', + ' else if (id == 3) {', + ' c(id);', + ' }', + '#endif', + '#ifdef USE_X', + ' d(id);', + '#else', + ' e(id);', + '#endif', + '}', + '#if 0', + 'void dead(void) {', + '#else', + 'void live(void) {', + '#endif', + '}', + ].join('\n'); + const out = blankCUnbalancedConditionalBranches(src); + expect(out.length).toBe(src.length); + expect(out.split('\n').length).toBe(src.split('\n').length); + const lines = out.split('\n'); + // The `else`-led group's branches each balance, so both stay, read in a + // row (`else if (id == 2) {…} else if (id == 3) {…}` is still C); only + // its directive lines, continuation included, become spaces. + expect(lines[5]).toBe(' '.repeat(src.split('\n')[5]!.length)); + expect(lines[6]).toBe(' '.repeat(src.split('\n')[6]!.length)); + expect(lines[7]).toBe(' else if (id == 2) { // "{" in a comment'); + expect(lines[8]).toBe(' b(id);'); + expect(lines[10]).toBe(' '); + expect(lines[11]).toBe(' else if (id == 3) {'); + expect(lines[12]).toBe(' c(id);'); + expect(lines[14]).toBe(' '); + // A balanced group is untouched, directives included. + expect(lines[15]).toBe('#ifdef USE_X'); + expect(lines[16]).toBe(' d(id);'); + expect(lines[18]).toBe(' e(id);'); + expect(lines[19]).toBe('#endif'); + // `#if 0` keeps the live branch instead. + expect(out).not.toContain('dead'); + expect(lines[24]).toBe('void live(void) {'); + // `#if 1` is live as written: its first branch stays, not its `#else`. + const on = blankCUnbalancedConditionalBranches( + ['void g(int id) {', ' if (id) { a(); }', '#if 1', ' else if (id == 2) { live(); }', '#else', ' else if (id == 3) { dead(); }', '#endif', '}'].join('\n') + ); + expect(on).toContain('else if (id == 2) { live(); }'); + expect(on).not.toContain('dead'); + // Arms that close the block above and open their own (`} else if (…) {`) + // balance too: under a flag, both builds' arms stay (betaflight's + // `#if ENABLE_RESCUE_PLAN` in failsafe.c), so neither one's calls are lost. + const arms = blankCUnbalancedConditionalBranches( + ['void f(int a) {', ' if (a == 1) {', ' one();', '#if ENABLE_X', ' } else if (a == 2) {', ' two();', '#else', ' } else if (a == 3) {', ' three();', '#endif', ' }', '}'].join('\n') + ).split('\n'); + expect(arms[3]).toBe(' '.repeat('#if ENABLE_X'.length)); + expect(arms[5]).toBe(' two();'); + expect(arms[8]).toBe(' three();'); + // A brace inside a block comment is not code. Lua's loadlib.c opens a + // banner with `** {====` inside a multi-line comment and closes it with + // `/* }==== */`: the group is whole, and every platform's functions stay. + const lua = [ + '#if defined(LUA_DL_DLOPEN)', + 'static void *ll_load (const char *path) { return dlopen(path); }', + '#elif defined(LUA_DL_DLL)', + '/*', + '** {======================================================', + '** This is an implementation of loadlib for Windows', + '** =======================================================', + '*/', + 'static void *ll_load (const char *path) { return LoadLibraryA(path); }', + '/* }====================================================== */', + '#else', + 'static void *ll_load (const char *path) { return NULL; }', + '#endif', + ].join('\n'); + expect(blankCUnbalancedConditionalBranches(lua)).toBe(lua); + // A `\` ending a line inside a block comment still continues the + // `#define` (jemalloc's tsd.h): the macro body is not code of the group. + const guarded = [ + '#ifndef GUARD_H', + '#define GUARD_H', + '#define O(n)\t\t\t\t\\', + 'static int n##_get(void) {\t\t\\', + '\t/*\t\t\t\t\t\\', + '\t * a comment inside the body\t\\', + '\t */\t\t\t\t\t\\', + '\treturn 0;\t\t\t\t\\', + '}', + 'O(a)', + '#endif', + ].join('\n'); + expect(blankCUnbalancedConditionalBranches(guarded)).toBe(guarded); + // No conditional group at all: identity. + const plain = 'int g(void) {\n#define X 1\n return X;\n}\n'; + expect(blankCUnbalancedConditionalBranches(plain)).toBe(plain); + }); + + it('blankCUnbalancedConditionalBranches: two function heads under #ifdef/#else keep the #else head; expression fragments keep their only branch', async () => { + const { blankCUnbalancedConditionalBranches } = await import('../src/extraction/languages/c-cpp'); + // jq's main.c: `umain` for WIN32, `main` otherwise — `main` must survive. + const heads = [ + '#ifdef WIN32', + 'int umain(int argc, char* argv[]) {', + '#else /*}*/', + 'int main(int argc, char* argv[]) {', + '#endif', + ' return run(argc, argv);', + '}', + ].join('\n'); + const h = blankCUnbalancedConditionalBranches(heads).split('\n'); + expect(h[1]).toBe(' '.repeat('int umain(int argc, char* argv[]) {'.length)); + expect(h[3]).toBe('int main(int argc, char* argv[]) {'); + expect(h[0]).toBe(' '); + // `#ifndef` is the inverse: its first branch is the default build. + const inv = heads.replace('#ifdef WIN32', '#ifndef POSIX'); + expect(blankCUnbalancedConditionalBranches(inv).split('\n')[1]).toBe('int umain(int argc, char* argv[]) {'); + // A directive right under the kept branch's own is a line of that branch: + // only the `#if`/`#else` line itself (and its `\` continuations) goes. + const withIncludes = [ + '#if defined(FEATURE) && \\', + ' defined(OTHER)', + '#include "feature.h"', + 'int f(int x) {', + '#else', + '#include "plain.h"', + 'int g(int x) {', + '#endif', + ' return x;', + '}', + ]; + const inc = blankCUnbalancedConditionalBranches(withIncludes.join('\n')).split('\n'); + expect(inc[1]).toBe(' '.repeat(withIncludes[1]!.length)); + expect(inc[2]).toBe(' '.repeat(withIncludes[2]!.length)); + expect(inc[5]).toBe('#include "plain.h"'); + expect(inc[6]).toBe('int g(int x) {'); + // A branch that is a fragment of an `if (…)` condition (STM32 HAL), or + // that opens with `} else if` (betaflight cli.c): directives blanked, the + // only branch kept, so the statement parses as written for that build. + const frag = [ + 'int check(op_t *h1, op_t *h4)', + '{', + ' if ((h1 == NULL)', + '#if defined(STM32G474xx)', + ' || (h4 == NULL)', + '#endif', + ' )', + ' {', + ' return 1;', + ' }', + ' if (', + '#if !defined(USE_FLASH)', + ' isEmpty(h1) ||', + '#endif', + ' strncasecmp(h1, "rom", 3) == 0) {', + ' reboot(1);', + '#if defined(USE_FLASH)', + ' } else if (isEmpty(h1)) {', + ' reboot(2);', + '#endif', + ' } else {', + ' reboot(0);', + ' }', + ' return 0;', + '}', + ].join('\n'); + const f = blankCUnbalancedConditionalBranches(frag).split('\n'); + expect(f[3]).toBe(' '.repeat('#if defined(STM32G474xx)'.length)); + expect(f[4]).toBe(' || (h4 == NULL)'); + expect(f[5]).toBe(' '); + expect(f[12]).toBe(' isEmpty(h1) ||'); + expect(f[17]).toBe(' } else if (isEmpty(h1)) {'); + expect(f[16]).toBe(' '.repeat('#if defined(USE_FLASH)'.length)); + }); + + it('a #if branch beginning with `else` no longer files the rest of the file under the enclosing function', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-c-ifelse-')); + try { + fs.writeFileSync( + path.join(dir, 'current.c'), + [ + 'void currentMeterRead(int id, meter_t *meter)', + '{', + ' if (id == 1) {', + ' adcRead(meter);', + ' }', + '#ifdef USE_VIRTUAL', + ' else if (id == 2) {', + ' virtualRead(meter);', + ' }', + '#endif', + ' else {', + ' resetMeter(meter);', + ' }', + '}', + '', + 'static bool markBusy(void)', + '{', + ' ATOMIC_BLOCK(NVIC_PRIO_MAX) {', + ' busy = true;', + ' }', + ' return true;', + '}', + '', + 'int after(void)', + '{', + ' return 1;', + '}', + '', + ].join('\n') + ); + const cg = await CodeGraph.init(dir, { index: true }); + try { + const fns = cg.getNodesByKind('function').filter((n) => n.filePath === 'current.c'); + const byName = Object.fromEntries(fns.map((n) => [n.name, n])); + expect(Object.keys(byName).sort()).toEqual(['after', 'currentMeterRead', 'markBusy']); + expect(byName.currentMeterRead!.endLine).toBe(14); + expect(byName.markBusy!.qualifiedName).toBe('markBusy'); + expect(byName.markBusy!.endLine).toBe(22); + expect(byName.after!.qualifiedName).toBe('after'); + expect(byName.after!.startLine).toBe(24); + } finally { + cg.close(); + } + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + it('blankCStatementMacroCalls blanks indented iterator macros, keeps the block', async () => { const { blankCStatementMacroCalls } = await import('../src/extraction/languages/c-cpp'); const src = [ diff --git a/src/extraction/languages/c-cpp.ts b/src/extraction/languages/c-cpp.ts index 52b3339345..7aeca660c7 100644 --- a/src/extraction/languages/c-cpp.ts +++ b/src/extraction/languages/c-cpp.ts @@ -1921,7 +1921,10 @@ export function blankCStatementMacroCalls(source: string): string { const content = (l: string): string => l.replace(/\r$/, '').trim(); for (let i = 0; i < lines.length; i++) { const line = lines[i] as string; - const m = /^[ \t]+([a-z_][a-z0-9_]*)[ \t]*\(/.exec(line); + // Lowercase iterator macros, or an all-caps block macro + // (`ATOMIC_BLOCK(NVIC_PRIO_MAX) {`, `PG_FOREACH(reg) {` — betaflight); + // a PascalCase name is left alone (a constructor, if the file is C++). + const m = /^[ \t]+([a-z_][a-z0-9_]*|[A-Z_][A-Z0-9_]*)[ \t]*\(/.exec(line); if (!m || C_STMT_MACRO_KEYWORDS.has(m[1] as string)) continue; const open = line.indexOf('(', m[0].length - 1); let depth = 0; @@ -2544,6 +2547,160 @@ export function blankCDesignatedMacroArgs(source: string): string { return rawStrings.restore(out.join('')); } +/** + * Open up a `#if … #elif … #else … #endif` group whose branches are not + * self-contained statements. tree-sitter-c keeps EVERY branch of a + * conditional group and parses each as a run of block items, so a group is + * only harmless when each branch is complete on its own. Three shapes in + * real C are not, and each ends in a phantom function (K&R + * `if(cond) { … }` reads as an implicit-int function definition named `if`, + * and the extractor then files it — and every symbol after it — under the + * enclosing function; 265 on a betaflight tree, #1729 follow-up): + * + * - a branch that BEGINS with `else` — `} #ifdef X else if (…) { … } #endif` + * (a conditional arm of an if-chain); + * - a branch that ENDS with a bare `if (…)` / `else if (…)` header whose + * body sits after the `#endif` (the ST HAL's per-device latency tables); + * - a branch whose braces do not balance — a function signature or a `{` + * that differs per configuration, with the body shared. + * + * The group's directive lines are blanked to spaces (newlines and `\r` kept, + * so offsets survive). When every branch's braces and parentheses balance, + * all branches stay, read in a row. Otherwise one branch stays verbatim and + * the rest become spaces too, which is what the preprocessor would hand a + * compiler for that configuration: the `#else` of a feature test, the first + * branch of `#ifndef`, the live one of `#if 0` or `#if 1`. Groups whose + * branches are whole statements — the vast majority — are untouched. This + * pass edits directive lines on purpose, so it runs AFTER + * `restoreDirectiveLines` (like the named-variadic pass). + * Groups are handled innermost-first; an inner group inside a blanked branch + * simply disappears with it. + */ +const C_COND_OPEN_RE = /^[ \t]*#[ \t]*(if|ifdef|ifndef)\b[ \t]*(.*)$/; +const C_COND_NEXT_RE = /^[ \t]*#[ \t]*(elif|else)\b/; +const C_COND_ENDIF_RE = /^[ \t]*#[ \t]*endif\b/; +export function blankCUnbalancedConditionalBranches(source: string): string { + if (source.indexOf('#') === -1) return source; + const lines = source.split('\n'); + // The same lines with comments and string / char literals as spaces: a `{` + // in a block comment over several lines (Lua's `** {======` banners) or a + // `#` line inside one is not code. Blanked along with `lines`, so an outer + // group reads an inner one as collapsed. + const masked = maskCppCommentsAndLiterals(source).split('\n'); + const stripCr = (l: string): string => (l.endsWith('\r') ? l.slice(0, -1) : l); + // Code content of a line. + const code = (k: number): string => stripCr(masked[k] as string).trim(); + const blank = (l: string): string => l.replace(/[^\r]/g, ' '); + const blankLine = (k: number): void => { + lines[k] = blank(lines[k] as string); + masked[k] = blank(masked[k] as string); + }; + // Directive line indices of each open group, innermost last. + // `positive`: the group opens on a feature test (`#ifdef X`, `#if defined(X)`, + // `#if X`), whose `#else` branch is the default build. `elseAt`: index into + // marks of that `#else`, if any. `literal`: `#if 0` or `#if 1`. + const stack: Array<{ marks: number[]; dead: boolean; literal: boolean; positive: boolean; elseAt: number }> = []; + const directive: boolean[] = []; + // Lines that continue the directive above them with a `\`. + const continued: boolean[] = []; + let changed = false; + let continuation = false; + for (let i = 0; i < lines.length; i++) { + const line = stripCr(masked[i] as string); + const isDirective: boolean = continuation || /^[ \t]*#/.test(line); + directive[i] = isDirective; + continued[i] = continuation; + const wasContinuation = continuation; + // Read on the raw line: a `\` that ends a line splices it even inside a + // comment (a multi-line comment in a `#define` body), and masking would + // hide it. + continuation = isDirective && /\\\s*$/.test(stripCr(lines[i] as string)); + if (!isDirective || wasContinuation) continue; + let m: RegExpExecArray | null; + if ((m = C_COND_OPEN_RE.exec(line))) { + const cond = (m[2] as string).trim(); + const dead = m[1] === 'if' && /^0\b/.test(cond); + // `#if 1` is the other way round from `#if 0`: its first branch is the live one. + const live = m[1] === 'if' && /^1\b/.test(cond); + const positive = m[1] === 'ifdef' || (m[1] === 'if' && !dead && !live && !/^!/.test(cond)); + stack.push({ marks: [i], dead, literal: dead || live, positive, elseAt: -1 }); + continue; + } + const top = stack[stack.length - 1]; + if (!top) continue; + if (C_COND_NEXT_RE.test(line)) { + if (/^[ \t]*#[ \t]*else\b/.test(line)) top.elseAt = top.marks.length; + top.marks.push(i); + continue; + } + if (!C_COND_ENDIF_RE.test(line)) continue; + stack.pop(); + top.marks.push(i); + // Which branch survives: never `#if 0`'s; for a feature test with an + // `#else`, the `#else` branch — the build without the feature is the one + // that defines `main`, not `umain` (jq's `#ifdef WIN32`); otherwise the + // first. A kept branch is verbatim; the others become spaces. + const keep = top.dead ? 1 : top.positive && top.elseAt > 0 ? top.elseAt : 0; + if (keep >= top.marks.length - 1) continue; // `#if 0` with nothing to keep + let damaged = false; + // Whether each branch's braces and parentheses balance on their own. + let balanced = true; + for (let b = 0; b < top.marks.length - 1; b++) { + const body: string[] = []; + for (let k = (top.marks[b] as number) + 1; k < (top.marks[b + 1] as number); k++) { + if (directive[k]) continue; // a `\`-continued directive line + const c = code(k); + if (c) body.push(c); + } + if (body.length === 0) continue; + const first = body[0] as string; + const last = body[body.length - 1] as string; + let depth = 0; + let parens = 0; + let dips = false; + for (const l of body) { + for (const ch of l) { + depth += ch === '{' ? 1 : ch === '}' ? -1 : 0; + parens += ch === '(' ? 1 : ch === ')' ? -1 : 0; + if (depth < 0) dips = true; // closes a block it did not open (`} else if (…) {`) + } + } + if (depth !== 0 || parens !== 0) balanced = false; + damaged ||= + depth !== 0 || + dips || + /^else\b/.test(first) || + // A fragment of an expression: `|| (h4 == NULL)` inside an `if (…)`, + // `isEmpty(s) ||` before the rest of the condition. + /^(\|\||&&|,|\)|:|\?)/.test(first) || + /(\|\||&&|,|\(|\?|:)$/.test(last) || + (/^(else[ \t]+)?if[ \t]*\(/.test(last) && /\)$/.test(last)); + } + if (!damaged) continue; + // A group on a flag whose branches each balance keeps them all: read in + // a row they are still C (`} else if (a) {…} else if (b) {…}`, list + // entries, `|| x || y`), and the parser needed only the directive lines + // gone. Dropping one would lose real code of a build the project may + // well use (betaflight's ENABLE_RESCUE_PLAN arms). A branch that opens or + // closes more than it should, or a literal `#if 0` / `#if 1`, keeps one. + const keepAll = balanced && !top.literal; + for (let b = 0; b < top.marks.length - 1; b++) { + const from = top.marks[b] as number; + const to = top.marks[b + 1] as number; + // The directive line (with its `\`-continuations), then — unless this + // is the kept branch — the branch body. + // Only its own `\` continuations: a `#define` or `#include` right under + // it is a line of the branch, kept with a kept branch. + for (let k = from; k < to && (k === from || continued[k]); k++) blankLine(k); + if (b === keep || keepAll) continue; + for (let k = from + 1; k < to; k++) blankLine(k); + } + blankLine(i); + changed = true; + } + return changed ? lines.join('\n') : source; +} + function preParseCSource(source: string): string { const rawStrings = maskCppRawStrings(source); source = rewriteFoundationEnumMacros(rawStrings.source); @@ -2571,9 +2728,16 @@ function preParseCSource(source: string): string { ) ); if (looksLikeCudaSource(blanked)) blanked = blankCudaConstructs(blanked); - // The named-variadic `#define` pass runs AFTER the directive restore — it - // deliberately edits directive lines (see its doc comment). - return rawStrings.restore(blankCNamedVariadicDefineDots(restoreDirectiveLines(source, blanked))); + // The named-variadic `#define` pass and the conditional-group collapse run + // AFTER the directive restore — they deliberately edit directive lines (see + // their doc comments) — and BEFORE the raw-string restore: both are + // length-preserving, and the collapse must count braces with raw-string + // bodies still masked. + return rawStrings.restore( + blankCUnbalancedConditionalBranches( + blankCNamedVariadicDefineDots(restoreDirectiveLines(source, blanked)) + ) + ); } /** A constructor prototype carries defaults even when its body is in another file. */