mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 23:49:14 +00:00
fix(scala): extract deferred (abstract) method declarations
A bodyless `def area: Double` parses as `function_declaration`; a `def` WITH a
body is `function_definition`. Only the latter was a Scala function type, so
every deferred (abstract) method was dropped.
In Scala that is the common case, not an edge case: a `trait` used as an
interface is entirely deferred defs, so the whole contract vanished — the trait
had no method nodes, and a concrete method calling a deferred one
(`describe` -> `name()`) had no target to link to. The same held for an
`abstract class`'s abstract members.
Fix: add `function_declaration` to the Scala function types. A bodyless `def`
only occurs inside a `trait`/`abstract class` body, so it is always a method
declaration; its name sits on the same `name` field as `function_definition`.
Added tests/test_scala_abstract_methods.py: deferred methods (with and without
a parameter list) become nodes in both a trait and an abstract class, a pure
interface trait keeps every contract method, and a call to a deferred method
now resolves.
(cherry picked from commit 7c82507b5e)
This commit is contained in:
1 parent
699fab7793
commit
b52c330ee6
2 files changed
+103
-1
No files matched your search
+7
-1
@@ -1454,7 +1454,13 @@ _SCALA_CONFIG = LanguageConfig(
|
||||
# cases, so it needs a node and a body walk like the others (its cases are
|
||||
# emitted by _scala_extra_walk, the parity of Java #1719 / Kotlin #1738).
|
||||
class_types=frozenset({"class_definition", "object_definition", "trait_definition", "enum_definition"}),
|
||||
function_types=frozenset({"function_definition"}),
|
||||
# `function_declaration` is a bodyless `def area: Double` — a deferred
|
||||
# (abstract) method. In a `trait` or `abstract class` it is the contract a
|
||||
# subclass must implement, exactly like Java/C#/TS abstract methods. Only
|
||||
# `function_definition` (a `def` WITH a body) was a function type, so every
|
||||
# abstract member was dropped: a pure-interface trait had no method nodes at
|
||||
# all, and a concrete method calling a deferred one had no target to link to.
|
||||
function_types=frozenset({"function_definition", "function_declaration"}),
|
||||
import_types=frozenset({"import_declaration"}),
|
||||
call_types=frozenset({"call_expression"}),
|
||||
call_function_field="",
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"""A deferred (abstract) Scala `def` is a method node, like a concrete one.
|
||||
|
||||
A bodyless `def area: Double` parses as `function_declaration`; a `def` WITH a
|
||||
body is `function_definition`. Only the latter was a Scala function type, so
|
||||
every abstract member was dropped. In Scala that is the common case, not an edge
|
||||
case: a `trait` used as an interface is *all* deferred defs, so the whole
|
||||
contract vanished from the graph — the trait had no method nodes, and a concrete
|
||||
method calling a deferred one (`describe` → `name()`) had no target to link to.
|
||||
|
||||
These specs pin that the deferred method becomes a node in both a `trait` and an
|
||||
`abstract class`, and that a call to it now resolves.
|
||||
"""
|
||||
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_deferred_method_without_parens_is_a_node(tmp_path):
|
||||
r = _extract(tmp_path, {"s.scala": (
|
||||
"trait Shape {\n"
|
||||
" def area: Double\n"
|
||||
"}\n"
|
||||
)})
|
||||
assert _method_edge(r, "Shape", ".area()")
|
||||
|
||||
|
||||
def test_deferred_method_with_params_is_a_node(tmp_path):
|
||||
r = _extract(tmp_path, {"s.scala": (
|
||||
"trait Shape {\n"
|
||||
" def scale(f: Double): Shape\n"
|
||||
"}\n"
|
||||
)})
|
||||
assert _method_edge(r, "Shape", ".scale()")
|
||||
|
||||
|
||||
def test_pure_interface_trait_keeps_every_contract_method(tmp_path):
|
||||
r = _extract(tmp_path, {"s.scala": (
|
||||
"trait Shape {\n"
|
||||
" def area: Double\n"
|
||||
" def name(): String\n"
|
||||
" def describe: String = name()\n"
|
||||
"}\n"
|
||||
)})
|
||||
assert _method_edge(r, "Shape", ".area()")
|
||||
assert _method_edge(r, "Shape", ".name()")
|
||||
assert _method_edge(r, "Shape", ".describe()")
|
||||
# The concrete method's call now has a target to resolve to.
|
||||
assert _calls(r, ".describe()", ".name()")
|
||||
|
||||
|
||||
def test_abstract_class_deferred_method_is_a_node(tmp_path):
|
||||
r = _extract(tmp_path, {"s.scala": (
|
||||
"abstract class Base {\n"
|
||||
" def compute(x: Int): Int\n"
|
||||
" def run(): Int = compute(1)\n"
|
||||
"}\n"
|
||||
)})
|
||||
assert _method_edge(r, "Base", ".compute()")
|
||||
assert _calls(r, ".run()", ".compute()")
|
||||
Reference in new issue
Block a user