Merge pull request #1071 from TheFedaikin/v8

Fix CI and ddd semantic contexts for ObjC, Julia, C, C++, Scala, Fortran, and PowerShell
This commit is contained in:
Safi
2026-05-29 01:54:30 +01:00
committed by GitHub
8 changed files with 619 additions and 125 deletions
+451 -28
View File
@@ -1017,6 +1017,106 @@ def _swift_property_type_node(property_node):
return None
# ── C / C++ type-ref helpers ─────────────────────────────────────────────────
_C_PRIMITIVE_TYPE_NODES = frozenset({
"primitive_type", "sized_type_specifier", "auto", "placeholder_type_specifier",
})
def _c_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]]) -> None:
"""Walk a C type expression; append (name, role) tuples for user-defined types.
Skips primitive types and qualifiers; recognises type_identifier."""
if node is None or node.type in _C_PRIMITIVE_TYPE_NODES:
return
t = node.type
if t == "type_identifier":
text = _read_text(node, source)
if text:
out.append((text, "generic_arg" if generic else "type"))
return
if t in ("pointer_declarator", "reference_declarator", "array_declarator",
"type_qualifier", "type_descriptor", "abstract_pointer_declarator",
"abstract_reference_declarator", "abstract_array_declarator"):
for c in node.children:
if c.is_named:
_c_collect_type_refs(c, source, generic, out)
def _cpp_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]]) -> None:
"""Walk a C++ type expression; append (name, role) tuples.
Resolves qualified_identifier tails (std::string string) and template_type
base + arguments (std::vector<HttpClient> vector + HttpClient as generic_arg)."""
if node is None or node.type in _C_PRIMITIVE_TYPE_NODES:
return
t = node.type
if t == "type_identifier":
text = _read_text(node, source)
if text:
out.append((text, "generic_arg" if generic else "type"))
return
if t == "qualified_identifier":
name_node = node.child_by_field_name("name")
if name_node is not None:
_cpp_collect_type_refs(name_node, source, generic, out)
return
if t == "template_type":
name_node = node.child_by_field_name("name")
if name_node is not None:
text = _read_text(name_node, source)
if text:
out.append((text, "generic_arg" if generic else "type"))
args_node = node.child_by_field_name("arguments")
if args_node is not None:
for c in args_node.children:
if c.is_named:
_cpp_collect_type_refs(c, source, True, out)
return
if t in ("type_descriptor", "pointer_declarator", "reference_declarator",
"array_declarator", "type_qualifier", "abstract_pointer_declarator",
"abstract_reference_declarator", "abstract_array_declarator"):
for c in node.children:
if c.is_named:
_cpp_collect_type_refs(c, source, generic, out)
# ── Scala type-ref helpers ───────────────────────────────────────────────────
def _scala_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]]) -> None:
"""Walk a Scala type expression; append (name, role) tuples.
Handles type_identifier, generic_type (List[T]), and common type wrappers."""
if node is None:
return
t = node.type
if t == "type_identifier":
text = _read_text(node, source)
if text:
out.append((text, "generic_arg" if generic else "type"))
return
if t == "generic_type":
base = node.child_by_field_name("type")
if base is None:
for c in node.children:
if c.type == "type_identifier":
base = c
break
if base is not None and base.type == "type_identifier":
text = _read_text(base, source)
if text:
out.append((text, "generic_arg" if generic else "type"))
for c in node.children:
if c.type == "type_arguments":
for arg in c.children:
if arg.is_named:
_scala_collect_type_refs(arg, source, True, out)
return
if t in ("compound_type", "infix_type", "function_type", "tuple_type",
"annotated_type", "projected_type"):
for c in node.children:
if c.is_named:
_scala_collect_type_refs(c, source, generic, out)
def _python_collect_param_refs(params_node, source: bytes) -> list[tuple[str, str]]:
"""Collect type refs from each typed parameter under a `parameters` node."""
out: list[tuple[str, str]] = []
@@ -2370,6 +2470,55 @@ def _extract_generic(path: Path, config: LanguageConfig) -> dict:
if tid.type == "type_identifier":
_emit_java_parent(_read_text(tid, source), "inherits", line)
# Scala: extends_clause carries `extends Base with Trait1 with Trait2`.
# The first base after `extends` is `inherits`; each subsequent
# type after `with` is `mixes_in`. Also walk class_parameters for
# constructor-as-field type references.
if config.ts_module == "tree_sitter_scala":
extend = node.child_by_field_name("extend")
if extend is None:
for c in node.children:
if c.type == "extends_clause":
extend = c
break
if extend is not None:
bases: list[tuple[str, int]] = []
for c in extend.children:
if c.type == "type_identifier":
bases.append((_read_text(c, source), c.start_point[0] + 1))
elif c.type == "generic_type":
base = c.child_by_field_name("type")
if base is None:
for sc in c.children:
if sc.type == "type_identifier":
base = sc
break
if base is not None:
bases.append((_read_text(base, source), c.start_point[0] + 1))
for idx, (base_name, base_line) in enumerate(bases):
rel = "inherits" if idx == 0 else "mixes_in"
base_nid = ensure_named_node(base_name, base_line)
if base_nid != class_nid:
add_edge(class_nid, base_nid, rel, base_line)
for c in node.children:
if c.type != "class_parameters":
continue
for cp in c.children:
if cp.type != "class_parameter":
continue
ptype = cp.child_by_field_name("type")
if ptype is None:
continue
cp_line = cp.start_point[0] + 1
refs: list[tuple[str, str]] = []
_scala_collect_type_refs(ptype, source, False, refs)
for ref_name, role in refs:
ctx = "generic_arg" if role == "generic_arg" else "field"
target_nid = ensure_named_node(ref_name, cp_line)
if target_nid != class_nid:
add_edge(class_nid, target_nid, "references",
cp_line, context=ctx)
# C++-specific: inheritance via base_class_clause (class and struct).
# tree-sitter-cpp shape:
# class_specifier / struct_specifier
@@ -2541,15 +2690,52 @@ def _extract_generic(path: Path, config: LanguageConfig) -> dict:
add_edge(parent_class_nid, target_nid, "references", line, context=ctx)
return
if (config.ts_module == "tree_sitter_scala"
and t == "val_definition"
and parent_class_nid):
type_node = node.child_by_field_name("type")
if type_node is not None:
line = node.start_point[0] + 1
refs: list[tuple[str, str]] = []
_scala_collect_type_refs(type_node, source, False, refs)
for ref_name, role in refs:
ctx = "generic_arg" if role == "generic_arg" else "field"
target_nid = ensure_named_node(ref_name, line)
if target_nid != parent_class_nid:
add_edge(parent_class_nid, target_nid, "references",
line, context=ctx)
# fall through so any call expressions in the initializer get walked
if (config.ts_module == "tree_sitter_cpp"
and t == "field_declaration"
and parent_class_nid):
# Skip method prototypes (field_declaration with a function_declarator
# is a member-function declaration, not a data member).
decls = list(node.children_by_field_name("declarator"))
is_method = any(
d.type == "function_declarator"
or (d.type in ("pointer_declarator", "reference_declarator")
and any(c.type == "function_declarator" for c in d.children))
for d in decls
)
if not is_method:
type_node = node.child_by_field_name("type")
if type_node is not None:
line = node.start_point[0] + 1
refs: list[tuple[str, str]] = []
_cpp_collect_type_refs(type_node, source, False, refs)
for ref_name, role in refs:
ctx = "generic_arg" if role == "generic_arg" else "field"
target_nid = ensure_named_node(ref_name, line)
if target_nid != parent_class_nid:
add_edge(parent_class_nid, target_nid, "references",
line, context=ctx)
# Emit a node for each data member. Use children_by_field_name so we
# only visit declarator children, not the type node (which would give
# us the type name, not the field name). Handles int x, y; via
# multiple declarator fields and static const int MAX = 100; via the
# init_declarator → field_identifier recursion in _get_cpp_func_name.
for decl in node.children_by_field_name("declarator"):
for decl in decls:
name = _get_cpp_func_name(decl, source)
if name:
line = decl.start_point[0] + 1
@@ -2757,6 +2943,73 @@ def _extract_generic(path: Path, config: LanguageConfig) -> dict:
if target_nid != func_nid:
add_edge(func_nid, target_nid, "references", line, context=ctx)
if config.ts_module in ("tree_sitter_c", "tree_sitter_cpp"):
collect = (_cpp_collect_type_refs if config.ts_module == "tree_sitter_cpp"
else _c_collect_type_refs)
return_node = node.child_by_field_name("type")
if return_node is not None:
refs: list[tuple[str, str]] = []
collect(return_node, source, False, refs)
for ref_name, role in refs:
ctx = "generic_arg" if role == "generic_arg" else "return_type"
target_nid = ensure_named_node(ref_name, line)
if target_nid != func_nid:
add_edge(func_nid, target_nid, "references", line, context=ctx)
# function_declarator may be wrapped in pointer/reference declarators
decl = node.child_by_field_name("declarator")
while decl is not None and decl.type in (
"pointer_declarator", "reference_declarator"):
decl = decl.child_by_field_name("declarator")
if decl is not None and decl.type == "function_declarator":
params_node = decl.child_by_field_name("parameters")
if params_node is not None:
for p in params_node.children:
if p.type != "parameter_declaration":
continue
ptype = p.child_by_field_name("type")
if ptype is None:
continue
refs = []
collect(ptype, source, False, refs)
for ref_name, role in refs:
ctx = "generic_arg" if role == "generic_arg" else "parameter_type"
target_nid = ensure_named_node(ref_name, line)
if target_nid != func_nid:
add_edge(func_nid, target_nid, "references",
line, context=ctx)
if config.ts_module == "tree_sitter_scala":
params_node = None
for c in node.children:
if c.type == "parameters":
params_node = c
break
if params_node is not None:
for p in params_node.children:
if p.type != "parameter":
continue
ptype = p.child_by_field_name("type")
if ptype is None:
continue
refs: list[tuple[str, str]] = []
_scala_collect_type_refs(ptype, source, False, refs)
for ref_name, role in refs:
ctx = "generic_arg" if role == "generic_arg" else "parameter_type"
target_nid = ensure_named_node(ref_name, line)
if target_nid != func_nid:
add_edge(func_nid, target_nid, "references",
line, context=ctx)
return_node = node.child_by_field_name("return_type")
if return_node is not None:
refs = []
_scala_collect_type_refs(return_node, source, False, refs)
for ref_name, role in refs:
ctx = "generic_arg" if role == "generic_arg" else "return_type"
target_nid = ensure_named_node(ref_name, line)
if target_nid != func_nid:
add_edge(func_nid, target_nid, "references",
line, context=ctx)
body = _find_body(node, config)
if body:
function_bodies.append((func_nid, body))
@@ -4257,6 +4510,15 @@ def extract_julia(path: Path) -> dict:
file_nid = _make_id(str(path))
add_node(file_nid, path.name, 1)
def ensure_named_node(name: str, line: int) -> str:
nid = _make_id(stem, name)
if nid in seen_ids:
return nid
nid = _make_id(name)
if nid not in seen_ids:
add_node(nid, name, line)
return nid
def _func_name_from_signature(sig_node) -> str | None:
"""Extract function name from a Julia signature node (call_expression > identifier)."""
for child in sig_node.children:
@@ -4310,29 +4572,40 @@ def extract_julia(path: Path) -> dict:
if t == "struct_definition":
# type_head may contain: identifier (simple) or binary_expression (Foo <: Bar)
type_head = next((c for c in node.children if c.type == "type_head"), None)
if type_head:
bin_expr = next((c for c in type_head.children if c.type == "binary_expression"), None)
if bin_expr:
# First identifier is the struct name, last is the supertype
identifiers = [c for c in bin_expr.children if c.type == "identifier"]
if identifiers:
struct_name = _read_text(identifiers[0], source)
struct_nid = _make_id(stem, struct_name)
line = node.start_point[0] + 1
add_node(struct_nid, struct_name, line)
add_edge(scope_nid, struct_nid, "defines", line)
if len(identifiers) >= 2:
super_name = _read_text(identifiers[-1], source)
add_edge(struct_nid, _make_id(stem, super_name), "inherits",
line, confidence="EXTRACTED")
else:
name_node = next((c for c in type_head.children if c.type == "identifier"), None)
if name_node:
struct_name = _read_text(name_node, source)
struct_nid = _make_id(stem, struct_name)
line = node.start_point[0] + 1
add_node(struct_nid, struct_name, line)
add_edge(scope_nid, struct_nid, "defines", line)
if not type_head:
return
struct_name: str | None = None
super_name: str | None = None
bin_expr = next((c for c in type_head.children if c.type == "binary_expression"), None)
if bin_expr:
identifiers = [c for c in bin_expr.children if c.type == "identifier"]
if identifiers:
struct_name = _read_text(identifiers[0], source)
if len(identifiers) >= 2:
super_name = _read_text(identifiers[-1], source)
else:
name_node = next((c for c in type_head.children if c.type == "identifier"), None)
if name_node:
struct_name = _read_text(name_node, source)
if not struct_name:
return
struct_nid = _make_id(stem, struct_name)
line = node.start_point[0] + 1
add_node(struct_nid, struct_name, line)
add_edge(scope_nid, struct_nid, "defines", line)
if super_name:
add_edge(struct_nid, ensure_named_node(super_name, line),
"inherits", line, confidence="EXTRACTED")
# Field types: each `name::Type` lowers to a typed_expression child of struct_definition
for child in node.children:
if child.type == "typed_expression":
type_ids = [c for c in child.children if c.type == "identifier"]
if len(type_ids) >= 2:
field_line = child.start_point[0] + 1
type_name = _read_text(type_ids[-1], source)
type_nid = ensure_named_node(type_name, field_line)
edges.append(_semantic_reference_edge(
struct_nid, type_nid, "field", str_path, field_line))
return
# Abstract type
@@ -4516,6 +4789,62 @@ def extract_fortran(path: Path) -> dict:
return _read_text(child, source).lower()
return None
def ensure_named_node(name: str, line: int) -> str:
nid = _make_id(stem, name)
if nid in seen_ids:
return nid
nid = _make_id(name)
if nid not in seen_ids:
add_node(nid, name, line)
return nid
def emit_signature_refs(scope_node, fn_nid: str, is_function: bool) -> None:
"""Emit references[parameter_type] / references[return_type] edges for
a subroutine/function based on its variable_declaration siblings."""
stmt_type = "function_statement" if is_function else "subroutine_statement"
stmt = next((c for c in scope_node.children if c.type == stmt_type), None)
if stmt is None:
return
param_names: set[str] = set()
params_node = next((c for c in stmt.children if c.type == "parameters"), None)
if params_node is not None:
for c in params_node.children:
if c.type == "identifier":
param_names.add(_read_text(c, source).lower())
result_name: str | None = None
if is_function:
result_node = next((c for c in stmt.children if c.type == "function_result"), None)
if result_node is not None:
res_id = next((c for c in result_node.children if c.type == "identifier"), None)
if res_id is not None:
result_name = _read_text(res_id, source).lower()
else:
# implicit result variable: same name as the function
result_name = _fortran_name(stmt)
for child in scope_node.children:
if child.type != "variable_declaration":
continue
derived = next((c for c in child.children if c.type == "derived_type"), None)
if derived is None:
continue
type_name_node = next((c for c in derived.children if c.type == "type_name"), None)
if type_name_node is None:
continue
type_name = _read_text(type_name_node, source).lower()
for var in child.children:
if var.type != "identifier":
continue
var_name = _read_text(var, source).lower()
var_line = var.start_point[0] + 1
if var_name in param_names:
tgt = ensure_named_node(type_name, var_line)
if tgt != fn_nid:
add_edge(fn_nid, tgt, "references", var_line, context="parameter_type")
elif is_function and var_name == result_name:
tgt = ensure_named_node(type_name, var_line)
if tgt != fn_nid:
add_edge(fn_nid, tgt, "references", var_line, context="return_type")
def walk_calls(node, scope_nid: str) -> None:
if node is None:
return
@@ -4567,6 +4896,18 @@ def extract_fortran(path: Path) -> dict:
walk(child, scope_nid)
return
if t == "derived_type_definition":
stmt = next((c for c in node.children if c.type == "derived_type_statement"), None)
if stmt is not None:
name_node = next((c for c in stmt.children if c.type == "type_name"), None)
if name_node is not None:
type_name = _read_text(name_node, source).lower()
type_nid = _make_id(stem, type_name)
line = node.start_point[0] + 1
add_node(type_nid, type_name, line)
add_edge(scope_nid, type_nid, "defines", line)
return
if t == "subroutine":
stmt = next((c for c in node.children if c.type == "subroutine_statement"), None)
name = _fortran_name(stmt) if stmt else None
@@ -4576,6 +4917,7 @@ def extract_fortran(path: Path) -> dict:
add_node(nid, f"{name}()", line)
add_edge(scope_nid, nid, "defines", line)
scope_bodies.append((nid, node))
emit_signature_refs(node, nid, is_function=False)
for child in node.children:
walk(child, nid)
return
@@ -4589,6 +4931,7 @@ def extract_fortran(path: Path) -> dict:
add_node(nid, f"{name}()", line)
add_edge(scope_nid, nid, "defines", line)
scope_bodies.append((nid, node))
emit_signature_refs(node, nid, is_function=True)
for child in node.children:
walk(child, nid)
return
@@ -5474,6 +5817,30 @@ def extract_powershell(path: Path) -> dict:
return child
return None
def ensure_named_node(name: str, line: int) -> str:
nid = _make_id(stem, name)
if nid in seen_ids:
return nid
nid = _make_id(name)
if nid not in seen_ids:
add_node(nid, name, line)
return nid
def _ps_type_name(type_literal_node) -> str | None:
"""Drill into a type_literal node and return the inner type_identifier text."""
if type_literal_node is None:
return None
for spec in type_literal_node.children:
if spec.type != "type_spec":
continue
for tname in spec.children:
if tname.type != "type_name":
continue
for tid in tname.children:
if tid.type == "type_identifier":
return _read_text(tid, source)
return None
def walk(node, parent_class_nid: str | None = None) -> None:
t = node.type
@@ -5502,6 +5869,17 @@ def extract_powershell(path: Path) -> dict:
walk(child, parent_class_nid=class_nid)
return
if t == "class_property_definition" and parent_class_nid:
type_literal = next((c for c in node.children if c.type == "type_literal"), None)
type_name = _ps_type_name(type_literal)
if type_name:
line = node.start_point[0] + 1
target_nid = ensure_named_node(type_name, line)
if target_nid != parent_class_nid:
add_edge(parent_class_nid, target_nid, "references",
line, context="field")
return
if t == "class_method_definition":
name_node = next((c for c in node.children if c.type == "simple_name"), None)
if name_node:
@@ -5515,6 +5893,32 @@ def extract_powershell(path: Path) -> dict:
method_nid = _make_id(stem, method_name)
add_node(method_nid, f"{method_name}()", line)
add_edge(file_nid, method_nid, "contains", line)
# Return type: type_literal sibling of simple_name
return_type_literal = next(
(c for c in node.children if c.type == "type_literal"), None)
return_type_name = _ps_type_name(return_type_literal)
if return_type_name:
target_nid = ensure_named_node(return_type_name, line)
if target_nid != method_nid:
add_edge(method_nid, target_nid, "references",
line, context="return_type")
# Parameter types: class_method_parameter_list
param_list = next(
(c for c in node.children if c.type == "class_method_parameter_list"), None)
if param_list is not None:
for p in param_list.children:
if p.type != "class_method_parameter":
continue
ptype_literal = next(
(c for c in p.children if c.type == "type_literal"), None)
ptype_name = _ps_type_name(ptype_literal)
if not ptype_name:
continue
p_line = p.start_point[0] + 1
target_nid = ensure_named_node(ptype_name, p_line)
if target_nid != method_nid:
add_edge(method_nid, target_nid, "references",
p_line, context="parameter_type")
body = _find_script_block_body(node)
if body:
function_bodies.append((method_nid, body))
@@ -7005,6 +7409,15 @@ def extract_objc(path: Path) -> dict:
n = node.child_by_field_name(field)
return _read(n) if n else None
def ensure_named_node(name: str, line: int) -> str:
nid = _make_id(stem, name)
if nid in seen_ids:
return nid
nid = _make_id(name)
if nid not in seen_ids:
add_node(nid, name, line)
return nid
def walk(node, parent_nid: str | None = None) -> None:
t = node.type
line = node.start_point[0] + 1
@@ -7047,17 +7460,27 @@ def extract_objc(path: Path) -> dict:
if child.type == ":":
colon_seen = True
elif colon_seen and child.type == "identifier":
super_nid = _make_id(_read(child))
super_nid = ensure_named_node(_read(child), line)
add_edge(cls_nid, super_nid, "inherits", line)
colon_seen = False
elif child.type == "parameterized_arguments":
# protocols adopted
# protocols adopted: @interface Foo : Bar <Proto1, Proto2>
for sub in child.children:
if sub.type == "type_name":
for s in sub.children:
if s.type == "type_identifier":
proto_nid = _make_id(_read(s))
add_edge(cls_nid, proto_nid, "imports", line, context="import")
proto_nid = ensure_named_node(_read(s), line)
add_edge(cls_nid, proto_nid, "implements", line)
elif child.type == "property_declaration":
prop_line = child.start_point[0] + 1
for sub in child.children:
if sub.type == "struct_declaration":
for s in sub.children:
if s.type == "type_identifier":
type_nid = ensure_named_node(_read(s), prop_line)
edges.append(_semantic_reference_edge(
cls_nid, type_nid, "field", str_path, prop_line))
break
elif child.type == "method_declaration":
walk(child, cls_nid)
return