Skip to content

Keep C and C++ functions that return pointers or references in the graph - #148

Merged
anhnh2002 merged 2 commits into
mainfrom
fix/c-cpp-pointer-return-functions
Oct 30, 2026
Merged

anhnh2002 merged 2 commits into
mainfrom
fix/c-cpp-pointer-return-functions

Conversation

@anhnh2002

@anhnh2002 anhnh2002 commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

The C and C++ analyzers looked for a function's function_declarator as a direct child of its function_definition. When the return type is a pointer (const char *f(), T **f()) or a C++ reference (T &f()), tree-sitter wraps the declarator in a pointer_declarator / reference_declarator, so the function was left out of the dependency graph, together with every call made from inside it. For lua, 162 of its ~1,300 C functions were missing, including public API such as lua_tolstring, lua_pushstring and luaL_checklstring.

A second case: with an export macro before a typedef'd return type (LUA_API lua_Unsigned lua_rawlen(...)), tree-sitter reads the type as the declarator and leaves the real name in an ERROR node before the parameters. The function was recorded under its return type's name (lapi.c::lua_Unsigned), and functions with the same return type collided.

Changes

  • New analyzers/c_declarators.py, shared by both analyzers:
    • function_declarator() finds the declarator through pointer, reference, parenthesized and attributed declarators. For a function that returns a function pointer, it takes the innermost one.
    • macro_typed_name() reads the real name from the ERROR node in the macro case.
  • c.py: both function extraction and _find_containing_function use the helpers, so calls from inside these functions get their edges.
  • cpp.py: every place that looked up the function_declarator of a definition or method declaration uses the helper. _find_containing_function uses _get_declarator_name, so qualified names work there too.

Measurements without LLM calls

Dependency graph built with DependencyParser(repo).parse_repository(), comparing main (1b23ea3) with this branch. "Missing" counts function definitions found by a rough regex in files the parser reads, with no component of that name in the same file.

Repository Components Dependency links Missing functions
lua (C) 1,239 → 1,401 2,441 → 3,135 166 → 0
tinyxml2 (C++) 327 → 423 363 → 601 49 → 0
lz4 (C) 549 → 588 793 → 909 36 → 0
cJSON (C) 146 → 169 197 → 304 24 → 1
inih (C) 30 → 35 20 → 26 5 → 0
qrcode (C) 519 → 522 936 → 942 1 → 0

The names that were wrong are gone: lapi.c::lua_CFunction, lapi.c::lua_Unsigned, lauxlib.c::lua_Integer (lua), and lib/xxhash.c::XXH_errorcode (lz4).

One function is still missing: cJSON's int CJSON_CDECL main(void). With a calling-convention macro between the type and the name, tree-sitter splits the head into a declaration plus a broken definition. Recovering it would need preprocessing.

Validation

  • 578 Python tests passed (pytest -p no:cacheprovider -o addopts="" tests/ -q).
  • New tests/test_c_cpp_declarators.py covers functions returning T *, T **, const char *, a function pointer, API T f() in C, and T & / const char * methods, T **, API T f() in C++. It also checks the call edges made from inside them. Both tests fail on main.
  • uvx ruff@latest check and uvx ruff@latest format --check passed on the 4 changed files; git diff --check passed.

…he graph

The C and C++ analyzers looked for a function's function_declarator as a
direct child of its function_definition. When the return type is a pointer
(`const char *f()`, `T **f()`) or a C++ reference (`T &f()`), tree-sitter
wraps it in a pointer/reference declarator, so the function was left out of
the dependency graph, together with every call made from inside it. A
function returning a function pointer was also lost.

An export macro before a typedef'd return type (`API lua_Integer f()`) was
named after its return type: tree-sitter reads the type as the declarator and
leaves the real name in an ERROR node before the parameters.

Both analyzers now find the declarator through those wrappers (shared helper
in analyzers/c_declarators.py) and read the macro case's name from the ERROR
node.
…s name

`int CJSON_CDECL main(void) { ... }`: with a parameter list of one word, tree-sitter-c reads `int
CJSON_CDECL` as a declaration missing its `;`, then a function_definition whose type is `main` and whose
declarator is the parenthesized `(void)`. It has no function_declarator, so the function and every call
made from it were left out. (With real parameters the macro lands in an ERROR node and the name is found,
as before.)

split_macro_function_name: a function_definition whose type is a type_identifier, whose declarator is a
parenthesized single identifier, and whose previous sibling is a declaration ending in a missing `;` on
the same line, defines a function named by that type. A real typedef'd return type (`Count two(void)`)
follows a complete declaration, so it is not taken for a name.

Measured (old branch head against this commit, components): cJSON 169 -> 170 (test.c::main); lz4, lua,
inih, tinyxml2 and jq unchanged.
@anhnh2002
anhnh2002 merged commit 7b0d435 into main Oct 30, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant