Просмотр исходного кода

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>
Colby Mchenry 6 часов назад
Родитель
Сommit
8df9ecac9d
3 измененных файлов с 146 добавлено и 1 удалено
  1. 1 0
      CHANGELOG.md
  2. 96 0
      __tests__/extraction.test.ts
  3. 49 1
      src/extraction/languages/c-cpp.ts

+ 1 - 0
CHANGELOG.md

@@ -213,6 +213,7 @@ 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)
 - A method called on the result of another call — `d.setdefault(k, []).append(v)`, `make().run()` — no longer produces a call edge to an unrelated top-level function that merely shares the name, in Python and JavaScript/TypeScript. The receiver is kept so the inner call still resolves; the outer method stays unresolved rather than guessed. Re-index after upgrading. (#1683, #1681)
 - A Python call through an imported project module whose name collides with a builtin collection method — `ledger.append(row)` after `from . import ledger` — is no longer dropped as `list.append`. The builtin-method filter now lets the receiver through when it is an imported module that resolves to a file in the project, so `resolveViaImport` can attach the real edge; a stdlib/PyPI receiver (`os.remove`) still produces none. Re-index after upgrading. (#1681, via #1704)
 - **A definition its language makes file-local no longer captures calls from other files.** A C `static` in another source file (`.c`/`.cc`/… — not a header's `static inline`, which is textually included), a Kotlin/Java/C#/Swift/Scala/Dart/PHP `private` member, a Go unexported name in another package, and a Rust non-`pub` item outside its module subtree cannot be what a name in another file means, but name matching accepted them whenever the names agreed: an Android `editor.apply()` onto an unrelated class's `private fun apply`, a JavaScript `fail(...)` onto a Go `func fail`, a Rust `.count()` onto a private `fn count` in another crate, and C USB helpers onto a `static` in a `.c` they never link. Such a target is now declined after the whole name-matching pipeline settles — the reference stays unresolved rather than falling through to a fuzzy namesake. Same-file definitions, a child Rust module reaching its ancestors' private items, and Rust `impl Trait for Type` methods stay resolvable. Re-index after upgrading. (#1730, #1731)

+ 96 - 0
__tests__/extraction.test.ts

@@ -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 = [

+ 49 - 1
src/extraction/languages/c-cpp.ts

@@ -1515,8 +1515,56 @@ 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. Even a
+ * small statement-level `M(a, b, .x = 1, .y = { 1, 2 },);` with a trailing
+ * comma recovers by extending the enclosing `function_definition` to EOF —
+ * the next function vanishes and later ones nest under the first (#1729 —
+ * 310 functions in 73 files on a betaflight 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. Runs before the kernel route
+ * point, so both the wasm and kernel C arms see the same bytes.
+ */
+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(