From 32105a009c7578598a1281bd7cdd8d0810b51372 Mon Sep 17 00:00:00 2001 From: danusha2345 Date: Mon, 7 Sep 2026 10:35:00 +0300 Subject: [PATCH] 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(