diff --git a/graphify/extract.py b/graphify/extract.py index a6c4fda25..0fe689033 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -1535,8 +1535,15 @@ _CSHARP_CONFIG = LanguageConfig( ) _KOTLIN_CONFIG = LanguageConfig( + # `type_alias` is `typealias ClickHandler = (Int) -> Unit`. Without it the + # declaration was dropped, yet an in-file field/parameter typed as the alias + # still resolved its name through `ensure_named_node` and minted a + # *sourceless* stub — a real in-file type losing to a sourceless god node + # (provenance). Scala and TypeScript already model type aliases as nodes; the + # alias has an `identifier` name (covered by `name_fallback_child_types`) and + # no body, so it emits a clean empty type node those references bind to. ts_module="tree_sitter_kotlin", - class_types=frozenset({"class_declaration", "object_declaration"}), + class_types=frozenset({"class_declaration", "object_declaration", "type_alias"}), function_types=frozenset({"function_declaration"}), # Grammar 1.1.0 (PyPI tree_sitter_kotlin) names the import node `import`; # older forks use `import_header`. Accept both (#2526). diff --git a/tests/test_kotlin_typealias_declarations.py b/tests/test_kotlin_typealias_declarations.py new file mode 100644 index 000000000..c1e516615 --- /dev/null +++ b/tests/test_kotlin_typealias_declarations.py @@ -0,0 +1,51 @@ +"""A Kotlin `typealias` is a named type and must be extracted as a sourced node. + +`typealias ClickHandler = (Int) -> Unit` parses as `type_alias`, which was in +neither `class_types` nor `function_types`, so the declaration was dropped. But +a property or parameter typed as the alias (`var onClick: ClickHandler?`, +`fun handler(h: ClickHandler)`) still ran the name `ClickHandler` through +`ensure_named_node`, which minted a *sourceless* stub and pointed the +`references` edge at it — even though the alias is declared in the same file. +That is a provenance violation (a real source-backed declaration losing to a +sourceless god node). Scala and TypeScript already model type aliases as nodes; +`type_alias` has an `identifier` name and no body, so adding it to the Kotlin +`class_types` emits a clean empty type node those references bind to. +""" +from pathlib import Path + +from graphify.extract import extract + + +def _extract(tmp_path, body: str): + (tmp_path / "m.kt").write_text(body) + return extract([tmp_path / "m.kt"], cache_root=tmp_path, parallel=False) + + +def _node(r, label): + return next((n for n in r["nodes"] if n.get("label") == label), None) + + +def test_typealias_is_a_sourced_node_not_a_sourceless_stub(tmp_path): + r = _extract( + tmp_path, + "typealias ClickHandler = (Int) -> Unit\n" + "class Button {\n" + " var onClick: ClickHandler? = null\n" + " fun handler(h: ClickHandler) { onClick = h }\n" + "}\n", + ) + alias = _node(r, "ClickHandler") + assert alias is not None, "typealias declaration was dropped" + assert alias.get("source_file"), ( + f"typealias ClickHandler is a sourceless god node: {alias}" + ) + assert not any( + (not n.get("source_file")) and n.get("label") == "ClickHandler" + for n in r["nodes"] + ) + refs_to_alias = { + e["source"] + for e in r["edges"] + if e["relation"] == "references" and e["target"] == alias["id"] + } + assert refs_to_alias, "references to the typealias did not bind to it"