Skip to content

Commit 7170285

Browse files
gadievronclaude
andcommitted
fix(parsers/c): resolve static-inline functions in included headers
`_is_visible_from` rejected any cross-file `static` callee, which is correct for a `.c` translation unit but wrong for a `static inline` function in an `#include`d header (the header-inline idiom — every including TU gets its own copy and can call it). The include-resolution loop found the header callee via `include_map`, then dropped the edge as internal-linkage. Fix is scoped to the include-resolution site only: a candidate whose file ends in a header extension (.h/.hpp/.hxx/.hh) is visible regardless of `static`. The repo-wide unique-name fallback and the prototype fallback stay strict — a `static` helper in another `.c` still does not resolve cross-TU. Tests: tests/parsers/c/test_c_static_inline_header_resolution.py (static-inline header resolves; static-in-other-.c still rejected; extern header still resolves). Precision invariant preserved: $ pytest tests/parsers/c/test_c_static_inline_header_resolution.py \ tests/parsers/c/test_c_call_resolution_precision.py -q 12 passed in 0.66s Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 6d5be3c commit 7170285

2 files changed

Lines changed: 88 additions & 3 deletions

File tree

libs/openant-core/parsers/c/call_graph_builder.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -497,9 +497,18 @@ def _resolve_call(self, call_name: str, caller_file: str,
497497
func_data = self.functions.get(func_id, {})
498498
fname = func_data.get('name', '')
499499
base_name = fname.split('::')[-1] if '::' in fname else fname
500-
# A static function in an included header has internal linkage
501-
# and is not callable from the including file.
502-
if base_name == call_name and self._is_visible_from(func_id, caller_file):
500+
# A function in an INCLUDED header is callable from the including
501+
# TU regardless of `static`: a `static inline` header helper (the
502+
# C header-inline idiom) is copied into every includer. The
503+
# cross-file static-linkage rejection in _is_visible_from is
504+
# correct only for a .c TU, so it must not gate an included-header
505+
# candidate. (Repo-wide and prototype fallbacks below stay strict.)
506+
header_candidate = func_data.get('file_path', '').endswith(
507+
('.h', '.hpp', '.hxx', '.hh')
508+
)
509+
if base_name == call_name and (
510+
header_candidate or self._is_visible_from(func_id, caller_file)
511+
):
503512
return func_id
504513

505514
# 3. Unique name match across entire repo
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Bug 2 regression: a `static inline` function defined in an INCLUDED header
2+
is callable from the including translation unit (the header-inline idiom — each
3+
TU gets its own copy). The resolver dropped the edge because `_is_visible_from`
4+
treated any cross-file `static` as internal-linkage-invisible, which is correct
5+
for a `.c` translation unit but wrong for an included `.h` header.
6+
7+
Paired must-preserve: tests/parsers/c/test_c_call_resolution_precision.py
8+
::test_unique_name_fallback_skips_static_in_other_file — a `static` fn in another
9+
`.c` must STILL NOT resolve cross-TU. This fix only un-blocks included headers.
10+
"""
11+
import sys
12+
from pathlib import Path
13+
14+
import pytest
15+
16+
CORE = Path(__file__).resolve().parents[3]
17+
if str(CORE) not in sys.path:
18+
sys.path.insert(0, str(CORE))
19+
20+
pytest.importorskip("tree_sitter_c")
21+
22+
from parsers.c.call_graph_builder import CallGraphBuilder # noqa: E402
23+
24+
25+
def _build(eo):
26+
b = CallGraphBuilder(eo)
27+
b.build_call_graph()
28+
out = b.build() if hasattr(b, "build") else b.export()
29+
return out.get("call_graph", {})
30+
31+
32+
def test_static_inline_in_included_header_resolves():
33+
eo = {
34+
"functions": {
35+
"m.c:caller": {"name": "caller", "file_path": "m.c",
36+
"code": "int caller(int n){ return helper(n); }"},
37+
"h.h:helper": {"name": "helper", "file_path": "h.h",
38+
"is_static": True, "is_inline": True,
39+
"code": "static inline int helper(int x){ return x + 1; }"},
40+
},
41+
"includes": {"m.c": ["h.h"], "h.h": []},
42+
}
43+
edges = _build(eo).get("m.c:caller", [])
44+
assert "h.h:helper" in edges, f"static-inline header call dropped: {edges}"
45+
46+
47+
def test_static_in_other_dot_c_still_rejected():
48+
# The #84 precision invariant, re-asserted locally: a static fn in another
49+
# .c TU must NOT resolve (no #include relationship; genuinely file-local).
50+
eo = {
51+
"functions": {
52+
"a.c:caller": {"name": "caller", "file_path": "a.c",
53+
"code": "void caller(void){ helper(); }"},
54+
"b.c:helper": {"name": "helper", "file_path": "b.c", "is_static": True,
55+
"code": "static void helper(void){}"},
56+
},
57+
"includes": {"a.c": [], "b.c": []},
58+
}
59+
edges = _build(eo).get("a.c:caller", [])
60+
assert "b.c:helper" not in edges, f"static helper() in b.c wrongly resolved cross-TU: {edges}"
61+
62+
63+
def test_non_static_in_included_header_still_resolves():
64+
# Guard: a normal (extern) header function must keep resolving (no regression).
65+
eo = {
66+
"functions": {
67+
"m.c:caller": {"name": "caller", "file_path": "m.c",
68+
"code": "int caller(int n){ return ext_helper(n); }"},
69+
"api.h:ext_helper": {"name": "ext_helper", "file_path": "api.h",
70+
"is_static": False,
71+
"code": "int ext_helper(int x);"},
72+
},
73+
"includes": {"m.c": ["api.h"], "api.h": []},
74+
}
75+
edges = _build(eo).get("m.c:caller", [])
76+
assert "api.h:ext_helper" in edges, f"extern header call dropped: {edges}"

0 commit comments

Comments
 (0)