Sfoglia il codice sorgente

fix(c): blank leading attribute macros so functions index under real names (#1311)

SEC_ATTR UINT32 LostName(VOID) — an unknown attribute macro before a
typedef'd return type — misparses in tree-sitter's C grammar: the macro
becomes the type, the return type the declarator, and the PARAMETER
LIST is stored as the function name ("(VOID)"). The C++ grammar
recovers this shape via recoverMangledCppName, but in C the real name
never reaches the mangled string, so only a pre-parse blank can help.

Attribute macros are project-specific, so the blank keys on structure:
line-leading ALL-CAPS token followed by TWO identifiers then `(` — the
`MACRO Ret name(` definition shape. Plain typedef'd returns, ALL-CAPS
calls, #define lines, multi-word builtin returns, and mid-line uses are
all rejected by construction. Offset-preserving like the C++ blanks.

curl re-index: 5,531 C functions before and after, zero name changes;
7 nodes in memdebug.c improve start-line accuracy by 1 (the macro line
no longer counts as part of the definition).

Fixes #1211

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Colby Mchenry 1 mese fa
parent
commit
b6a05d155b
3 ha cambiato i file con 95 aggiunte e 5 eliminazioni
  1. 1 0
      CHANGELOG.md
  2. 54 1
      __tests__/extraction.test.ts
  3. 40 4
      src/extraction/languages/c-cpp.ts

+ 1 - 0
CHANGELOG.md

@@ -18,6 +18,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ### Fixes
 
+- C functions declared with a project-specific attribute macro in front of a typedef'd return type (`SEC_ATTR UINT32 MyFunc(VOID)` — common in embedded and kernel code) are now indexed under their real names. Previously the parser tripped over the unknown macro and stored the parameter list as the function name, leaving entries like `"(VOID)"` in the graph and making the real function unfindable. (#1211)
 - C++ methods defined out-of-line inside a namespace (`namespace sim { Output MyClass::Apply(...) { ... } }`) now carry the namespace in their qualified name, matching their class. Fully-qualified call sites from other files (`sim::MyClass::Apply(...)`) resolve to the definition again, so `codegraph callers` and file impact no longer come up empty for this pattern. (#1291)
 - C++ methods defined out-of-line on a template class (`template <typename T> T Box<T>::get() { ... }`) no longer keep the template parameter list in their qualified name. They now index as `Box::get` — identical to an inline definition of the same method — so they link to their class and resolve from call sites again, and pathological multi-line template parameter lists can no longer blow the qualified name past filesystem name limits. (#1286)
 - Go route detection no longer misidentifies ordinary method calls that share HTTP verb names — `cache.Put("key", value)`, `store.Get("config", out)`, `bus.Handle("user.created", handler)` and the like were being indexed as HTTP routes, polluting route listings in cache-heavy codebases. A registration now has to look like one: its first argument must be a `/`-prefixed path (all routers) or a Go 1.22 `"METHOD /path"` pattern on `Handle`/`HandleFunc`, which now also extracts the method instead of listing the route as `ANY`. (#1259)

+ 54 - 1
__tests__/extraction.test.ts

@@ -11,7 +11,7 @@ import * as os from 'os';
 import { CodeGraph } from '../src';
 import { extractFromSource, scanDirectory, buildDefaultIgnore, discoverEmbeddedRepoRoots, buildScopeIgnore } from '../src/extraction';
 import { detectLanguage, isLanguageSupported, getSupportedLanguages, initGrammars, loadAllGrammars, isSourceFile } from '../src/extraction/grammars';
-import { stripCppTemplateArgs, blankCppExportMacros, blankCppInlineMacros, blankMetalAttributes, blankCudaConstructs, blankCppAnnotationMacroCalls, blankCppApiPrefixMacros, blankCppInlineAnnotationMacros, recoverMangledCppName } from '../src/extraction/languages/c-cpp';
+import { stripCppTemplateArgs, blankCppExportMacros, blankCppInlineMacros, blankMetalAttributes, blankCudaConstructs, blankCppAnnotationMacroCalls, blankCppApiPrefixMacros, blankCppInlineAnnotationMacros, blankCLeadingAttrMacros, recoverMangledCppName } from '../src/extraction/languages/c-cpp';
 import { normalizePath } from '../src/utils';
 
 beforeAll(async () => {
@@ -4115,6 +4115,59 @@ class Both : public Base<char>, public Plain {};
     });
   });
 
+  describe('C leading attribute macro before typedef return type (#1211)', () => {
+    // `SEC_ATTR UINT32 LostName(VOID)` — tree-sitter's C grammar reads the
+    // unknown macro as the type, the typedef'd return as the declarator, and
+    // stores the PARAMETER LIST as the function name ("(VOID)"). The
+    // structural pre-parse blank recovers the definition; the issue's whole
+    // isolation table is pinned here.
+    it("recovers the issue's full isolation table under their real names", () => {
+      const code = `#define SEC_ATTR __attribute__((section(".init")))
+typedef unsigned int UINT32;
+#define VOID void
+
+SEC_ATTR VOID   GoodName(VOID)  { }
+SEC_ATTR UINT32 LostName(VOID)  { return 0; }
+UINT32 NoAttr(void) { return 0; }
+SEC_ATTR int BuiltinRet(void) { return 0; }
+__attribute__((section(".init"))) UINT32 RawAttr(void) { return 0; }
+SEC_ATTR UINT32 OneNamedArg(UINT32 x) { return x; }
+SEC_ATTR UINT32* PtrRet(VOID) { return 0; }
+`;
+      const result = extractFromSource('attrs.c', code);
+      const fns = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name);
+      expect(fns).toEqual(
+        expect.arrayContaining([
+          'GoodName', 'LostName', 'NoAttr', 'BuiltinRet', 'RawAttr', 'OneNamedArg', 'PtrRet',
+        ])
+      );
+      // The bug shape: a parameter list stored as a name.
+      expect(fns.find((n) => n.includes('('))).toBeUndefined();
+    });
+
+    it('blankCLeadingAttrMacros only touches the MACRO-ret-name-( definition shape', () => {
+      // Blanked: the definition shape (offset-preserving).
+      expect(blankCLeadingAttrMacros('SEC_ATTR UINT32 f(void) {}')).toBe(
+        '         UINT32 f(void) {}'
+      );
+      // Untouched: a plain typedef'd return with ONE identifier before `(`.
+      expect(blankCLeadingAttrMacros('UINT32 helper(void) {}')).toBe('UINT32 helper(void) {}');
+      // Untouched: an ALL-CAPS function CALL at line start.
+      expect(blankCLeadingAttrMacros('MY_ASSERT(x);')).toBe('MY_ASSERT(x);');
+      // Untouched: #define lines (start with #, not line-leading CAPS).
+      const def = '#define SEC_ATTR __attribute__((section(".init")))';
+      expect(blankCLeadingAttrMacros(def)).toBe(def);
+      // Untouched: multi-word builtin returns (the grammar keeps the name there).
+      expect(blankCLeadingAttrMacros('SEC_ATTR unsigned int f(void) {}')).toBe(
+        'SEC_ATTR unsigned int f(void) {}'
+      );
+      // Untouched: mid-line uses.
+      expect(blankCLeadingAttrMacros('x = SEC_ATTR UINT32 y(z);')).toBe(
+        'x = SEC_ATTR UINT32 y(z);'
+      );
+    });
+  });
+
   describe('C++ out-of-line template method receivers (#1286)', () => {
     // `template<typename T> T Box<T>::get()` used to store qualified_name
     // `Box<T>::get` — the `<T>` qualifier never matched the class node indexed

+ 40 - 4
src/extraction/languages/c-cpp.ts

@@ -709,11 +709,47 @@ function preParseCppSource(source: string, filePath?: string): string {
   return blanked;
 }
 
-/** C source pre-processing: C-detected headers in CUDA projects (llm.c keeps
- * `__device__` helpers and kernel prototypes in plain `.h`) get the same
- * content-gated CUDA blank as C++. */
+/**
+ * Blank an unknown attribute macro sitting in front of a C function
+ * definition's return type: `SEC_ATTR UINT32 LostName(VOID) { … }` (macro
+ * wrapping `__attribute__((…))`, common in embedded/kernel C). tree-sitter's
+ * C grammar reads the macro as the declaration's type, the real return type
+ * as the declarator, and stores the PARAMETER LIST as the function name —
+ * `LostName` indexes as `"(VOID)"` and is unfindable (#1211). The C++ grammar
+ * recovers this shape differently (glued name, salvaged post-hoc by
+ * `recoverMangledCppName`), but in C the real name never reaches the mangled
+ * string, so only a pre-parse blank can recover it.
+ *
+ * Attribute macros are project-specific (`SEC_ATTR`, `INIT_TEXT`, …), so this
+ * keys on structure, not a curated list, matched tightly:
+ *  - line-leading (`^[ \t]*`) — declaration position, never an expression use;
+ *  - ALL-CAPS token of ≥3 chars (`[A-Z][A-Z0-9_]{2,}`) — ordinary C types in
+ *    definitions are rarely spelled this way, and when they are (`UINT32 f()`)
+ *    they're followed by ONE identifier + `(`, which the lookahead rejects;
+ *  - followed by TWO identifier tokens (return type, then name — `*` allowed
+ *    for pointer returns) and then `(` — i.e. exactly the
+ *    `MACRO Ret name(` definition shape. `MACRO name(` calls, `#define`
+ *    lines (start with `#`), and multi-word builtin returns
+ *    (`MACRO unsigned int f(` — where the C grammar already keeps the name)
+ *    are all left untouched.
+ * Equal-length spaces preserve every byte offset, like the C++ blanks above.
+ */
+const C_LEADING_ATTR_MACRO_RE =
+  /^([ \t]*)([A-Z][A-Z0-9_]{2,})(?=\s+[A-Za-z_]\w*[\s*]+[A-Za-z_]\w*\s*\()/gm;
+export function blankCLeadingAttrMacros(source: string): string {
+  return source.replace(
+    C_LEADING_ATTR_MACRO_RE,
+    (_m, ws: string, macro: string) => ws + ' '.repeat(macro.length)
+  );
+}
+
+/** C source pre-processing: recover functions hidden behind a leading
+ * attribute macro (#1211), then — for 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. */
 function preParseCSource(source: string): string {
-  return looksLikeCudaSource(source) ? blankCudaConstructs(source) : source;
+  const blanked = blankCLeadingAttrMacros(source);
+  return looksLikeCudaSource(blanked) ? blankCudaConstructs(blanked) : blanked;
 }
 
 export const cppExtractor: LanguageExtractor = {