fix(fortran): emit calls edges for function invocations

Function calls (`y = f(x)`) were silently dropped — only `subroutine_call`
(`call sub(...)`) was handled in walk_calls. tree-sitter-fortran represents a
function invocation as a `call_expression`, which had no branch, so every
function-to-function call produced no edge.

Handle `call_expression`. Because Fortran uses the same `name(...)` syntax for
array indexing, the callee is resolved against procedures defined in the file
(`target_nid in seen_ids`) before emitting — so array accesses like `arr(i)`
cannot fabricate spurious `calls` edges. Adds a function + caller to the
fixture and a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Synvoya
2026-07-01 11:58:38 +01:00
committed by safishamsi
co-authored by Claude Opus 4.8
parent f2ea6a6087
commit b8f41c77eb
3 changed files with 44 additions and 0 deletions
+13
View File
@@ -7507,6 +7507,19 @@ def extract_fortran(path: Path) -> dict:
target_nid = _make_id(stem, callee)
add_edge(scope_nid, target_nid, "calls", node.start_point[0] + 1,
confidence="EXTRACTED", context="call")
# x = compute(args) — function invocations are `call_expression`, which
# shares Fortran's `name(...)` syntax with array indexing. Only emit a
# call edge when the callee resolves to a procedure defined in this file
# (an array variable produces no matching node), so array accesses can't
# fabricate spurious `calls` edges.
elif t == "call_expression":
name_node = next((c for c in node.children if c.type == "identifier"), None)
if name_node:
callee = _read_text(name_node, source).lower()
target_nid = _make_id(stem, callee)
if target_nid in seen_ids and target_nid != scope_nid:
add_edge(scope_nid, target_nid, "calls", node.start_point[0] + 1,
confidence="EXTRACTED", context="call")
for child in node.children:
walk_calls(child, scope_nid)
+13
View File
@@ -43,6 +43,19 @@ contains
print *, "Area =", area
end subroutine print_area
function double_val(x) result(y)
real, intent(in) :: x
real :: y
y = x * 2.0
end function double_val
subroutine report(radius)
real, intent(in) :: radius
real :: scaled
scaled = double_val(radius)
print *, scaled
end subroutine report
end module geometry
+18
View File
@@ -1531,6 +1531,24 @@ def test_fortran_finds_calls():
assert len(call_edges) >= 1
def test_fortran_finds_function_call():
"""`y = f(x)` function invocations must emit a calls edge.
Function calls are `call_expression` (not `subroutine_call`); that node was
never handled, so every function-to-function call was dropped. The callee is
resolved against defined procedures so array indexing (`arr(i)`) can't
fabricate a spurious edge.
"""
r = extract_fortran(FIXTURES / "sample.f90")
labels = {n["id"]: n["label"] for n in r["nodes"]}
found = any(
"report" in labels.get(e["source"], "")
and "double_val" in labels.get(e["target"], "")
for e in r["edges"] if e["relation"] == "calls"
)
assert found, "report() should have a calls edge to double_val()"
def test_fortran_case_insensitive_names():
r = extract_fortran(FIXTURES / "sample.f90")
labels = [n["label"] for n in r["nodes"]]