Files
graphify/tests/test_js_callback_calls.py
T
safishamsiandClaude Opus 4.8 cfc6a75c86 fix(extract): TS member-call gating + Kotlin grammar match (#2553, #2552, #2526, #2550, #2551)
TypeScript (#2553, #2552, thanks @Earthfreedom):
- _resolve_typescript_member_calls matched a receiver type by name alone
  and emitted EXTRACTED, so a third-party import could bind to an unrelated
  local class of the same name. It now requires the matched type to be
  same-file or imported by the caller's file, and tiers table-inferred
  receivers to INFERRED.
- calls inside a callback passed to another call (const h = wrapper(arrow))
  were never walked; the callback body is now walked and attributed to the
  declaration, through the same import-gated resolution so it cannot
  fabricate edges. The #2553 gate lands with #2552 by design.

Kotlin (#2526, #2550, #2551; adapts PR #2531, thanks @Mustaqeem66;
reports from @spaceBrownie and @thomasrengot-hub):
- match the bundled tree-sitter-kotlin 1.1.0 import node and resolve each
  import to the real target node (imports were silently dropped), so
  genuine calls promote to EXTRACTED via import evidence;
- a fully-qualified call com.example.Foo.bar() now produces a calls edge;
- a file the grammar cannot fully parse (e.g. one-line class C { val x })
  now warns instead of silently extracting nothing, and declarations
  recovered inside an error span keep their enclosing class.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-08 23:37:32 +01:00

86 lines
3.8 KiB
Python

"""Calls inside a callback passed to a module-level call must not be dropped (#2552).
`export const handler = wrapper(async (req) => { helperA(); })` has a
`call_expression` initializer, so `_js_extra_walk` took the const-literal branch
and never tracked the callback's body — `walk_calls` never descended into it and
the `helperA()` call was lost. The fix tracks each TOPMOST closure in such an
initializer under the const's nid, so its calls flow through the normal
machinery (import-evidence gate included).
The composition test guards the #2552/#2553 coupling: the newly-walked callback
body feeds member calls into `_resolve_typescript_member_calls`, whose origin
gate (#2553) must keep a third-party-typed receiver from fabricating an edge to
an unrelated local class.
"""
from __future__ import annotations
from graphify.extract import extract
_HELPERS = "export function helperA(): number { return 1; }\n"
def _extract(tmp_path, files: dict[str, str]):
for name, body in files.items():
p = tmp_path / name
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(body)
r = extract([tmp_path / n for n in files],
cache_root=tmp_path / "graphify-out", parallel=False)
lbl = {n["id"]: n["label"] for n in r["nodes"]}
calls = {(lbl.get(e["source"]), lbl.get(e["target"])) for e in r["edges"]
if e["relation"] == "calls"}
return calls, lbl, r
_HANDLER = ("import { helperA } from './helpers';\n"
"function wrapper(fn: (req: unknown) => Promise<number>) { return fn; }\n"
"export const handler = wrapper(async (req) => { return helperA(); });\n"
"export function control(): number { return helperA(); }\n")
def test_callback_body_calls_are_captured(tmp_path):
calls, _, _ = _extract(tmp_path, {
"helpers.ts": _HELPERS,
"handler.ts": _HANDLER,
})
assert ("handler", "helperA()") in calls, \
f"callback body call dropped; calls={sorted(calls)}"
# control: a plain named-function caller in the same file is unaffected
assert ("control()", "helperA()") in calls
def test_callback_body_call_is_not_double_counted(tmp_path):
_, lbl, r = _extract(tmp_path, {
"helpers.ts": _HELPERS,
"handler.ts": _HANDLER,
})
n = sum(1 for e in r["edges"]
if e["relation"] == "calls"
and lbl.get(e["source"]) == "handler"
and lbl.get(e["target"]) == "helperA()")
assert n == 1, f"expected exactly one handler -> helperA calls edge, got {n}"
def test_callback_member_call_is_origin_gated(tmp_path):
# Coupling guard: #2552 makes the callback body visible to the member-call
# resolver; #2553's origin gate must then block the name-only `Repo` match
# (third-party type) while the import-evidenced helperA() call resolves.
calls, lbl, r = _extract(tmp_path, {
"helpers.ts": _HELPERS,
"fileb.ts": "export class Repo {\n save(): void {}\n}\n",
"h.ts": ("import { helperA } from './helpers';\n"
"import type { Repo } from 'external-pkg';\n"
"function wrapper(fn: (repo: Repo) => void) { return fn; }\n"
"export const h = wrapper((repo: Repo) => "
"{ repo.save(); helperA(); });\n"),
})
assert ("h", "helperA()") in calls, \
f"import-evidenced callback call must resolve; calls={sorted(calls)}"
sf = {n["id"]: str(n.get("source_file", "")) for n in r["nodes"]}
fabricated = [e for e in r["edges"]
if e["relation"] in ("calls", "references", "indirect_call")
and sf.get(e["source"], "").endswith("h.ts")
and sf.get(e["target"], "").endswith("fileb.ts")]
assert not fabricated, \
f"third-party-typed receiver fabricated edge(s) to local Repo: {fabricated}"