Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions graphify/cross_repo_calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
"cpp": frozenset({".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx", ".h", ".cu", ".cuh"}),
"csharp": frozenset({".cs"}),
"java": frozenset({".java"}),
# A Kotlin call may be answered by any JVM-family declaration: the classpath is one
# namespace, and a Kotlin module over a Java library is the common Android shape.
"kotlin": frozenset({".kt", ".kts", ".java", ".scala", ".groovy", ".gradle"}),
"swift": frozenset({".swift"}),
}

Expand Down
112 changes: 111 additions & 1 deletion graphify/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@

from graphify.symbol_resolution import resolve_bash_source_edges # noqa: E402

from graphify.extractors.engine import REFERENCE_CONTEXTS, _CSHARP_TYPE_PARAMETER_SCOPE_DECLARATIONS, _C_PRIMITIVE_TYPE_NODES, _JAVA_BUILTIN_TYPES, _JAVA_TYPE_PARAMETER_SCOPE_DECLARATIONS, _JS_FUNCTION_VALUE_TYPES, _JS_SCOPE_BOUNDARY, _PYTHON_ANNOTATION_NOISE, _PYTHON_TYPE_CONTAINERS, _RUBY_CLASS_FACTORIES, _c_collect_type_refs, _cpp_collect_type_refs, _cpp_declarator_name, _cpp_local_var_types, _csharp_attribute_names, _csharp_classify_base, _csharp_collect_type_refs, _csharp_extra_walk, _csharp_namespace_id, _csharp_namespace_name, _csharp_pre_scan_interfaces, _csharp_type_parameters_in_scope, _dynamic_import_js, _extract_generic, _find_body, _find_require_call, _get_cpp_func_name, _java_annotation_names, _java_collect_type_refs, _java_extra_walk, _java_type_parameters_in_scope, _js_collect_pattern_idents, _js_dispatch_value_idents, _js_extra_walk, _js_local_bound_names, _js_member_assignment_target, _js_module_bound_names, _kotlin_collect_type_refs, _kotlin_function_return_type_node, _kotlin_property_type_node, _kotlin_user_type_name, _php_collect_type_refs, _php_method_return_type_node, _php_name_text, _python_collect_assignment_targets, _python_collect_param_refs, _python_collect_type_refs, _python_local_bound_names, _python_module_bound_names, _python_param_names, _read_csharp_type_name, _require_imports_js, _ruby_const_last_name, _ruby_extra_walk, _ruby_local_class_bindings, _ruby_new_class_name, _scala_collect_type_refs, _semantic_reference_edge, _source_location, _swift_classify_base, _swift_collect_type_refs, _swift_constructor_type, _swift_declaration_keyword, _swift_extra_walk, _swift_local_var_types, _swift_pre_scan, _swift_property_name, _swift_property_type_node, _swift_receiver_name, _swift_user_type_name, _ts_decorator_name, _ts_descendant_decorators, _ts_emit_decorator_edges, _ts_extra_walk, _ts_method_name, _ts_receiver_type_table # noqa: E402,F401
from graphify.extractors.engine import REFERENCE_CONTEXTS, _CSHARP_TYPE_PARAMETER_SCOPE_DECLARATIONS, _C_PRIMITIVE_TYPE_NODES, _JAVA_BUILTIN_TYPES, _JAVA_TYPE_PARAMETER_SCOPE_DECLARATIONS, _JS_FUNCTION_VALUE_TYPES, _JS_SCOPE_BOUNDARY, _KOTLIN_BUILTIN_TYPES, _PYTHON_ANNOTATION_NOISE, _PYTHON_TYPE_CONTAINERS, _RUBY_CLASS_FACTORIES, _c_collect_type_refs, _cpp_collect_type_refs, _cpp_declarator_name, _cpp_local_var_types, _csharp_attribute_names, _csharp_classify_base, _csharp_collect_type_refs, _csharp_extra_walk, _csharp_namespace_id, _csharp_namespace_name, _csharp_pre_scan_interfaces, _csharp_type_parameters_in_scope, _dynamic_import_js, _extract_generic, _find_body, _find_require_call, _get_cpp_func_name, _java_annotation_names, _java_collect_type_refs, _java_extra_walk, _java_type_parameters_in_scope, _js_collect_pattern_idents, _js_dispatch_value_idents, _js_extra_walk, _js_local_bound_names, _js_member_assignment_target, _js_module_bound_names, _kotlin_collect_type_refs, _kotlin_function_return_type_node, _kotlin_property_type_node, _kotlin_user_type_name, _php_collect_type_refs, _php_method_return_type_node, _php_name_text, _python_collect_assignment_targets, _python_collect_param_refs, _python_collect_type_refs, _python_local_bound_names, _python_module_bound_names, _python_param_names, _read_csharp_type_name, _require_imports_js, _ruby_const_last_name, _ruby_extra_walk, _ruby_local_class_bindings, _ruby_new_class_name, _scala_collect_type_refs, _semantic_reference_edge, _source_location, _swift_classify_base, _swift_collect_type_refs, _swift_constructor_type, _swift_declaration_keyword, _swift_extra_walk, _swift_local_var_types, _swift_pre_scan, _swift_property_name, _swift_property_type_node, _swift_receiver_name, _swift_user_type_name, _ts_decorator_name, _ts_descendant_decorators, _ts_emit_decorator_edges, _ts_extra_walk, _ts_method_name, _ts_receiver_type_table # noqa: E402,F401

