mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 07:29:33 +00:00
fix(typescript): extract abstract method signatures
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)
This commit is contained in:
1 parent
7340b82812
commit
699fab7793
2 files changed
+108
-1
No files matched your search
+6
-1
@@ -1266,7 +1266,12 @@ _TS_CONFIG = LanguageConfig(
|
||||
"enum_declaration", # named enums
|
||||
"type_alias_declaration", # named type aliases
|
||||
}),
|
||||
function_types=frozenset({"function_declaration", "generator_function_declaration", "method_definition", "method_signature"}),
|
||||
# `abstract_method_signature`: an abstract class's method is a contract
|
||||
# declaration exactly like an `interface`'s `method_signature` (already
|
||||
# captured) — a subclass must implement it. Without it the abstract method
|
||||
# was dropped, so the base of an `abstract class` sat in the graph with only
|
||||
# its concrete methods and no node for the overridden contract.
|
||||
function_types=frozenset({"function_declaration", "generator_function_declaration", "method_definition", "method_signature", "abstract_method_signature"}),
|
||||
import_types=frozenset({"import_statement", "export_statement"}),
|
||||
call_types=frozenset({"call_expression", "new_expression"}),
|
||||
call_function_field="function",
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"""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()")
|
||||
Reference in new issue
Block a user