Skip to content

C functions with a macro attribute + a typedef'd return type are indexed under their parameter list #1211

Description

@Dshuishui

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:

GoodName|5
LostName|6

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions