Summary
For a C definition of the form MACRO_ATTR TYPEDEF_RET name(VOID), the indexer
mis-resolves the declarator and stores the parameter list (e.g. (VOID) /
(void)) as the function name instead of name.
Environment
- codegraph: 1.2.0
- language: C
Minimal reproduction
#define SEC_ATTR __attribute__((section(".init")))
typedef unsigned int UINT32;
#define VOID void
SEC_ATTR VOID GoodName(VOID) { } /* indexed as "GoodName" (ok) */
SEC_ATTR UINT32 LostName(VOID) { return 0; } /* indexed as "(VOID)" (bug) */
codegraph init
sqlite3 .codegraph/codegraph.db \
"SELECT name, start_line FROM nodes WHERE language='c' AND kind='function' ORDER BY start_line;"
Actual:
GoodName|5
(VOID)|6 <- LostName is stored under its parameter list "(VOID)"
Expected:
Exact trigger (isolation)
The name is lost only when all three conditions hold. Removing any one keeps
the name.
| definition |
why |
result |
UINT32 NoAttr(void) |
no attribute |
NoAttr ✅ |
SEC_ATTR int BuiltinRet(void) |
builtin return type |
BuiltinRet ✅ |
__attribute__((section(".init"))) UINT32 RawAttr(void) |
literal attribute (not a macro) |
RawAttr ✅ |
SEC_ATTR UINT32 OneNamedArg(UINT32 x) |
a named parameter |
OneNamedArg ✅ |
SEC_ATTR UINT32 AttrTypedef(void) |
macro attr + typedef ret + bare (void) |
(void) ❌ |
So the trigger is: (a) the attribute comes from a macro (a literal
__attribute__(...) is parsed correctly), and (b) the return type is a
typedef'd type (a builtin return like int / void is fine), and (c)
the parameter list is the bare no-argument form (VOID) (a lone type token; even
one named parameter recovers the real name).
GoodName above survives only because its return type VOID is #define VOID void, i.e. a builtin.
Cause (traced in the bundled extractor, extraction/languages/c-cpp.js, 1.2.0)
An unknown macro before a typedef'd return type makes tree-sitter misparse the
declaration, and neither of the C extractor's safety nets catches it.
1. The C extractor has no pre-parse macro handling. The C++ extractor
(cppExtractor) sets preParse: preParseCppSource, which blanks macros before
tree-sitter runs so a stray token before the return type can't derail the parse.
The C extractor (cExtractor) has no preParse — only the post-hoc
recoverMangledName fallback. So in C the macro SEC_ATTR reaches tree-sitter as
an unknown leading token, SEC_ATTR UINT32 LostName(VOID) is misparsed, and the
extracted declarator name comes out as the parameter list (VOID).
2. The post-hoc name recovery does not cover this mangle shape.
recoverMangledName (recoverMangledCppName) targets the "return type glued
onto the name" mangle (GetName → "FString GetName"), which has an internal
space. Its first guard is:
if (!/\s/.test(name) || name.startsWith('operator') || name.startsWith('~'))
return name;
The mangled name here is (VOID) — no internal whitespace — so recovery returns
it unchanged. (The real name also isn't present in the string (VOID), so
string-level recovery can't reconstruct it; it would need the source token before
the parameter list.)
Confirmed to be the C path specifically — the identical source resolves
correctly as C++; only the file extension changed:
a.c -> (VOID) (cExtractor)
b.cpp -> LostName (cppExtractor)
So the name is recoverable (the C++ path gets it); the C path both misparses (no
pre-parse) and then skips recovery (the whitespace guard). The
builtin-vs-typedef, literal-vs-macro, and named-parameter distinctions in the
isolation tables above are all consistent with a tree-sitter type-vs-name
disambiguation that only fails when every token before the declarator is
unfamiliar (unknown macro + typedef'd return) and the parameter list is the bare
(VOID) form.
Possible fix directions
- Give
cExtractor a preParse step as cppExtractor has (though a curated
allow-list won't cover arbitrary project macros like SEC_ATTR /
LITE_OS_SEC_TEXT_INIT); and/or
- Extend the name recovery to handle a declarator that misparsed into a
parameter-list shape (^\(.*\)$) by recovering the identifier immediately
before the parameter list from the source, rather than from the mangled name
string (which no longer contains it).
Cross-check with a real compiler
The source is valid and unambiguous — a full C toolchain names the function
correctly (using a portable attribute so the section syntax is platform-neutral):
printf '%s\n' \
'#define SEC_ATTR __attribute__((noinline))' \
'typedef unsigned int UINT32;' \
'#define VOID void' \
'SEC_ATTR UINT32 LostName(VOID) { return 0; }' > one.c
cc -c one.c -o one.o && nm one.o | grep -i lostname
# -> ... T _LostName
Impact
Real-world C that uses a section/placement macro on no-argument entry points is
affected pervasively. For example, OpenHarmony LiteOS-M kernel functions are
almost all of this shape:
LITE_OS_SEC_TEXT_INIT UINT32 LOS_KernelInit(VOID) { ... }
Every such function is indexed as (VOID), and multiple ones in a file collapse
to (VOID), (VOID)_1, … Because the real names are gone from the node table,
call sites that reference them (LOS_KernelInit()) also fail to resolve, so the
call edges into these functions are dropped — the call graph for such a codebase
is largely unusable.
Summary
For a C definition of the form
MACRO_ATTR TYPEDEF_RET name(VOID), the indexermis-resolves the declarator and stores the parameter list (e.g.
(VOID)/(void)) as the function name instead ofname.Environment
Minimal reproduction
codegraph init sqlite3 .codegraph/codegraph.db \ "SELECT name, start_line FROM nodes WHERE language='c' AND kind='function' ORDER BY start_line;"Actual:
Expected:
Exact trigger (isolation)
The name is lost only when all three conditions hold. Removing any one keeps
the name.
UINT32 NoAttr(void)NoAttr✅SEC_ATTR int BuiltinRet(void)BuiltinRet✅__attribute__((section(".init"))) UINT32 RawAttr(void)RawAttr✅SEC_ATTR UINT32 OneNamedArg(UINT32 x)OneNamedArg✅SEC_ATTR UINT32 AttrTypedef(void)(void)(void)❌So the trigger is: (a) the attribute comes from a macro (a literal
__attribute__(...)is parsed correctly), and (b) the return type is atypedef'd type (a builtin return like
int/voidis fine), and (c)the parameter list is the bare no-argument form
(VOID)(a lone type token; evenone named parameter recovers the real name).
GoodNameabove survives only because its return typeVOIDis#define VOID void, i.e. a builtin.Cause (traced in the bundled extractor,
extraction/languages/c-cpp.js, 1.2.0)An unknown macro before a typedef'd return type makes tree-sitter misparse the
declaration, and neither of the C extractor's safety nets catches it.
1. The C extractor has no pre-parse macro handling. The C++ extractor
(
cppExtractor) setspreParse: preParseCppSource, which blanks macros beforetree-sitter runs so a stray token before the return type can't derail the parse.
The C extractor (
cExtractor) has nopreParse— only the post-hocrecoverMangledNamefallback. So in C the macroSEC_ATTRreaches tree-sitter asan unknown leading token,
SEC_ATTR UINT32 LostName(VOID)is misparsed, and theextracted declarator name comes out as the parameter list
(VOID).2. The post-hoc name recovery does not cover this mangle shape.
recoverMangledName(recoverMangledCppName) targets the "return type gluedonto the name" mangle (
GetName→"FString GetName"), which has an internalspace. Its first guard is:
The mangled name here is
(VOID)— no internal whitespace — so recovery returnsit unchanged. (The real name also isn't present in the string
(VOID), sostring-level recovery can't reconstruct it; it would need the source token before
the parameter list.)
Confirmed to be the C path specifically — the identical source resolves
correctly as C++; only the file extension changed:
So the name is recoverable (the C++ path gets it); the C path both misparses (no
pre-parse) and then skips recovery (the whitespace guard). The
builtin-vs-typedef, literal-vs-macro, and named-parameter distinctions in the
isolation tables above are all consistent with a tree-sitter type-vs-name
disambiguation that only fails when every token before the declarator is
unfamiliar (unknown macro + typedef'd return) and the parameter list is the bare
(VOID)form.Possible fix directions
cExtractorapreParsestep ascppExtractorhas (though a curatedallow-list won't cover arbitrary project macros like
SEC_ATTR/LITE_OS_SEC_TEXT_INIT); and/orparameter-list shape (
^\(.*\)$) by recovering the identifier immediatelybefore the parameter list from the source, rather than from the mangled name
string (which no longer contains it).
Cross-check with a real compiler
The source is valid and unambiguous — a full C toolchain names the function
correctly (using a portable attribute so the section syntax is platform-neutral):
Impact
Real-world C that uses a section/placement macro on no-argument entry points is
affected pervasively. For example, OpenHarmony LiteOS-M kernel functions are
almost all of this shape:
Every such function is indexed as
(VOID), and multiple ones in a file collapseto
(VOID),(VOID)_1, … Because the real names are gone from the node table,call sites that reference them (
LOS_KernelInit()) also fail to resolve, so thecall edges into these functions are dropped — the call graph for such a codebase
is largely unusable.