fix(extraction): blank C designated-initializer macro args before parsing (#1755)

tree-sitter-c has no rule for `.field = value` as a call argument. A
statement-level `MACRO(a, b, .x = …, .y = { … },);` recovers by extending
the enclosing function_definition to EOF — later functions vanish or nest
as outer::inner (#1729). blankCDesignatedMacroArgs empties such argument
lists to spaces (newlines kept) at the head of preParseCSource, before the
kernel route point, so both wasm and kernel C arms see the same bytes.

Tests cover the issue fixture (trailing-comma designated args) and a
120-field scale guard. Refs #1729.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
This commit is contained in:
Colby Mchenry
2026-09-08 01:38:46 -05:00
committed by GitHub
co-authored by Colby McHenry
parent 9a32487491
commit 8df9ecac9d
3 changed files with 146 additions and 1 deletions
+96
View File
@@ -11903,6 +11903,102 @@ 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 {
// Issue fixture: designated-initializer args + trailing comma. Without
// blankCDesignatedMacroArgs, tree-sitter-c error recovery extends
// `function_definition` to EOF — `g` vanishes and `h` nests as `f::h`.
fs.writeFileSync(
path.join(dir, 'pid.c'),
[
'void f(void)',
'{',
' M(a, b,',
' .x = 1,',
' .y = { 1, 2 },',
' );',
'}',
'',
'void g(void)',
'{',
'}',
'',
'int h(void)',
'{',
' return 1;',
'}',
'',
].join('\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(['f', 'g', 'h']);
expect(byName.f!.endLine).toBe(7);
expect(byName.g!.qualifiedName).toBe('g');
expect(byName.h!.qualifiedName).toBe('h');
} finally {
cg.close();
}
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
it('a large designated-initializer macro call keeps later functions top-level (#1729)', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1729-large-'));
try {
// Scale guard for betaflight-sized RESET_CONFIG argument lists.
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 = [