From 0061624561a87a7137e1f78f6e9967a67e153ab2 Mon Sep 17 00:00:00 2001 From: kebwlmbhee Date: Tue, 14 Jul 2026 10:14:30 +0800 Subject: [PATCH] fix(extract): filter Kotlin builtin/stdlib types from references graph _kotlin_collect_type_refs had no equivalent of _JAVA_BUILTIN_TYPES / _PYTHON_ANNOTATION_NOISE / _GO_PREDECLARED_TYPES, so every Boolean/String/ Int/List/... type annotation in a .kt file became a real graph node/edge. Unrelated files sharing only these language-level types were getting merged into the same community by cluster-only. Add _KOTLIN_BUILTIN_TYPES (kotlin.* scalars, collections, throwables - deliberately not Android/JVM framework types like Context/View/Bundle, matching how _JAVA_BUILTIN_TYPES doesn't filter framework types either) and check it plus _JAVA_BUILTIN_TYPES (Kotlin freely references java.util.Date/UUID/etc) at the three call sites that previously appended unconditionally. Deliberately excludes "Result" from the filter list even though kotlin.Result exists - it collides with the very common user-defined sealed-class name (confirmed against this repo's own sample.kt fixture, which defines its own Result). Verified on a ~400-file Android/Kotlin project: 3693->3239 nodes (-12%), 6481->5221 edges (-19%), with a previously merged 5-file grab-bag community cleanly splitting out an unrelated screen-dimension utility. --- graphify/extractors/engine.py | 35 ++++++++++++++++++++++++++++++++--- tests/test_languages.py | 21 +++++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/graphify/extractors/engine.py b/graphify/extractors/engine.py index 90c95cc49..723bef7ca 100644 --- a/graphify/extractors/engine.py +++ b/graphify/extractors/engine.py @@ -607,6 +607,35 @@ def _php_method_return_type_node(method_node): return c return None +# Kotlin stdlib scalar/collection/core types that appear constantly as type +# annotations but carry no useful semantic meaning as graph nodes (mirrors +# _JAVA_BUILTIN_TYPES / _PYTHON_ANNOTATION_NOISE / _GO_PREDECLARED_TYPES). +# Kotlin compiles to the JVM and freely references java.* types too, so this +# is combined with _JAVA_BUILTIN_TYPES at the call site rather than duplicated. +_KOTLIN_BUILTIN_TYPES = frozenset({ + # kotlin — scalars & core + "Any", "Unit", "Nothing", "Boolean", "Byte", "Short", "Int", "Long", + "Float", "Double", "Char", "String", "CharSequence", "Number", + "Comparable", "Enum", "Annotation", "Pair", "Triple", "Lazy", + "Function", + # kotlin — throwables + "Throwable", "Exception", "RuntimeException", "Error", + "IllegalArgumentException", "IllegalStateException", "NullPointerException", + "IndexOutOfBoundsException", "ClassCastException", "NumberFormatException", + "ArithmeticException", "UnsupportedOperationException", + "NoSuchElementException", "ConcurrentModificationException", + "StackOverflowError", "OutOfMemoryError", "AssertionError", + "InterruptedException", + # kotlin.collections + "Array", "List", "MutableList", "ArrayList", "Set", "MutableSet", + "HashSet", "LinkedHashSet", "Map", "MutableMap", "HashMap", + "LinkedHashMap", "Collection", "MutableCollection", "Iterable", + "MutableIterable", "Iterator", "MutableIterator", "ListIterator", + "MutableListIterator", "Sequence", "Comparator", + # kotlin.text + "Regex", "MatchResult", "StringBuilder", +}) + def _kotlin_user_type_name(user_type_node, source: bytes) -> str | None: """Return the head identifier text from a Kotlin user_type node (without generics).""" if user_type_node is None: @@ -636,14 +665,14 @@ def _kotlin_collect_type_refs(node, source: bytes, generic: bool, out: list[tupl for c in node.children: if c.type in ("identifier", "type_identifier"): text = _read_text(c, source) - if text: + if text and text not in _KOTLIN_BUILTIN_TYPES and text not in _JAVA_BUILTIN_TYPES: out.append((text, "generic_arg" if generic else "type")) break if c.type == "simple_user_type": for sub in c.children: if sub.type in ("identifier", "type_identifier"): text = _read_text(sub, source) - if text: + if text and text not in _KOTLIN_BUILTIN_TYPES and text not in _JAVA_BUILTIN_TYPES: out.append((text, "generic_arg" if generic else "type")) break break @@ -659,7 +688,7 @@ def _kotlin_collect_type_refs(node, source: bytes, generic: bool, out: list[tupl return if t in ("identifier", "type_identifier"): text = _read_text(node, source) - if text: + if text and text not in _KOTLIN_BUILTIN_TYPES and text not in _JAVA_BUILTIN_TYPES: out.append((text, "generic_arg" if generic else "type")) return if t in ("nullable_type", "parenthesized_type", "type_reference"): diff --git a/tests/test_languages.py b/tests/test_languages.py index f610fbc40..f36ea7603 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -656,6 +656,27 @@ def test_kotlin_parameter_return_generic_and_field_contexts(): assert ("run", "DataProcessor") in _edge_labels(r, "references", "generic_arg") assert ("DataProcessor", "Result") in _edge_labels(r, "references", "field") +def test_kotlin_builtin_types_not_emitted_as_references(): + # kotlin.* scalar/collection/core types used as parameter, return, or field + # types carry no useful graph meaning: they never resolve to a project node, + # so emitting `references` edges to them is pure noise (mirrors the Java + # _JAVA_BUILTIN_TYPES / Python _PYTHON_ANNOTATION_NOISE handling). + r = extract_kotlin(FIXTURES / "sample.kt") + ref_targets = {target for (_, target) in _edge_labels(r, "references")} + for builtin in ("String", "Int"): + assert builtin not in ref_targets, ( + f"builtin type {builtin!r} should not be a references target" + ) + +def test_kotlin_user_types_still_emit_references(): + # Guard against over-filtering: a user-defined class sharing its name with a + # common domain-modeling identifier (Result) must still resolve to a real + # edge - the builtin filter is a fixed name list, so it must stay narrow + # enough not to swallow common user-chosen names like a sealed-class "Result". + r = extract_kotlin(FIXTURES / "sample.kt") + assert ("DataProcessor", "Result") in _edge_labels(r, "references", "field") + assert ("run", "DataProcessor") in _edge_labels(r, "references", "parameter_type") + # ── Scala ─────────────────────────────────────────────────────────────────────