feat(scala): link classes to typeclasses in derives clauses (#2048)

`case class Point(x: Int) derives Show, Eq` (and the same on enums) now
emits a `references` edge with the `derives` context from the type to each
derived typeclass, next to the class-level context bounds it resembles. A
qualified name (`derives cats.Show`) links by its last segment.

Co-Authored-By: Grok Bot <noreply@x.ai>
(cherry picked from commit e6ccd59f20)
This commit is contained in:
Mahaboob pasha authored and safishamsi committed 2026-10-11 21:06:10 +01:00
1 parent 1adadc7f79
commit bcf9624477
2 files changed
+63

No files matched your search

+14
View File
@@ -5640,6 +5640,20 @@ def _extract_generic(
if target_nid != class_nid:
add_edge(class_nid, target_nid, "references",
line, context=ctx)
# `derives Show, cats.Eq` (on classes and enums) names typeclasses
# the type depends on, like a context bound (#2048). A qualified
# name links by its last segment.
derive = node.child_by_field_name("derive")
for d in derive.children_by_field_name("type") if derive is not None else ():
tail = d if d.type == "type_identifier" else next(
(x for x in reversed(d.children) if x.type == "type_identifier"), None)
if tail is None:
continue
d_line = d.start_point[0] + 1
target_nid = ensure_named_node(_read_text(tail, source), d_line)
if target_nid != class_nid:
add_edge(class_nid, target_nid, "references",
d_line, context="derives")
# C#: a primary constructor (`class Foo(IBar bar)`, C# 12+) declares
# its dependencies on the type declaration itself rather than in a
+49
View File
@@ -228,3 +228,52 @@ def test_class_bound_edge_is_tagged_extracted_with_a_location(tmp_path):
for e in edges:
assert e.get("confidence") == "EXTRACTED", e
assert e.get("source_location") == "L7", e
# `derives` clauses (#2048) name typeclasses the type depends on, the same kind
# of dependency as a context bound, so each one is a `references` edge with the
# `derives` context. Grammar: a `derive` field on class_definition and
# enum_definition, one `type` child per derived typeclass.
DERIVES_SRC = '''\
trait Show
trait Eq
case class Point(x: Int) derives Show, Eq
enum Color derives Eq { case Red }
case class Wrapped(x: Int) derives cats.Show
class Bare(x: Int)
'''
def _derives_targets(tmp_path, label):
(tmp_path / "Derives.scala").write_text(DERIVES_SRC)
r = extract_scala(tmp_path / "Derives.scala")
nid = {n["label"]: n["id"] for n in r["nodes"]}
targets = {e["target"] for e in r["edges"]
if e["relation"] == "references"
and e["source"] == nid[label]
and e.get("context") == "derives"}
return targets, nid
def test_case_class_derives_each_typeclass(tmp_path):
targets, nid = _derives_targets(tmp_path, "Point")
assert targets == {nid["Show"], nid["Eq"]}
def test_enum_derives_typeclass(tmp_path):
targets, nid = _derives_targets(tmp_path, "Color")
assert targets == {nid["Eq"]}
def test_qualified_derives_links_by_last_segment(tmp_path):
targets, nid = _derives_targets(tmp_path, "Wrapped")
assert targets == {nid["Show"]}
def test_class_without_derives_emits_no_derives_edge(tmp_path):
targets, _ = _derives_targets(tmp_path, "Bare")
assert not targets