feat(scala): extract class-level context bounds

`class Foo[A: Ordering]` (and the same on case classes, traits and enums)
now emits a `references` edge with the `type_bound` context from the type
to each bound, reusing the method-level path from #2046.

Closes #4080

Co-Authored-By: Grok Bot <noreply@x.ai>
(cherry picked from commit d1257efb7d)
This commit is contained in:
Mahaboob pasha authored and safishamsi committed 2026-10-05 19:40:11 +01:00
1 parent 438a050e08
commit ee292729ae
2 files changed
+107

No files matched your search

+16
View File
@@ -5056,6 +5056,22 @@ def _extract_generic(
if target_nid != class_nid:
add_edge(class_nid, target_nid, "references",
cp_line, context=ctx)
# Class-level context bounds (`class Foo[A: Ordering]`, also on
# traits and enums) are the same typeclass dependency as the
# method-level case (#2046), so they get the same `type_bound`
# context, attributed to the class (#4080).
for c in node.children:
if c.type != "type_parameters":
continue
bound_refs: list[tuple[str, str]] = []
_scala_collect_context_bounds(c, source, bound_refs)
for ref_name, role in bound_refs:
ctx = ("generic_arg" if role == "generic_arg"
else "type_bound")
target_nid = ensure_named_node(ref_name, line)
if target_nid != class_nid:
add_edge(class_nid, target_nid, "references",
line, context=ctx)
# C#: a primary constructor (`class Foo(IBar bar)`, C# 12+) declares
# its dependencies on the type declaration itself rather than in a
+91
View File
@@ -137,3 +137,94 @@ def test_bound_never_points_at_the_function_itself(tmp_path):
r, nid = _build(tmp_path)
for label in ("sort()", "program()", ".member()"):
assert nid[label] not in _bound_targets(r, label, nid)
# Class-level context bounds (#4080). `class_definition`, `trait_definition`
# and `enum_definition` carry the same `type_parameters` node as a method, so
# the bound is the same typeclass dependency, attributed to the type itself.
CLASS_SRC = '''\
trait Ordering
trait Show
trait Eq
trait Async
trait Comparable
class Sorted[A: Ordering](xs: List[A])
case class Pretty[T: Show: Eq](t: T)
trait Effectful[F[_]: Async]
enum Ranked[A: Ordering] { case Low }
class Plain[T](t: T)
class Upper[T <: Comparable[T]](t: T)
class Holder {
def member[K: Ordering](k: K): K = k
}
'''
def _build_classes(tmp_path):
(tmp_path / "ClassContextBounds.scala").write_text(CLASS_SRC)
r = extract_scala(tmp_path / "ClassContextBounds.scala")
nid = {n["label"]: n["id"] for n in r["nodes"]}
return r, nid
def test_class_context_bound_emits_reference(tmp_path):
r, nid = _build_classes(tmp_path)
assert nid["Ordering"] in _bound_targets(r, "Sorted", nid)
def test_case_class_several_context_bounds_each_emit_a_reference(tmp_path):
r, nid = _build_classes(tmp_path)
targets = _bound_targets(r, "Pretty", nid)
assert nid["Show"] in targets
assert nid["Eq"] in targets
def test_trait_higher_kinded_context_bound_emits_reference(tmp_path):
r, nid = _build_classes(tmp_path)
assert nid["Async"] in _bound_targets(r, "Effectful", nid)
def test_enum_context_bound_emits_reference(tmp_path):
r, nid = _build_classes(tmp_path)
assert nid["Ordering"] in _bound_targets(r, "Ranked", nid)
def test_class_without_context_bound_emits_no_bound_edge(tmp_path):
r, nid = _build_classes(tmp_path)
assert not _bound_targets(r, "Plain", nid)
assert not _bound_targets(r, "Upper", nid)
def test_class_context_bound_keeps_constructor_param_as_field(tmp_path):
# The bound must not displace the class_parameters walk: `xs: List[A]`
# still references List under the `field` context.
r, nid = _build_classes(tmp_path)
refs = {(e["target"], e.get("context")) for e in r["edges"]
if e["relation"] == "references" and e["source"] == nid["Sorted"]}
assert (nid["List"], "field") in refs
assert (nid["List"], "type_bound") not in refs
def test_member_bound_is_not_hoisted_to_the_enclosing_class(tmp_path):
r, nid = _build_classes(tmp_path)
assert nid["Ordering"] in _bound_targets(r, ".member()", nid)
assert not _bound_targets(r, "Holder", nid)
def test_class_bound_edge_is_tagged_extracted_with_a_location(tmp_path):
r, nid = _build_classes(tmp_path)
edges = [e for e in r["edges"]
if e["relation"] == "references"
and e["source"] == nid["Sorted"]
and e.get("context") == "type_bound"]
assert edges
for e in edges:
assert e.get("confidence") == "EXTRACTED", e
assert e.get("source_location") == "L7", e