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(