fix(extract): nested types get a contains edge from the enclosing type (#2040)

In _extract_generic's class branch the `contains` edge was hard-coded to source
from the file node, so a nested class/object/trait attached to the file instead
of its enclosing type across ~19 languages (only C# even flagged the node with
is_nested_type, and still emitted no edge to the parent). The edge now sources
from parent_class_nid when set, else the file node — keeping the containment
tree connected (file -> Outer -> Inner). A `!= class_nid` guard avoids a
self-loop when same-name nesting collides ids (class ids omit the enclosing
name). The C# is_nested_type flag is retained (load-bearing for cross-file
resolution). Methods were already parent-sourced and are unaffected.
This commit is contained in:
safishamsi
2026-07-20 15:40:32 +01:00
parent 1f4e3b2fe1
commit f1405dd7c1
2 changed files with 46 additions and 1 deletions
+13 -1
View File
@@ -2371,7 +2371,19 @@ def _extract_generic(
metadata = {"is_nested_type": True}
add_node(class_nid, class_name, line, metadata=metadata)
callable_def_nids.add(class_nid) # a class is callable (constructor)
add_edge(file_nid, class_nid, "contains", line)
# A nested class/object/trait is contained by its ENCLOSING type, not
# the file (#2040). parent_class_nid is threaded down the walk for
# every language and is always a real class-like node (never a
# namespace — namespace handlers pass it through unchanged), so it is
# a valid edge source. The `!= class_nid` guard avoids a self-loop
# when same-name nesting (`class Foo: class Foo`) collides ids, since
# class ids omit the enclosing type name. Top-level types (parent
# None) still source from the file, keeping the containment tree
# connected: file -> Outer -> Inner.
if parent_class_nid and parent_class_nid != class_nid:
add_edge(parent_class_nid, class_nid, "contains", line)
else:
add_edge(file_nid, class_nid, "contains", line)
# TS/JS decorators on the class and its members (@Component, @Injectable,
# @Input, @Inject, @Entity, …). Decorators live only in class subtrees.
+33
View File
@@ -553,6 +553,39 @@ def test_java_enum_and_annotation_declarations_are_type_nodes(tmp_path):
assert definitions["Audited"].get("source_file") == str(source)
def test_nested_types_contained_by_enclosing_type(tmp_path):
"""#2040: a nested class/object/trait's `contains` edge sources from its
ENCLOSING type, not the file node; top-level types still source from the file
(keeping the tree connected: file -> Outer -> Inner)."""
# Java inner class
j = tmp_path / "Outer.java"
j.write_text("class Outer {\n class Inner { void m() {} }\n}\n")
cj = _edge_labels(extract_java(j), "contains")
assert ("Outer.java", "Outer") in cj # top-level still file-sourced
assert ("Outer", "Inner") in cj # nested sourced from enclosing type
assert ("Outer.java", "Inner") not in cj # NOT from the file (the bug)
# Scala nested class + object (the issue's repro shape)
s = tmp_path / "Outer.scala"
s.write_text("class Outer {\n class Inner\n object Obj\n}\n")
cs = _edge_labels(extract_scala(s), "contains")
assert ("Outer.scala", "Outer") in cs
assert ("Outer", "Inner") in cs
assert ("Outer", "Obj") in cs
assert ("Outer.scala", "Inner") not in cs
def test_csharp_nested_type_gets_containment_edge(tmp_path):
"""#2040 for C#: the nested type now gets a real `contains` edge from its
enclosing type (the is_nested_type flag it already carried is retained and
covered by test_csharp_type_resolution)."""
c = tmp_path / "N.cs"
c.write_text("namespace N {\n class Outer {\n class Inner {}\n }\n}\n")
cc = _edge_labels(extract_csharp(c), "contains")
assert ("Outer", "Inner") in cc
assert ("N.cs", "Inner") not in cc
def test_csharp_field_type_references_have_field_context():
r = extract_csharp(FIXTURES / "sample.cs")
refs = _references(r)