From 32105a009c7578598a1281bd7cdd8d0810b51372 Mon Sep 17 00:00:00 2001 From: danusha2345 Date: Mon, 7 Sep 2026 10:35:00 +0300 Subject: [PATCH 1/2] fix(extraction): blank a C macro call's designated-initializer arguments before parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tree-sitter-c has no rule for `.field = value` as a call argument. A statement-level `MACRO(a, b, .x = …, .y = …, …);` parses with an ERROR per argument, and past roughly a hundred of them the grammar's error recovery gives up on the enclosing function: the function_definition runs to the end of the file, the function after it produces no node at all, and every later function is nested under the first (#1729). betaflight resets each config struct that way — `resetPidProfile` was lines 168–1667 in the graph against 168–309 in the source, with 45 functions as its children; 310 such functions in 73 files across the tree. Name matching read the nesting as a scope (#1230), so exact-match declined those 45 for every cross-file caller and the fuzzy fallback carried 117 real calls at 0.5. blankCDesignatedMacroArgs runs at the head of preParseCSource: a statement-level call of a macro-cased name whose argument list holds a designator (`.name =` or `[index] =` at argument depth) has that list emptied to spaces, newlines kept, so `RESET_CONFIG(\n\n…\n);` parses cleanly and every offset survives. The references inside the initializer are the price; the broken parse was not yielding them either. The pre-parse runs before the kernel route point, so both arms see the same bytes and the kernel needs no change. betaflight fork, 2,109 C files, against b9ca4b7: resetPidProfile 168–309, nested C functions 310 → 265 (the rest are `#if`-damaged HAL sources, a different shape), +3 function nodes (`isTpaActive` and two more the old parse dropped), 117 fuzzy calls → 118 exact-match calls, 45 `contains` edges moved from resetPidProfile to the file node. Nothing else moved. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 2 ++ __tests__/extraction.test.ts | 53 +++++++++++++++++++++++++++++++ src/extraction/languages/c-cpp.ts | 49 +++++++++++++++++++++++++++- 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1815c4150..a76457569 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -201,6 +201,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). #### Symbols, tests and the viewer +- **A C macro call written with designated initializers no longer swallows every function after it.** Betaflight resets each config struct with `RESET_CONFIG(type, dst, .field = value, …)`, a shape the C grammar cannot parse; past a hundred or so fields its error recovery ran the enclosing function to the end of the file, the next function vanished from the index and every later one was filed under the first, where name matching then treated it as an unreachable closure. The argument list of such a call is now blanked before parsing, offsets kept, so the file's functions come out with their real extents. On that tree 45 functions in `pid.c` alone moved back to top level and their 117 callers resolve at exact-match confidence. Re-index after upgrading. (#1729) + - **Files under an `e2e/` directory count as tests.** Their calls no longer appear as production callers in Steps, dead-code and test badges. - **Production code under a `samples` or `examples` package path is no longer treated as test code.** A Kotlin or Java project whose package path runs through `com/google/samples/…` (Now in Android, for one) had nearly every file counted as a fixture, so the Map opened on `build-logic`, the entry points hid the app, and dead-code and test badges were wrong. Only the project layout above a `src/` folder decides now; the package path below it never does. diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index ad0ba2374..85597f0f6 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -11671,6 +11671,59 @@ describe('C/C++ kernel-port preParse blanks (R7a)', () => { expect(blankLoneMacroLines(bare)).toBe(bare); }); + it('blankCDesignatedMacroArgs empties a designated-initializer macro call, offsets kept (#1729)', async () => { + const { blankCDesignatedMacroArgs } = await import('../src/extraction/languages/c-cpp'); + const src = [ + 'void resetProfile(profile_t *p)', + '{', + ' RESET_CONFIG(profile_t, p,', + ' .pid = { [PID_ROLL] = PID_ROLL_DEFAULT, [PID_YAW] = { 50, 75 } },', + ' .limit = 500, // trailing comma follows', + ' );', + ' log(.5);', + ' OTHER_MACRO(a == b, c);', + '}', + ].join('\n'); + const out = blankCDesignatedMacroArgs(src); + expect(out.length).toBe(src.length); + expect(out.split('\n').length).toBe(src.split('\n').length); + expect(out).toContain('RESET_CONFIG('); + expect(out).not.toContain('.pid'); + expect(out).not.toContain('PID_ROLL'); + // The closing `);` keeps its column; the argument lines are spaces. + expect(out.split('\n')[5]).toBe(' );'); + expect(out.split('\n')[3]).toBe(' '.repeat(src.split('\n')[3].length)); + // A numeric literal and a comparison are not designators. + expect(out).toContain('log(.5);'); + expect(out).toContain('OTHER_MACRO(a == b, c);'); + }); + + it('a designated-initializer macro call no longer swallows the functions after it (#1729)', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1729-')); + try { + // 120 `.field = value` arguments: past the point where tree-sitter-c's + // error recovery ran the enclosing function to the end of the file. + const fields = Array.from({ length: 120 }, (_, i) => ` .field${i} = ${i},`).join('\n'); + fs.writeFileSync( + path.join(dir, 'pid.c'), + `void resetProfile(profile_t *p)\n{\n RESET_CONFIG(profile_t, p,\n${fields}\n );\n}\n\nvoid g(void)\n{\n}\n\nint h(void)\n{\n return 1;\n}\n` + ); + const cg = await CodeGraph.init(dir, { index: true }); + try { + const fns = cg.getNodesByKind('function').filter((n) => n.filePath === 'pid.c'); + const byName = Object.fromEntries(fns.map((n) => [n.name, n])); + expect(Object.keys(byName).sort()).toEqual(['g', 'h', 'resetProfile']); + expect(byName.resetProfile!.endLine).toBe(125); + expect(byName.g!.qualifiedName).toBe('g'); + expect(byName.h!.qualifiedName).toBe('h'); + } 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 cdd573b5a..4dbe84957 100644 --- a/src/extraction/languages/c-cpp.ts +++ b/src/extraction/languages/c-cpp.ts @@ -1515,8 +1515,55 @@ export function blankCNamedVariadicDefineDots(source: string): string { * C-detected headers in CUDA projects (llm.c keeps `__device__` helpers and * kernel prototypes in plain `.h`) — the same content-gated CUDA blank as * C++. Offset-preserving. */ +/** + * Blank the argument list of a statement-level `MACRO( … );` call whose + * arguments are designated initializers — betaflight's + * + * RESET_CONFIG(pidProfile_t, pidProfile, + * .pid = { [PID_ROLL] = PID_ROLL_DEFAULT, … }, + * .pidSumLimit = PIDSUM_LIMIT, + * … + * ); + * + * tree-sitter-c has no rule for `.field = value` as a call argument, and past + * roughly a hundred such arguments its error recovery gives up on the + * enclosing function: the `function_definition` runs to the end of the file, + * the next function vanishes and every one after it is nested under the first + * (#1729 — 310 functions in 73 files on that tree, which name matching then + * treated as unreachable closures). Emptying the argument list to spaces, + * newlines kept, leaves `RESET_CONFIG(\n\n…\n);` — a call the grammar parses + * cleanly — at the cost of the references inside the initializer, which the + * broken parse was not yielding either. Statement-level only (`);` follows), + * macro-cased name only, offsets preserved. + */ +export function blankCDesignatedMacroArgs(source: string): string { + if (source.indexOf('=') === -1) return source; + const out = source.split(''); + const re = /^[ \t]*([A-Z_][A-Z0-9_]*)\s*\(/gm; + let m: RegExpExecArray | null; + while ((m = re.exec(source))) { + const open = m.index + m[0].length - 1; + let depth = 1; + let i = open + 1; + for (; i < source.length && depth > 0; i++) { + const c = source[i]; + if (c === '(') depth++; + else if (c === ')') depth--; + } + if (depth !== 0) continue; + const close = i - 1; + const args = source.slice(open + 1, close); + // A designator at argument depth: `.name =` or `[index] =`. + if (!/(^|[,{(\s])(\.[A-Za-z_]\w*|\[[^\]]+\])\s*=[^=]/.test(args)) continue; + if (!/^\s*;/.test(source.slice(close + 1))) continue; + for (let k = open + 1; k < close; k++) if (out[k] !== '\n') out[k] = ' '; + re.lastIndex = close; + } + return out.join(''); +} + function preParseCSource(source: string): string { - const inner = blankCKernelAnnotations(blankCCplusplusGuardBodies(source)); + const inner = blankCDesignatedMacroArgs(blankCKernelAnnotations(blankCCplusplusGuardBodies(source))); let blanked = blankCLeadingAttrMacros( blankLoneMacroLines( blankCStatementMacroCalls( From e1165f8a1a6ddc0c472e3980198da1943f51d417 Mon Sep 17 00:00:00 2001 From: danusha2345 Date: Mon, 7 Sep 2026 11:17:45 +0300 Subject: [PATCH 2/2] fix(extraction): collapse a damaged C `#if` group to its first branch before parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tree-sitter-c keeps every branch of a `#if … #elif … #else … #endif` 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: a branch that begins with `else` (`} #ifdef X else if (…) { … } #endif`), a branch that ends with a bare `if (…)` header whose body follows the `#endif` (the ST HAL's per-device flash-latency tables), and a branch whose braces do not balance (a signature or a `{` that differs per configuration, body shared). Each ends in a K&R-shaped `if(cond) { … }` that the grammar reads as an implicit-int function definition named `if`; the extractor then files it — and every function after it — under the enclosing function, or the enclosing function runs to the end of the file and the ones after it vanish (265 such nested "functions" in 72 files on a betaflight tree, the remainder after #1729's designated-initializer blank). A second, smaller cause was an all-caps block macro in statement position (`ATOMIC_BLOCK(NVIC_PRIO_MAX) {`, `PG_FOREACH(reg) {`), which the statement-macro blank skipped by design because it only matched lowercase iterator macros. blankCUnbalancedConditionalBranches runs at the tail of preParseCSource, after restoreDirectiveLines, because it edits directive lines on purpose: for a group where any branch shows one of the three symptoms, the first branch not written `#if 0` is kept verbatim and every other branch plus the group's directive lines (`\`-continuations included) are blanked to spaces, newlines and `\r` kept — what the preprocessor would hand a compiler for that configuration. Balanced groups, the vast majority, are untouched. Groups nest innermost-first. blankCStatementMacroCalls now also accepts an ALL_CAPS name (PascalCase — a constructor, if the file is C++ — still excluded). The C++ grammar parses both shapes cleanly (verified on the same snippets as .cpp), so preParseCppSource is unchanged; the pre-parse runs before the kernel route point, so the kernel arm needs no change. Measured on betaflight (2,109 .c files), against the #1729 build: nested C functions 265 → 5 (left: `if MACRO {` conditions with no parentheses, and a `STATIC_DMA_DATA_AUTO union { … } x;` local); C function nodes 39,993 → 39,772 — 260 phantoms gone, ~39 real functions back (spiInternalInitStream, spiInternalStartDMA, spiInternalStopDMA, processSmartPortTelemetry, esc4wayProcess, arm_mat_mult_q15, …). Edges: 1,263 lost / 729 gained; the lost rows are calls previously attributed to the file node or to a phantom `if` (709 exact-match calls, 490 file-level `contains` of what were really locals), the gained rows are the same calls re-attributed to the real function (671 exact-match). Fuzzy-resolved calls 154 → 37. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 2 + __tests__/extraction.test.ts | 112 ++++++++++++++++++++++++++++ src/extraction/languages/c-cpp.ts | 118 +++++++++++++++++++++++++++++- 3 files changed, 228 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a76457569..79399825e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -203,6 +203,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - **A C macro call written with designated initializers no longer swallows every function after it.** Betaflight resets each config struct with `RESET_CONFIG(type, dst, .field = value, …)`, a shape the C grammar cannot parse; past a hundred or so fields its error recovery ran the enclosing function to the end of the file, the next function vanished from the index and every later one was filed under the first, where name matching then treated it as an unreachable closure. The argument list of such a call is now blanked before parsing, offsets kept, so the file's functions come out with their real extents. On that tree 45 functions in `pid.c` alone moved back to top level and their 117 callers resolve at exact-match confidence. Re-index after upgrading. (#1729) +- **A C `#if` group whose branches are not whole statements no longer produces phantom functions.** An `else if (…) { … }` arm kept behind `#ifdef`, an `if (…)` header whose body sits after the `#endif` (the ST HAL's per-device latency tables), or a function signature that differs per configuration all read to the C grammar as a function *named* `if` — and every real function after it in the file was then filed underneath it, or dropped. Such a group is now collapsed to its first live branch before parsing, offsets kept, and block macros written in capitals (`ATOMIC_BLOCK(…) { … }`) are recognized like their lowercase cousins. On a betaflight tree 265 phantom nested functions became 5, and whole functions that had been missing (`spiInternalStartDMA`, `processSmartPortTelemetry`, the CMSIS matrix routines) are back with their callers at exact-match confidence. Re-index after upgrading. + - **Files under an `e2e/` directory count as tests.** Their calls no longer appear as production callers in Steps, dead-code and test badges. - **Production code under a `samples` or `examples` package path is no longer treated as test code.** A Kotlin or Java project whose package path runs through `com/google/samples/…` (Now in Android, for one) had nearly every file counted as a fixture, so the Map opened on `build-logic`, the entry points hid the app, and dead-code and test badges were wrong. Only the project layout above a `src/` folder decides now; the package path below it never does. diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index 85597f0f6..37bd3b01b 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -11724,6 +11724,118 @@ describe('C/C++ kernel-port preParse blanks (R7a)', () => { } }); + it('blankCUnbalancedConditionalBranches collapses a damaged #if group to its first branch, 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: directive lines (continuation included) and the + // second branch are spaces; the first branch is verbatim. + 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(' '.repeat(src.split('\n')[11]!.length)); + expect(out).not.toContain('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) {'); + // 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('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 4dbe84957..a58139477 100644 --- a/src/extraction/languages/c-cpp.ts +++ b/src/extraction/languages/c-cpp.ts @@ -946,7 +946,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; @@ -1562,6 +1565,110 @@ export function blankCDesignatedMacroArgs(source: string): string { return out.join(''); } +/** + * Collapse a `#if … #elif … #else … #endif` group whose branches are not + * self-contained statements to its first live branch. 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 first branch not written `#if 0` is kept verbatim; every other branch + * AND the group's directive lines are blanked to spaces (newlines and `\r` + * kept, so offsets survive), which is exactly what the preprocessor would + * hand a compiler for that configuration. The symbols the other branches + * held were not extracting cleanly anyway. Balanced groups — 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'); + const stripCr = (l: string): string => (l.endsWith('\r') ? l.slice(0, -1) : l); + // Code content of a line: comments and string / char literals removed. + const code = (l: string): string => + stripCr(l) + .replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, '') + .replace(/\/\*.*?\*\//g, '') + .replace(/\/\/.*$/, '') + .trim(); + const blank = (l: string): string => l.replace(/[^\r]/g, ' '); + // Directive line indices of each open group, innermost last. + const stack: Array<{ marks: number[]; keep: number }> = []; + const directive: boolean[] = []; + let changed = false; + let continuation = false; + for (let i = 0; i < lines.length; i++) { + const line = stripCr(lines[i] as string); + const isDirective: boolean = continuation || /^[ \t]*#/.test(line); + directive[i] = isDirective; + const wasContinuation = continuation; + continuation = isDirective && /\\\s*$/.test(line); + if (!isDirective || wasContinuation) continue; + let m: RegExpExecArray | null; + if ((m = C_COND_OPEN_RE.exec(line))) { + const dead = m[1] === 'if' && /^0\b/.test(m[2] as string); + stack.push({ marks: [i], keep: dead ? 1 : 0 }); + continue; + } + const top = stack[stack.length - 1]; + if (!top) continue; + if (C_COND_NEXT_RE.test(line)) { + top.marks.push(i); + continue; + } + if (!C_COND_ENDIF_RE.test(line)) continue; + stack.pop(); + top.marks.push(i); + if (top.keep >= top.marks.length - 1) continue; // `#if 0` with nothing to keep + let damaged = false; + for (let b = 0; b < top.marks.length - 1 && !damaged; 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(lines[k] as string); + 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; + for (const l of body) for (const ch of l) depth += ch === '{' ? 1 : ch === '}' ? -1 : 0; + damaged = + depth !== 0 || + /^else\b/.test(first) || + (/^(else[ \t]+)?if[ \t]*\(/.test(last) && /\)$/.test(last)); + } + if (!damaged) continue; + 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. + for (let k = from; k < to && (k === from || directive[k]); k++) lines[k] = blank(lines[k] as string); + if (b === top.keep) continue; + for (let k = from + 1; k < to; k++) lines[k] = blank(lines[k] as string); + } + lines[i] = blank(lines[i] as string); + changed = true; + } + return changed ? lines.join('\n') : source; +} + function preParseCSource(source: string): string { const inner = blankCDesignatedMacroArgs(blankCKernelAnnotations(blankCCplusplusGuardBodies(source))); let blanked = blankCLeadingAttrMacros( @@ -1586,9 +1693,12 @@ 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 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). + return blankCUnbalancedConditionalBranches( + blankCNamedVariadicDefineDots(restoreDirectiveLines(source, blanked)) + ); } export const cppExtractor: LanguageExtractor = {