mirror of
https://github.com/safishamsi/graphify.git
synced 2026-08-29 09:46:43 +00:00
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>
86 lines
3.8 KiB
Python
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}"
|