mirror of
https://github.com/safishamsi/graphify.git
synced 2026-09-17 11:06:09 +00:00
fix(ts): resolve new_expression constructor calls (#3116)
A TypeScript/JavaScript `new Foo()` emitted no edge. Fall back to the new_expression's constructor child in the generic callee branch, so it emits a calls edge to the constructed class (member, chained, and generic forms), reusing the existing built-in-global guard so new Map()/new Promise() are not fabricated. Consistent with the C# object-creation handling (#2997).
This commit is contained in:
@@ -5388,8 +5388,10 @@ def _extract_generic(
|
||||
# unique-but-wrong one (#3078).
|
||||
member_receiver = _ruby_const_full_name(recv, source) or None
|
||||
else:
|
||||
# Generic: get callee from call_function_field
|
||||
# Generic: get callee from call_function_field (or constructor on new_expression)
|
||||
func_node = node.child_by_field_name(config.call_function_field) if config.call_function_field else None
|
||||
if func_node is None and node.type == "new_expression":
|
||||
func_node = node.child_by_field_name("constructor")
|
||||
if func_node:
|
||||
if func_node.type == "identifier":
|
||||
callee_name = _read_text(func_node, source)
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
"""TS/JS/TSX `new Foo(...)` constructor calls emit `calls` edges (#3116).
|
||||
|
||||
In tree-sitter JS/TS, `new_expression` exposes its callee under the `constructor`
|
||||
field rather than `function`. The generic path in `walk_calls` previously queried
|
||||
only `call_function_field="function"`, dropping constructor calls.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from graphify.extract import extract, extract_js
|
||||
|
||||
|
||||
def _calls(tmp_path: 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, encoding="utf-8")
|
||||
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, r
|
||||
|
||||
|
||||
def test_ts_new_expression_emits_calls_edge_in_file(tmp_path: Path):
|
||||
calls, _ = _calls(tmp_path, {
|
||||
"main.ts": (
|
||||
"class Foo {\n"
|
||||
" constructor(x: number) {}\n"
|
||||
"}\n"
|
||||
"function caller() {\n"
|
||||
" const x = new Foo(1);\n"
|
||||
"}\n"
|
||||
)
|
||||
})
|
||||
assert any(s == "caller()" and t == "Foo" for s, t in calls)
|
||||
|
||||
|
||||
def test_ts_new_expression_resolves_cross_file(tmp_path: Path):
|
||||
calls, r = _calls(tmp_path, {
|
||||
"foo.ts": "export class Foo {}\n",
|
||||
"caller.ts": (
|
||||
'import { Foo } from "./foo";\n'
|
||||
"export function caller() {\n"
|
||||
" const x = new Foo();\n"
|
||||
"}\n"
|
||||
),
|
||||
})
|
||||
assert any(s == "caller()" and t == "Foo" for s, t in calls)
|
||||
cross_edges = [
|
||||
e for e in r["edges"]
|
||||
if e["relation"] == "calls"
|
||||
and "caller" in e["source"]
|
||||
and "foo" in e["target"].lower()
|
||||
]
|
||||
assert len(cross_edges) == 1
|
||||
|
||||
|
||||
def test_js_new_expression_emits_calls_edge(tmp_path: Path):
|
||||
calls, _ = _calls(tmp_path, {
|
||||
"app.js": (
|
||||
"class Service {}\n"
|
||||
"function init() {\n"
|
||||
" const s = new Service();\n"
|
||||
"}\n"
|
||||
)
|
||||
})
|
||||
assert any(s == "init()" and t == "Service" for s, t in calls)
|
||||
|
||||
|
||||
def test_tsx_new_expression_emits_calls_edge(tmp_path: Path):
|
||||
calls, _ = _calls(tmp_path, {
|
||||
"comp.tsx": (
|
||||
"class Widget {}\n"
|
||||
"function App() {\n"
|
||||
" const w = new Widget();\n"
|
||||
" return <div>{w}</div>;\n"
|
||||
"}\n"
|
||||
)
|
||||
})
|
||||
assert any(s == "App()" and t == "Widget" for s, t in calls)
|
||||
|
||||
|
||||
def test_ts_member_new_expression_raw_calls(tmp_path: Path):
|
||||
file_path = tmp_path / "member.ts"
|
||||
file_path.write_text(
|
||||
"function caller() {\n"
|
||||
" const s = new pkg.Foo();\n"
|
||||
"}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
r = extract_js(file_path)
|
||||
assert any(
|
||||
rc["callee"] == "Foo"
|
||||
and rc.get("is_member_call") is True
|
||||
and rc.get("receiver") == "pkg"
|
||||
for rc in r.get("raw_calls", [])
|
||||
)
|
||||
Reference in New Issue
Block a user