from graphify.extractors.pascal import _PAS_BEGIN_END_TOKEN_RE, _PAS_CALL_RE, _PAS_END_SEMI_RE, _PAS_IMPL_HEADER_RE, _PAS_KEYWORDS, _PAS_METHOD_DECL_RE, _PAS_MODULE_RE, _PAS_TOKEN_RE, _PAS_TYPE_HEADER_RE, _PAS_USES_RE, _extract_pascal_regex, _pascal_find_body, _pascal_split_bases, _pascal_split_sections, _pascal_split_uses, _pascal_strip_comments, extract_pascal # noqa: E402,F401

Expand Down Expand Up @@ -4414,6 +4414,110 @@ def _resolve_csharp_qualified_calls(
})


def _resolve_kotlin_member_calls(
per_file: list[dict],
all_nodes: list[dict],
all_edges: list[dict],
) -> None:
"""Resolve Kotlin member calls (``greeter.greet()``) through the receiver's type.

The shared cross-file pass skips member calls, so a call on a typed receiver whose
method is declared in another file resolved to nothing at all. The per-file
``kotlin_type_table`` names the declared type of every property, primary-constructor
parameter, function parameter and local binding; this pass looks the receiver up
there, takes the single class/object declaring that type, and emits the ``calls``
edge to its member — EXTRACTED when the receiver names the type in source
(``Registry.register()``), INFERRED when the type came from the table.

A receiver typed to a class this corpus declares nowhere is parked on the caller for
a merged graph to finish (#3152).
"""
raw = [
rc
for result in per_file
for rc in result.get("raw_calls", [])
if rc.get("lang") == "kotlin" and rc.get("is_member_call")
and rc.get("receiver") and rc.get("callee") and rc.get("caller_nid")
]
if not raw:
return
type_table_by_file: dict[str, dict[str, str]] = {}
for result in per_file:
tt = result.get("kotlin_type_table")
if tt and tt.get("path"):
type_table_by_file[tt["path"]] = tt.get("table", {})

def _key(label: str) -> str:
return re.sub(r"[^a-zA-Z0-9]+", "", str(label)).lower()

# A genuine declaration is the target of a `contains` edge from its file node; a bare
# type reference mints a same-label stub that would otherwise make a real name ambiguous.
# The whole JVM family counts, `.java` included: one classpath makes interop a real
# answer, while a same-named class outside the family is a collision.
contained = {e.get("target") for e in all_edges if e.get("relation") == "contains"}
type_def_nids: dict[str, list[str]] = {}
node_by_id: dict[str, dict] = {}
for n in all_nodes:
node_by_id[n.get("id")] = n
if (_lang_family(n.get("source_file")) == "jvm"
and n.get("id") in contained and _is_type_like_definition(n)):
type_def_nids.setdefault(_key(n.get("label", "")), []).append(n["id"])

method_index: dict[tuple[str, str], str] = {}
for e in all_edges:
if e.get("relation") != "method":
continue
tnode = node_by_id.get(e.get("target"))
if tnode is not None:
method_index[(e.get("source"), _key(tnode.get("label", "")))] = e["target"]

existing_pairs = {(e.get("source"), e.get("target")) for e in all_edges}
for rc in raw:
receiver, callee, caller = rc["receiver"], rc["callee"], rc["caller_nid"]
# A capitalized receiver is the type itself — a companion/static call or an
# `object` singleton. Kotlin imports the class name into scope rather than a
# module alias, so unlike TS/JS this spelling is not also the namespace idiom.
if receiver[:1].isupper():
type_name = receiver
type_qualified = True
else:
type_name = type_table_by_file.get(rc.get("source_file", ""), {}).get(receiver)
type_qualified = False
if not type_name:
continue
if (type_name in _LANGUAGE_BUILTIN_GLOBALS or type_name in _KOTLIN_BUILTIN_TYPES
or type_name in _JAVA_BUILTIN_TYPES):
continue
type_defs = type_def_nids.get(_key(type_name), [])
if not type_defs:
# Declared nowhere here — usually "in a repo this build does not contain",
# so park it for the merge (#3152). The extractor's `lang` tag already says
# who is asking, so no suffix sniff is needed.
_park_unresolved_member_call(
node_by_id.get(caller), callee, type_name, "kotlin", rc,
)
continue
if len(type_defs) != 1: # ambiguous -> bail (god-node guard)
continue
target = method_index.get((type_defs[0], _key(callee)))
if not target or target == caller or (caller, target) in existing_pairs:
continue
existing_pairs.add((caller, target))
all_edges.append({
"source": caller,
"target": target,
"relation": "calls",
"context": "call",
"confidence": "EXTRACTED" if type_qualified else "INFERRED",
# The rubric's discrete INFERRED scale (references/extraction-spec.md):
# a single-definition type-table hit is the high-confidence rung.
"confidence_score": 1.0 if type_qualified else 0.85,
"source_file": rc.get("source_file", ""),
"source_location": rc.get("source_location"),
"weight": 1.0,
})


def _resolve_kotlin_qualified_calls(
per_file: list[dict],
all_nodes: list[dict],
Expand Down Expand Up @@ -4591,6 +4695,12 @@ def _resolve_kotlin_qualified_calls(
resolve_pascal_inherited_calls,
)
)
# Kotlin receiver-typed member-call resolution: `greeter.greet()` where the method is
# declared in another file. The shared pass skips member calls, so these had no edge.
register_language_resolver(
LanguageResolver("kotlin_member_calls", frozenset({".kt", ".kts"}),
_resolve_kotlin_member_calls)
)
# Kotlin fully-qualified call resolution (#2550): `com.pkg.Fn()` /
# `com.pkg.Object.method()` raw_calls the shared pass skips (member calls with
# no receiver). Runs in the tail registry like the other member-call resolvers;
Expand Down
Loading
Loading