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>
This commit is contained in:
Colby Mchenry
2026-07-16 15:17:57 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent e437918026
commit b6a05d155b
3 changed files with 95 additions and 5 deletions
+54 -1
View File
@@ -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