mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 15:40:05 +00:00
An abstract class's `abstract area(): number;` parses as
`abstract_method_signature`, which was not a TypeScript function type, so the
abstract method was dropped. An interface's `method_signature` has long been a
function type, so `interface D { draw(): void }` put `draw` in the graph — but
the identical contract on an `abstract class` did not.
The result: the base of an abstract class kept only its concrete methods, a
polymorphic `shape.area()` had no declaration to resolve to, and the overridden
contract had no node to carry it.
An abstract method is the same kind of contract as an interface method: a
subclass must implement it. Add `abstract_method_signature` to the TS function
types (inherited by the TSX config). Its name sits on the same `name` field as
`method_signature`, so no other change is needed. Abstract fields
(`public_field_definition`) are left as-is, matching concrete fields which are
not nodes either.
Added tests/test_ts_abstract_methods.py: the abstract method becomes a node,
a call through the abstract type resolves to it, and it reaches parity with the
interface method it mirrors.
(cherry picked from commit 2100098042)
103 lines
3.4 KiB
Python
103 lines
3.4 KiB
Python
"""An `abstract` method of an abstract class is a node, like an interface method.
|
|
|
|
`method_signature` (an interface's method) has been a TypeScript function type
|
|
for a while, so `interface Drawable { draw(): void }` puts `draw` in the graph.
|
|
An abstract class's `abstract area(): number;` parses as a DIFFERENT node,
|
|
`abstract_method_signature`, which was not a function type — so the abstract
|
|
method was dropped. The base of an `abstract class` kept only its concrete
|
|
methods, and a polymorphic `shape.area()` had no declaration to resolve to and
|
|
no node to carry the overridden contract.
|
|
|
|
An abstract method is the same kind of contract as an interface method: a
|
|
subclass must implement it. These specs pin that the declaration becomes a node
|
|
and that calls through it resolve, matching the interface behaviour.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
from graphify.extract import extract
|
|
|
|
|
|
def _extract(tmp_path, files: dict[str, str]) -> dict:
|
|
for name, body in files.items():
|
|
p = tmp_path / name
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
p.write_text(body)
|
|
old = os.getcwd()
|
|
try:
|
|
os.chdir(tmp_path)
|
|
return extract([Path(n) for n in files], cache_root=Path(tempfile.mkdtemp()))
|
|
finally:
|
|
os.chdir(old)
|
|
|
|
|
|
def _method_edge(r: dict, owner: str, method: str) -> bool:
|
|
labels = {n["id"]: str(n.get("label", "")) for n in r["nodes"]}
|
|
return any(
|
|
e.get("relation") == "method"
|
|
and labels.get(e.get("source")) == owner
|
|
and labels.get(e.get("target")) == method
|
|
for e in r["edges"]
|
|
)
|
|
|
|
|
|
def _calls(r: dict, src: str, tgt: str) -> bool:
|
|
labels = {n["id"]: str(n.get("label", "")) for n in r["nodes"]}
|
|
return any(
|
|
e.get("relation") == "calls"
|
|
and labels.get(e.get("source")) == src
|
|
and labels.get(e.get("target")) == tgt
|
|
for e in r["edges"]
|
|
)
|
|
|
|
|
|
def test_abstract_method_becomes_a_node(tmp_path):
|
|
r = _extract(tmp_path, {"s.ts": (
|
|
"abstract class Shape {\n"
|
|
" abstract area(): number;\n"
|
|
"}\n"
|
|
)})
|
|
assert _method_edge(r, "Shape", ".area()")
|
|
|
|
|
|
def test_abstract_and_concrete_methods_both_present(tmp_path):
|
|
r = _extract(tmp_path, {"s.ts": (
|
|
"abstract class Shape {\n"
|
|
" abstract area(): number;\n"
|
|
" describe(): string { return this.area().toString(); }\n"
|
|
"}\n"
|
|
)})
|
|
assert _method_edge(r, "Shape", ".area()")
|
|
assert _method_edge(r, "Shape", ".describe()")
|
|
# The concrete method that calls the abstract one links to it.
|
|
assert _calls(r, ".describe()", ".area()")
|
|
|
|
|
|
def test_call_through_abstract_type_resolves_to_the_declaration(tmp_path):
|
|
r = _extract(tmp_path, {"s.ts": (
|
|
"abstract class Shape {\n"
|
|
" abstract area(): number;\n"
|
|
"}\n"
|
|
"function use(s: Shape): number {\n"
|
|
" return s.area();\n"
|
|
"}\n"
|
|
)})
|
|
assert _calls(r, "use()", ".area()")
|
|
|
|
|
|
def test_abstract_method_parity_with_interface_method(tmp_path):
|
|
# Same shape via an interface already worked; the abstract class must match.
|
|
r = _extract(tmp_path, {"s.ts": (
|
|
"interface Drawable {\n"
|
|
" draw(): void;\n"
|
|
"}\n"
|
|
"abstract class Widget {\n"
|
|
" abstract draw(): void;\n"
|
|
"}\n"
|
|
)})
|
|
assert _method_edge(r, "Drawable", ".draw()")
|
|
assert _method_edge(r, "Widget", ".draw()")
|