From cf7ef5ab8f0168c32eb0ecde7af36b0433b258c8 Mon Sep 17 00:00:00 2001 From: danusha2345 Date: Mon, 7 Sep 2026 11:17:45 +0300 Subject: [PATCH 1/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 | 1 + __tests__/extraction.test.ts | 112 +++++++++++++++++++++++++++ src/extraction/languages/c-cpp.ts | 122 +++++++++++++++++++++++++++++- 3 files changed, 231 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17022e6343..6d2b576b2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes +- **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. - In TypeScript, an awaited call with type arguments like `await fetchApi("/me")` or `await api.get(url)` now links to the function it calls. Before, the call was dropped, so typed fetch wrappers looked like they had no callers. (#2586) - In JavaScript and TypeScript, a call or `new` in a parameter's default value, like `function renderWithNote(note = buildNote())`, a destructured `{ size = defaultSize() }` or `constructor(private svc = new Service())`, now counts as the function's own call. Before, it was dropped, so `buildNote` was missing that caller and impact on it came back incomplete. Re-index JavaScript and TypeScript projects after upgrading. Thanks @inth3shadows for the report and the pull request. (#2588) - In React, a component rendered at module scope, in a top-level constant or inside a test's `it(…)` callback, like `createRoot(el).render()` or `render()`, now counts as rendered there, so an app's entry reaches its root component and a component's tests show among its callers. (#2587) diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index a481cb8a45..1825d8b2ae 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -13796,6 +13796,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 52b3339345..40f8bfc479 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,110 @@ export function blankCDesignatedMacroArgs(source: string): string { return rawStrings.restore(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 rawStrings = maskCppRawStrings(source); source = rewriteFoundationEnumMacros(rawStrings.source); @@ -2571,9 +2678,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. */ From f3db1726e8cacd09557492351787568f60c1ae4d Mon Sep 17 00:00:00 2001 From: danusha2345 Date: Tue, 8 Sep 2026 10:53:40 +0300 Subject: [PATCH 2/2] fix(extraction): a damaged #if group keeps its default-build branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keeping the first branch dropped `main` from jq's main.c: `#ifdef WIN32 int umain(…) { #else int main(…) { #endif` kept the WIN32 head. A group that opens on a feature test (`#ifdef X`, `#if defined(X)`, `#if X`) and has an `#else` now keeps the `#else` branch — the build without the feature; `#ifndef` / `#if !…` keep the first, `#if 0` the live one. Two more damage shapes, both from betaflight: a branch that is a fragment of an expression (`|| (h4 == NULL)` inside an `if (…)`, `isEmpty(s) ||` before the rest of a condition) and one that opens with `} else if (…) {` — closing a block it did not open. Both left phantom `if` functions behind (7 on that tree). Co-Authored-By: Claude Fable 5.1 --- __tests__/extraction.test.ts | 71 ++++++++++++++++++++++++++++--- src/extraction/languages/c-cpp.ts | 34 ++++++++++++--- 2 files changed, 93 insertions(+), 12 deletions(-) diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index 1825d8b2ae..dbbe4e3bcd 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -13831,15 +13831,16 @@ describe('C/C++ kernel-port preParse blanks (R7a)', () => { 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. + // The `else`-led group opens on a feature test and has an `#else`: the + // `#else` branch (the build without the feature) is verbatim; directive + // lines (continuation included) and the feature branch are 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[7]).toBe(' '.repeat(src.split('\n')[7]!.length)); + expect(out).not.toContain('b(id)'); expect(lines[10]).toBe(' '); - expect(lines[11]).toBe(' '.repeat(src.split('\n')[11]!.length)); - expect(out).not.toContain('c(id)'); + 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'); @@ -13854,6 +13855,64 @@ describe('C/C++ kernel-port preParse blanks (R7a)', () => { 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 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 { diff --git a/src/extraction/languages/c-cpp.ts b/src/extraction/languages/c-cpp.ts index 40f8bfc479..56e24386d3 100644 --- a/src/extraction/languages/c-cpp.ts +++ b/src/extraction/languages/c-cpp.ts @@ -2590,7 +2590,10 @@ export function blankCUnbalancedConditionalBranches(source: string): string { .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 }> = []; + // `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. + const stack: Array<{ marks: number[]; dead: boolean; positive: boolean; elseAt: number }> = []; const directive: boolean[] = []; let changed = false; let continuation = false; @@ -2603,20 +2606,28 @@ export function blankCUnbalancedConditionalBranches(source: string): string { 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 }); + const cond = (m[2] as string).trim(); + const dead = m[1] === 'if' && /^0\b/.test(cond); + const positive = m[1] === 'ifdef' || (m[1] === 'if' && !dead && !/^!/.test(cond)); + stack.push({ marks: [i], dead, 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); - if (top.keep >= top.marks.length - 1) continue; // `#if 0` with nothing to keep + // 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; for (let b = 0; b < top.marks.length - 1 && !damaged; b++) { const body: string[] = []; @@ -2629,10 +2640,21 @@ export function blankCUnbalancedConditionalBranches(source: string): string { 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; + let dips = false; + for (const l of body) { + for (const ch of l) { + depth += ch === '{' ? 1 : ch === '}' ? -1 : 0; + if (depth < 0) dips = true; // closes a block it did not open (`} else if (…) {`) + } + } 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; @@ -2642,7 +2664,7 @@ export function blankCUnbalancedConditionalBranches(source: string): string { // 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; + if (b === keep) continue; for (let k = from + 1; k < to; k++) lines[k] = blank(lines[k] as string); } lines[i] = blank(lines[i] as string);