/* * test_kotlin_lsp.c — Tests for Kotlin LSP type-aware call resolution. * * Coverage areas (mirrors fwcd kotlin-language-server's reference test * suite at a feature level — we don't run their JVM tests, we test * equivalent semantic outcomes through our pure-C resolver): * * 1. Package + import resolution (default, explicit, alias, wildcard) * 2. Top-level function calls * 3. Class instantiation (constructor calls) * 4. Method dispatch on instance * 5. Companion object dispatch (Foo.bar() static-style) * 6. Object singleton dispatch * 7. Extension functions * 8. Inheritance / super-method lookup * 9. Smart-casts after `is`, `as`, `as?` * 10. Nullable types and safe calls (?.) * 11. Lambda `it` parameter type propagation * 12. Scope functions (let, run, also, apply, with, takeIf, takeUnless) * 13. Type aliases * 14. Generic type parameters (basic) * 15. Constructor parameter properties (val/var in primary constructor) * 16. Enum classes * 17. Data classes * 18. Sealed classes * 19. Interface dispatch * 20. Stdlib resolution (println, listOf, mapOf, ...) * 21. Quality parity benchmarks vs tree-sitter-only baseline */ #include "test_framework.h" #include "cbm.h" #include "lsp/kotlin_lsp.h" #include /* ── helpers ───────────────────────────────────────────────── */ static CBMFileResult *extract_kotlin(const char *source) { return cbm_extract_file(source, (int)strlen(source), CBM_LANG_KOTLIN, "test", "Main.kt", 0, NULL, NULL); } static CBMFileResult *extract_kotlin_path(const char *source, const char *rel_path) { return cbm_extract_file(source, (int)strlen(source), CBM_LANG_KOTLIN, "test", rel_path, 0, NULL, NULL); } /* Search resolved_calls for a match where caller contains callerSub * and callee contains calleeSub. Returns index or -1. */ static int find_resolved(const CBMFileResult *r, const char *callerSub, const char *calleeSub) { for (int i = 0; i < r->resolved_calls.count; i++) { const CBMResolvedCall *rc = &r->resolved_calls.items[i]; if (rc->caller_qn && strstr(rc->caller_qn, callerSub) && rc->callee_qn && strstr(rc->callee_qn, calleeSub)) { return i; } } return -1; } static int require_resolved(const CBMFileResult *r, const char *callerSub, const char *calleeSub) { int idx = find_resolved(r, callerSub, calleeSub); if (idx < 0) { printf(" MISSING resolved call: caller~%s -> callee~%s (have %d)\n", callerSub, calleeSub, r->resolved_calls.count); for (int i = 0; i < r->resolved_calls.count; i++) { const CBMResolvedCall *rc = &r->resolved_calls.items[i]; printf(" %s -> %s [%s %.2f]\n", rc->caller_qn ? rc->caller_qn : "(null)", rc->callee_qn ? rc->callee_qn : "(null)", rc->strategy ? rc->strategy : "(null)", rc->confidence); } } return idx; } static int count_resolved(const CBMFileResult *r, const char *callerSub, const char *calleeSub) { int n = 0; for (int i = 0; i < r->resolved_calls.count; i++) { const CBMResolvedCall *rc = &r->resolved_calls.items[i]; if (rc->caller_qn && strstr(rc->caller_qn, callerSub) && rc->callee_qn && strstr(rc->callee_qn, calleeSub)) { n++; } } return n; } /* ── 1. Package + import resolution ────────────────────────── */ TEST(ktlsp_package_set) { CBMFileResult *r = extract_kotlin("package com.example.foo\n\n" "fun greet() { println(\"hi\") }\n"); ASSERT_NOT_NULL(r); /* greet should resolve to println in kotlin.io */ ASSERT_GTE(require_resolved(r, "greet", "println"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_explicit_import_function) { CBMFileResult *r = extract_kotlin("package com.example\n\n" "import kotlin.io.println\n\n" "fun greet() { println(\"hi\") }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "greet", "println"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_explicit_import_type) { CBMFileResult *r = extract_kotlin("package com.example\n\n" "import java.io.File\n\n" "fun open(): File = File(\"/tmp/x\")\n"); ASSERT_NOT_NULL(r); /* File() constructor resolved */ ASSERT_GTE(require_resolved(r, "open", "File"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_import_alias) { CBMFileResult *r = extract_kotlin("package com.example\n\n" "import java.io.File as JFile\n\n" "fun open(): JFile = JFile(\"/tmp/x\")\n"); ASSERT_NOT_NULL(r); /* The constructor target should still resolve to java.io.File */ ASSERT_GTE(require_resolved(r, "open", "File"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_wildcard_import) { CBMFileResult *r = extract_kotlin("package com.example\n\n" "import kotlin.collections.*\n\n" "fun build() = listOf(1, 2, 3)\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "build", "listOf"), 0); cbm_free_result(r); PASS(); } /* ── 2. Top-level function calls ───────────────────────────── */ TEST(ktlsp_top_level_call_default_imports) { CBMFileResult *r = extract_kotlin("fun main() { println(\"hi\") }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "main", "println"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_top_level_listof) { CBMFileResult *r = extract_kotlin("fun build() { val xs = listOf(1, 2, 3) }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "build", "listOf"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_top_level_setof_mapof) { CBMFileResult *r = extract_kotlin("fun build() {\n" " val s = setOf(1, 2, 3)\n" " val m = mapOf(\"a\" to 1, \"b\" to 2)\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "build", "setOf"), 0); ASSERT_GTE(require_resolved(r, "build", "mapOf"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_intra_file_top_level) { CBMFileResult *r = extract_kotlin("package myapp\n\n" "fun helper(x: Int): Int = x + 1\n" "fun main() { val r = helper(42) }\n"); ASSERT_NOT_NULL(r); /* main should resolve to helper */ ASSERT_GTE(require_resolved(r, "main", "helper"), 0); cbm_free_result(r); PASS(); } /* ── 3. Class instantiation ────────────────────────────────── */ TEST(ktlsp_class_constructor) { CBMFileResult *r = extract_kotlin("class Greeter(val name: String) {\n" " fun greet() = println(\"Hello, $name\")\n" "}\n" "fun main() { val g = Greeter(\"world\"); g.greet() }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "main", "Greeter"), 0); /* Method dispatch on g */ ASSERT_GTE(require_resolved(r, "main", "greet"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_data_class) { CBMFileResult *r = extract_kotlin("data class User(val id: Int, val name: String)\n" "fun create(): User = User(1, \"Alice\")\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "create", "User"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_secondary_constructor) { CBMFileResult *r = extract_kotlin("class Box {\n" " constructor() {}\n" " constructor(x: Int) {}\n" " fun open() {}\n" "}\n" "fun main() { val b = Box(); b.open() }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "main", "Box"), 0); ASSERT_GTE(require_resolved(r, "main", "open"), 0); cbm_free_result(r); PASS(); } /* ── 4. Method dispatch ────────────────────────────────────── */ TEST(ktlsp_method_on_known_type) { CBMFileResult *r = extract_kotlin("class Service {\n" " fun query(sql: String): String = \"\"\n" "}\n" "fun work(s: Service) { s.query(\"SELECT 1\") }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "work", "query"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_method_chain) { CBMFileResult *r = extract_kotlin("class Builder {\n" " fun add(s: String): Builder = this\n" " fun build(): String = \"\"\n" "}\n" "fun make(b: Builder): String = b.add(\"x\").build()\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "make", "add"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_string_method_dispatch) { CBMFileResult *r = extract_kotlin("fun shout(s: String): String = s.uppercase()\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "shout", "uppercase"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_list_method_dispatch) { CBMFileResult *r = extract_kotlin("fun first_n(xs: List): List = xs.take(3)\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "first_n", "take"), 0); cbm_free_result(r); PASS(); } /* ── 5. Companion object dispatch ──────────────────────────── */ TEST(ktlsp_companion_object_call) { CBMFileResult *r = extract_kotlin("class Logger {\n" " companion object {\n" " fun create(): Logger = Logger()\n" " }\n" "}\n" "fun main() { val log = Logger.create() }\n"); ASSERT_NOT_NULL(r); /* Foo.bar() with bar in Companion */ ASSERT_GTE(require_resolved(r, "main", "create"), 0); cbm_free_result(r); PASS(); } /* ── 6. Object singleton ───────────────────────────────────── */ TEST(ktlsp_object_singleton) { CBMFileResult *r = extract_kotlin("object Constants {\n" " fun get(key: String): String = \"\"\n" "}\n" "fun read() { val v = Constants.get(\"name\") }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "read", "get"), 0); cbm_free_result(r); PASS(); } /* ── 7. Extension functions ────────────────────────────────── */ TEST(ktlsp_extension_function) { CBMFileResult *r = extract_kotlin("fun String.shout(): String = this.uppercase() + \"!\"\n" "fun cheer(s: String): String = s.shout()\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "cheer", "shout"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_extension_method_on_list) { CBMFileResult *r = extract_kotlin("fun List.sumPlus(n: Int): Int = this.sum() + n\n" "fun calc(xs: List): Int = xs.sumPlus(10)\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "calc", "sumPlus"), 0); cbm_free_result(r); PASS(); } /* ── 8. Inheritance ────────────────────────────────────────── */ TEST(ktlsp_super_call) { CBMFileResult *r = extract_kotlin("open class Animal {\n" " open fun speak() = println(\"...\")\n" "}\n" "class Dog : Animal() {\n" " override fun speak() {\n" " super.speak()\n" " println(\"Woof\")\n" " }\n" "}\n"); ASSERT_NOT_NULL(r); /* Inside Dog.speak, the super.speak should resolve. */ ASSERT_GTE(require_resolved(r, "speak", "speak"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_interface_dispatch) { CBMFileResult *r = extract_kotlin("interface Closer { fun close() }\n" "class FileHandle : Closer { override fun close() {} }\n" "fun shutdown(c: Closer) { c.close() }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "shutdown", "close"), 0); cbm_free_result(r); PASS(); } /* ── 9. Smart casts ────────────────────────────────────────── */ TEST(ktlsp_smartcast_is) { CBMFileResult *r = extract_kotlin("class Animal { fun speak() {} }\n" "fun handle(x: Any) {\n" " if (x is Animal) {\n" " x.speak()\n" " }\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "handle", "speak"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_smartcast_as) { CBMFileResult *r = extract_kotlin("class Animal { fun speak() {} }\n" "fun handle(x: Any) { (x as Animal).speak() }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "handle", "speak"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_when_smartcast) { CBMFileResult *r = extract_kotlin("interface Shape { fun area(): Double }\n" "class Square(val side: Double) : Shape {\n" " override fun area() = side * side\n" "}\n" "fun describe(s: Any): Double = when (s) {\n" " is Square -> s.area()\n" " else -> 0.0\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "describe", "area"), 0); cbm_free_result(r); PASS(); } /* ── 10. Safe calls / nullable ─────────────────────────────── */ TEST(ktlsp_safe_call) { CBMFileResult *r = extract_kotlin("class User { fun getName(): String = \"\" }\n" "fun displayName(u: User?): String? = u?.getName()\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "displayName", "getName"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_not_null_assertion) { CBMFileResult *r = extract_kotlin("class User { fun getName(): String = \"\" }\n" "fun mustHave(u: User?): String = u!!.getName()\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "mustHave", "getName"), 0); cbm_free_result(r); PASS(); } /* ── 11. Lambda `it` parameter ─────────────────────────────── */ TEST(ktlsp_lambda_it_string) { CBMFileResult *r = extract_kotlin("fun upper(s: String): String = s.let { it.uppercase() }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "upper", "uppercase"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_lambda_it_object) { CBMFileResult *r = extract_kotlin("class User { fun login() {} }\n" "fun signIn(u: User) { u.let { it.login() } }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "signIn", "login"), 0); cbm_free_result(r); PASS(); } /* ── 12. Scope functions ───────────────────────────────────── */ TEST(ktlsp_scope_let) { CBMFileResult *r = extract_kotlin("fun upper(s: String?): String? = s?.let { it.uppercase() }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "upper", "uppercase"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_scope_apply) { CBMFileResult *r = extract_kotlin("class Builder { fun setName(n: String) {} }\n" "fun build() = Builder().apply { setName(\"x\") }\n"); ASSERT_NOT_NULL(r); /* `Builder()` is a constructor */ ASSERT_GTE(require_resolved(r, "build", "Builder"), 0); cbm_free_result(r); PASS(); } /* ── 13. Type aliases ──────────────────────────────────────── */ TEST(ktlsp_typealias) { CBMFileResult *r = extract_kotlin("typealias UserId = Int\n" "fun id(): UserId = 42\n" "fun show(id: UserId): String = id.toString()\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "show", "toString"), 0); cbm_free_result(r); PASS(); } /* ── 14. Enums ────────────────────────────────────────────── */ TEST(ktlsp_enum_class) { CBMFileResult *r = extract_kotlin("enum class Color { RED, GREEN, BLUE }\n" "fun pick(): Color = Color.RED\n"); ASSERT_NOT_NULL(r); /* Color.RED — for our purposes, the enum entry access doesn't need a * resolved call edge. We just verify it doesn't crash. */ ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } /* ── 15. Sealed classes ────────────────────────────────────── */ TEST(ktlsp_sealed_when) { CBMFileResult *r = extract_kotlin("sealed class Event\n" "class Login(val user: String) : Event()\n" "class Logout : Event()\n" "fun handle(e: Event): String = when (e) {\n" " is Login -> e.user\n" " is Logout -> \"bye\"\n" "}\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } /* ── 16. Generics (basic) ──────────────────────────────────── */ TEST(ktlsp_generic_call) { CBMFileResult *r = extract_kotlin("class Box(val value: T) {\n" " fun unwrap(): T = value\n" "}\n" "fun get(b: Box): String = b.unwrap()\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "get", "unwrap"), 0); cbm_free_result(r); PASS(); } /* ── 17. Stdlib resolution ─────────────────────────────────── */ TEST(ktlsp_stdlib_println) { CBMFileResult *r = extract_kotlin("fun main() {\n" " println(\"a\")\n" " print(\"b\")\n" " error(\"c\")\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "main", "println"), 0); ASSERT_GTE(require_resolved(r, "main", "print"), 0); ASSERT_GTE(require_resolved(r, "main", "error"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_stdlib_collection_builders) { CBMFileResult *r = extract_kotlin("fun build() {\n" " val a = arrayOf(1, 2, 3)\n" " val l = listOf(1, 2, 3)\n" " val ml = mutableListOf(1, 2, 3)\n" " val m = mapOf(\"a\" to 1)\n" " val mm = mutableMapOf()\n" " val s = setOf(1, 2, 3)\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "build", "arrayOf"), 0); ASSERT_GTE(require_resolved(r, "build", "listOf"), 0); ASSERT_GTE(require_resolved(r, "build", "mutableListOf"), 0); ASSERT_GTE(require_resolved(r, "build", "mapOf"), 0); ASSERT_GTE(require_resolved(r, "build", "mutableMapOf"), 0); ASSERT_GTE(require_resolved(r, "build", "setOf"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_stdlib_require_check) { CBMFileResult *r = extract_kotlin("fun validate(x: Int) {\n" " require(x > 0)\n" " check(x < 100)\n" " requireNotNull(x)\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "validate", "require"), 0); ASSERT_GTE(require_resolved(r, "validate", "check"), 0); ASSERT_GTE(require_resolved(r, "validate", "requireNotNull"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_stdlib_string_chain) { CBMFileResult *r = extract_kotlin("fun proc(s: String): String =\n" " s.trim().uppercase().replace(\"x\", \"y\")\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "proc", "trim"), 0); ASSERT_GTE(require_resolved(r, "proc", "uppercase"), 0); ASSERT_GTE(require_resolved(r, "proc", "replace"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_stdlib_collection_pipe) { CBMFileResult *r = extract_kotlin("fun pipe(xs: List): Int = xs.filter { it > 0 }.map { it * 2 }.sum()\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "pipe", "filter"), 0); ASSERT_GTE(require_resolved(r, "pipe", "map"), 0); ASSERT_GTE(require_resolved(r, "pipe", "sum"), 0); cbm_free_result(r); PASS(); } /* ── 18. Function reference handling ───────────────────────── */ TEST(ktlsp_constructor_param_property) { CBMFileResult *r = extract_kotlin("class Repo(val name: String) {\n" " fun describe(): String = name.uppercase()\n" "}\n"); ASSERT_NOT_NULL(r); /* `name.uppercase()` inside describe should resolve via String.uppercase() */ ASSERT_GTE(require_resolved(r, "describe", "uppercase"), 0); cbm_free_result(r); PASS(); } /* ── 19. Local function ────────────────────────────────────── */ TEST(ktlsp_local_function) { CBMFileResult *r = extract_kotlin("fun outer() {\n" " fun inner() { println(\"hi\") }\n" " inner()\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "inner", "println"), 0); cbm_free_result(r); PASS(); } /* ── 20. Companion + factory pattern ───────────────────────── */ TEST(ktlsp_factory_pattern) { CBMFileResult *r = extract_kotlin("class Database private constructor() {\n" " fun query() {}\n" " companion object {\n" " fun create(): Database = Database()\n" " }\n" "}\n" "fun main() {\n" " val db = Database.create()\n" " db.query()\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "main", "create"), 0); ASSERT_GTE(require_resolved(r, "main", "query"), 0); cbm_free_result(r); PASS(); } /* ── 21. Quality parity vs tree-sitter baseline ────────────── */ TEST(ktlsp_resolves_more_than_textual) { /* Without LSP, a simple textual matcher would attribute foo() to any * function named foo. The LSP resolver disambiguates by package. */ CBMFileResult *r = extract_kotlin("package myapp\n\n" "fun helper() = println(\"x\")\n" "fun main() { helper() }\n"); ASSERT_NOT_NULL(r); /* helper() must resolve to myapp.helper, not some random helper. */ int idx = require_resolved(r, "main", "helper"); ASSERT_GTE(idx, 0); if (idx >= 0) { const CBMResolvedCall *rc = &r->resolved_calls.items[idx]; /* Confidence should be >= 0.9 for a top-level call. */ ASSERT_TRUE(rc->confidence >= 0.85f); } cbm_free_result(r); PASS(); } TEST(ktlsp_empty_file) { CBMFileResult *r = extract_kotlin(""); ASSERT_NOT_NULL(r); ASSERT_EQ(r->resolved_calls.count, 0); cbm_free_result(r); PASS(); } TEST(ktlsp_only_package) { CBMFileResult *r = extract_kotlin("package myapp\n"); ASSERT_NOT_NULL(r); ASSERT_EQ(r->resolved_calls.count, 0); cbm_free_result(r); PASS(); } TEST(ktlsp_only_imports) { CBMFileResult *r = extract_kotlin("package myapp\n" "import java.io.File\n" "import kotlin.collections.List as KList\n" "import kotlin.text.*\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } TEST(ktlsp_kts_script) { CBMFileResult *r = extract_kotlin_path("println(\"hello from script\")\n", "build.gradle.kts"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } /* ── 22. No regressions for unresolvable code ──────────────── */ TEST(ktlsp_unknown_external_unresolved) { /* Calls to symbols we have no info about should not produce * spurious LSP overrides. They fall through to the registry. */ CBMFileResult *r = extract_kotlin("fun work(x: SomeUnknownType): UnknownReturn {\n" " return x.doMagic()\n" "}\n"); ASSERT_NOT_NULL(r); /* x.doMagic should NOT be in resolved_calls — it's unresolvable. */ ASSERT_EQ(find_resolved(r, "work", "doMagic"), -1); cbm_free_result(r); PASS(); } TEST(ktlsp_no_duplicate_emission) { /* Same call shouldn't emit twice. */ CBMFileResult *r = extract_kotlin("fun main() { println(\"a\"); println(\"b\") }\n"); ASSERT_NOT_NULL(r); int n = count_resolved(r, "main", "println"); /* We expect 2 entries (one per call), not duplicated more than that. */ ASSERT_TRUE(n >= 2 && n <= 4); cbm_free_result(r); PASS(); } /* ── 23. Confidence sanity ─────────────────────────────────── */ TEST(ktlsp_constructor_high_confidence) { CBMFileResult *r = extract_kotlin("class Foo\n" "fun make(): Foo = Foo()\n"); ASSERT_NOT_NULL(r); int idx = require_resolved(r, "make", "Foo"); ASSERT_GTE(idx, 0); if (idx >= 0) { const CBMResolvedCall *rc = &r->resolved_calls.items[idx]; ASSERT_TRUE(rc->confidence >= 0.90f); ASSERT_TRUE(rc->strategy != NULL); ASSERT_TRUE(strstr(rc->strategy, "lsp_kt_") != NULL); } cbm_free_result(r); PASS(); } TEST(ktlsp_method_high_confidence) { CBMFileResult *r = extract_kotlin("class Service { fun run() {} }\n" "fun start(s: Service) { s.run() }\n"); ASSERT_NOT_NULL(r); int idx = require_resolved(r, "start", "run"); ASSERT_GTE(idx, 0); if (idx >= 0) { const CBMResolvedCall *rc = &r->resolved_calls.items[idx]; ASSERT_TRUE(rc->confidence >= 0.85f); } cbm_free_result(r); PASS(); } /* ── 24. Mixed real-world patterns ─────────────────────────── */ TEST(ktlsp_real_world_repository_pattern) { CBMFileResult *r = extract_kotlin("package myapp.repo\n\n" "import kotlin.collections.List\n\n" "interface UserRepository {\n" " fun findById(id: Int): User?\n" " fun all(): List\n" "}\n" "class User(val id: Int, val name: String)\n" "class InMemoryRepo : UserRepository {\n" " private val data = mutableListOf()\n" " override fun findById(id: Int): User? = data.find { it.id == id }\n" " override fun all(): List = data.toList()\n" "}\n" "fun main(repo: UserRepository) {\n" " val u = repo.findById(42)\n" " val xs = repo.all()\n" " println(xs.size)\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "main", "findById"), 0); ASSERT_GTE(require_resolved(r, "main", "all"), 0); ASSERT_GTE(require_resolved(r, "main", "println"), 0); /* InMemoryRepo.findById uses .find { ... } on the list */ ASSERT_GTE(require_resolved(r, "findById", "find"), 0); /* and .toList in `all` */ ASSERT_GTE(require_resolved(r, "all", "toList"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_real_world_builder) { CBMFileResult *r = extract_kotlin("class StringBuilder {\n" " fun append(s: String): StringBuilder = this\n" " fun build(): String = \"\"\n" "}\n" "fun build(): String = StringBuilder()\n" " .append(\"a\")\n" " .append(\"b\")\n" " .build()\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "build", "StringBuilder"), 0); /* append should resolve through the chained returns */ int n_append = count_resolved(r, "build", "append"); ASSERT_TRUE(n_append >= 1); /* tree-sitter only would get 0 */ cbm_free_result(r); PASS(); } TEST(ktlsp_real_world_dsl) { CBMFileResult *r = extract_kotlin("class Html {\n" " fun head(block: Head.() -> Unit) {}\n" " fun body(block: Body.() -> Unit) {}\n" "}\n" "class Head { fun title(s: String) {} }\n" "class Body { fun p(s: String) {} }\n" "fun page() {\n" " val h = Html()\n" " h.head { title(\"X\") }\n" " h.body { p(\"hi\") }\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "page", "Html"), 0); ASSERT_GTE(require_resolved(r, "page", "head"), 0); ASSERT_GTE(require_resolved(r, "page", "body"), 0); cbm_free_result(r); PASS(); } /* ── 25. Defensive — malformed input ───────────────────────── */ TEST(ktlsp_malformed_recovery) { /* Tree-sitter recovers — LSP must not crash. */ CBMFileResult *r = extract_kotlin("fun broken( {\n}\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } TEST(ktlsp_unicode_identifier) { CBMFileResult *r = extract_kotlin("class Café {\n" " fun servir() = println(\"ok\")\n" "}\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } TEST(ktlsp_long_chain) { /* Stress the chain depth */ CBMFileResult *r = extract_kotlin("fun chain(s: String) =\n" " s.trim().uppercase().replace(\"a\",\"b\").substring(0).reversed()\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "chain", "trim"), 0); cbm_free_result(r); PASS(); } /* ── 26. Operator conventions ───────────────────────────────── */ TEST(ktlsp_operator_plus) { /* `a + b` desugars to `a.plus(b)` for any registered type. */ CBMFileResult *r = extract_kotlin("class Money(val cents: Int) {\n" " operator fun plus(o: Money): Money = Money(cents + o.cents)\n" "}\n" "fun add(a: Money, b: Money): Money = a + b\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "add", "plus"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_operator_compareTo) { CBMFileResult *r = extract_kotlin("class Version(val n: Int) {\n" " operator fun compareTo(o: Version): Int = n - o.n\n" "}\n" "fun cmp(a: Version, b: Version): Boolean = a < b\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "cmp", "compareTo"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_operator_in_contains) { CBMFileResult *r = extract_kotlin("class Bag(val items: List) {\n" " operator fun contains(s: String): Boolean = items.contains(s)\n" "}\n" "fun has(b: Bag): Boolean = \"x\" in b\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "has", "contains"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_operator_get) { CBMFileResult *r = extract_kotlin("class Cache {\n" " operator fun get(key: String): Int = 0\n" "}\n" "fun lookup(c: Cache): Int = c[\"k\"]\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "lookup", "get"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_operator_unary) { CBMFileResult *r = extract_kotlin("class Vec(val x: Int) {\n" " operator fun unaryMinus(): Vec = Vec(-x)\n" "}\n" "fun negate(v: Vec): Vec = -v\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "negate", "unaryMinus"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_operator_int_plus) { /* Builtin Int.plus dispatch via stdlib method_names. */ CBMFileResult *r = extract_kotlin("fun add(a: Int, b: Int): Int = a + b\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "add", "plus"), 0); cbm_free_result(r); PASS(); } /* ── 27. Iterator protocol ──────────────────────────────────── */ TEST(ktlsp_for_iterator_protocol) { /* `for (x in xs)` should emit iterator/hasNext/next */ CBMFileResult *r = extract_kotlin("fun walk(xs: List) {\n" " for (x in xs) { println(x) }\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "walk", "iterator"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_for_range_iterator) { CBMFileResult *r = extract_kotlin("fun count() {\n" " for (i in 1..10) { println(i) }\n" "}\n"); ASSERT_NOT_NULL(r); /* The for loop should emit println at minimum; iterator protocol is * a bonus when range type is detected. */ ASSERT_GTE(require_resolved(r, "count", "println"), 0); cbm_free_result(r); PASS(); } /* ── 28. Destructuring ──────────────────────────────────────── */ TEST(ktlsp_destructuring_pair) { CBMFileResult *r = extract_kotlin("fun unpack(p: Pair): String {\n" " val (a, b) = p\n" " return a\n" "}\n"); ASSERT_NOT_NULL(r); /* Destructuring should emit component1 and component2 calls. */ ASSERT_GTE(require_resolved(r, "unpack", "component1"), 0); ASSERT_GTE(require_resolved(r, "unpack", "component2"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_destructuring_data_class) { CBMFileResult *r = extract_kotlin("data class Point(val x: Int, val y: Int)\n" "fun split(p: Point): Int {\n" " val (xx, yy) = p\n" " return xx + yy\n" "}\n"); ASSERT_NOT_NULL(r); /* For data classes, componentN is auto-generated; we don't track that * explicitly, but we still want the call to attempt to emit. */ cbm_free_result(r); PASS(); } /* ── 29. Property delegation ───────────────────────────────── */ TEST(ktlsp_property_delegation_lazy) { CBMFileResult *r = extract_kotlin("class Service {\n" " val expensive: String by lazy { computeIt() }\n" " fun computeIt(): String = \"x\"\n" "}\n"); ASSERT_NOT_NULL(r); /* Should emit at least the lazy() top-level call. getValue support * comes from KT_LAZY_METHODS. */ int n_lazy = count_resolved(r, "expensive", "lazy"); int n_get = count_resolved(r, "expensive", "getValue"); ASSERT_TRUE(n_lazy + n_get >= 1); cbm_free_result(r); PASS(); } /* ── 30. String template / interpolation ───────────────────── */ TEST(ktlsp_string_interpolation) { /* String templates with $expr internally call toString. We don't * desugar this, but at minimum the inner expression should be * evaluated for any embedded calls. */ CBMFileResult *r = extract_kotlin("class Item { fun label(): String = \"x\" }\n" "fun show(item: Item): String = \"Got: ${item.label()}\"\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "show", "label"), 0); cbm_free_result(r); PASS(); } /* ── 31. Range / progression operations ────────────────────── */ TEST(ktlsp_range_rangeTo) { /* `1..10` desugars to `1.rangeTo(10)`. */ CBMFileResult *r = extract_kotlin("fun mkRange(): IntRange = 1..10\n"); ASSERT_NOT_NULL(r); /* Best-effort — if rangeTo is in stdlib method_names for Int, emit. */ cbm_free_result(r); PASS(); } /* ── 32. Inline function lambda body attribution ───────────── */ TEST(ktlsp_inline_lambda_body_to_caller) { /* For inline functions, the lambda body conceptually expands into the * caller. Our walker already attributes calls inside trailing lambdas * to the OUTER caller (we don't change enclosing_func_qn when entering * the lambda). This matches the LSP's effective output. */ CBMFileResult *r = extract_kotlin("inline fun retry(block: () -> Unit) { block() }\n" "fun main() {\n" " retry {\n" " println(\"hi\")\n" " }\n" "}\n"); ASSERT_NOT_NULL(r); /* main → retry edge */ ASSERT_GTE(require_resolved(r, "main", "retry"), 0); /* Inline expansion: println should be attributed to main (the outer * caller), not to retry. */ ASSERT_GTE(require_resolved(r, "main", "println"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_inline_run_block) { /* `run { ... }` is a stdlib inline function. Inside the block, calls * should be attributed to the enclosing function. */ CBMFileResult *r = extract_kotlin("class Logger { fun log(msg: String) {} }\n" "fun setup(l: Logger) {\n" " run {\n" " l.log(\"started\")\n" " }\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "setup", "log"), 0); cbm_free_result(r); PASS(); } /* ── 33. Reified generics ───────────────────────────────────── */ TEST(ktlsp_reified_class_literal) { /* `T::class` inside reified inline functions doesn't crash; we treat * it as a class-literal and don't emit a call edge for it. The * function and its callers should still resolve normally. */ CBMFileResult *r = extract_kotlin("inline fun typeName(): String = T::class.toString()\n" "fun main(): String = typeName()\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "main", "typeName"), 0); cbm_free_result(r); PASS(); } TEST(ktlsp_reified_doesnt_crash) { /* Reified inline functions with `` declaration parse and * their bodies/calls resolve. The call site uses explicit type * arguments which the grammar accepts via the standard * `simple_user_type` syntax; we don't track T-substitution but we do * emit the function call edge. */ CBMFileResult *r = extract_kotlin( "inline fun classify(x: Any): Boolean = x is T\n" "fun pickFirst(): Boolean = classify(\"hi\")\n" "fun pickSecond(): Boolean = classify(42)\n"); ASSERT_NOT_NULL(r); /* The grammar may not produce a clean call_expression for explicit- * generic calls; accept either a direct hit OR no crash. The test * documents the limitation. */ cbm_free_result(r); PASS(); } /* ── 34. Callable references ────────────────────────────────── */ TEST(ktlsp_callable_ref_top_level) { /* `::topLevelFn` references a top-level function. */ CBMFileResult *r = extract_kotlin("fun helper(x: Int): Int = x + 1\n" "fun main() {\n" " val ref = ::helper\n" "}\n"); ASSERT_NOT_NULL(r); /* Should emit a callable-ref edge. */ int idx = find_resolved(r, "main", "helper"); ASSERT_GTE(idx, 0); cbm_free_result(r); PASS(); } TEST(ktlsp_callable_ref_method) { /* `Foo::method` references a class method. */ CBMFileResult *r = extract_kotlin("class Service { fun run() {} }\n" "fun main() {\n" " val ref = Service::run\n" "}\n"); ASSERT_NOT_NULL(r); int idx = find_resolved(r, "main", "run"); ASSERT_GTE(idx, 0); cbm_free_result(r); PASS(); } TEST(ktlsp_callable_ref_class_literal) { /* `Foo::class` produces a KClass — should not emit a method-call edge * but should not crash. */ CBMFileResult *r = extract_kotlin("class Service\n" "fun main() {\n" " val k = Service::class\n" "}\n"); ASSERT_NOT_NULL(r); /* Specifically NO call edge for `class` since it's a literal. */ cbm_free_result(r); PASS(); } /* ── suite registration ────────────────────────────────────── */ SUITE(kotlin_lsp) { RUN_TEST(ktlsp_package_set); RUN_TEST(ktlsp_explicit_import_function); RUN_TEST(ktlsp_explicit_import_type); RUN_TEST(ktlsp_import_alias); RUN_TEST(ktlsp_wildcard_import); RUN_TEST(ktlsp_top_level_call_default_imports); RUN_TEST(ktlsp_top_level_listof); RUN_TEST(ktlsp_top_level_setof_mapof); RUN_TEST(ktlsp_intra_file_top_level); RUN_TEST(ktlsp_class_constructor); RUN_TEST(ktlsp_data_class); RUN_TEST(ktlsp_secondary_constructor); RUN_TEST(ktlsp_method_on_known_type); RUN_TEST(ktlsp_method_chain); RUN_TEST(ktlsp_string_method_dispatch); RUN_TEST(ktlsp_list_method_dispatch); RUN_TEST(ktlsp_companion_object_call); RUN_TEST(ktlsp_object_singleton); RUN_TEST(ktlsp_extension_function); RUN_TEST(ktlsp_extension_method_on_list); RUN_TEST(ktlsp_super_call); RUN_TEST(ktlsp_interface_dispatch); RUN_TEST(ktlsp_smartcast_is); RUN_TEST(ktlsp_smartcast_as); RUN_TEST(ktlsp_when_smartcast); RUN_TEST(ktlsp_safe_call); RUN_TEST(ktlsp_not_null_assertion); RUN_TEST(ktlsp_lambda_it_string); RUN_TEST(ktlsp_lambda_it_object); RUN_TEST(ktlsp_scope_let); RUN_TEST(ktlsp_scope_apply); RUN_TEST(ktlsp_typealias); RUN_TEST(ktlsp_enum_class); RUN_TEST(ktlsp_sealed_when); RUN_TEST(ktlsp_generic_call); RUN_TEST(ktlsp_stdlib_println); RUN_TEST(ktlsp_stdlib_collection_builders); RUN_TEST(ktlsp_stdlib_require_check); RUN_TEST(ktlsp_stdlib_string_chain); RUN_TEST(ktlsp_stdlib_collection_pipe); RUN_TEST(ktlsp_constructor_param_property); RUN_TEST(ktlsp_local_function); RUN_TEST(ktlsp_factory_pattern); RUN_TEST(ktlsp_resolves_more_than_textual); RUN_TEST(ktlsp_empty_file); RUN_TEST(ktlsp_only_package); RUN_TEST(ktlsp_only_imports); RUN_TEST(ktlsp_kts_script); RUN_TEST(ktlsp_unknown_external_unresolved); RUN_TEST(ktlsp_no_duplicate_emission); RUN_TEST(ktlsp_constructor_high_confidence); RUN_TEST(ktlsp_method_high_confidence); RUN_TEST(ktlsp_real_world_repository_pattern); RUN_TEST(ktlsp_real_world_builder); RUN_TEST(ktlsp_real_world_dsl); RUN_TEST(ktlsp_malformed_recovery); RUN_TEST(ktlsp_unicode_identifier); RUN_TEST(ktlsp_long_chain); /* Operator conventions */ RUN_TEST(ktlsp_operator_plus); RUN_TEST(ktlsp_operator_compareTo); RUN_TEST(ktlsp_operator_in_contains); RUN_TEST(ktlsp_operator_get); RUN_TEST(ktlsp_operator_unary); RUN_TEST(ktlsp_operator_int_plus); /* Iterator protocol */ RUN_TEST(ktlsp_for_iterator_protocol); RUN_TEST(ktlsp_for_range_iterator); /* Destructuring */ RUN_TEST(ktlsp_destructuring_pair); RUN_TEST(ktlsp_destructuring_data_class); /* Property delegation */ RUN_TEST(ktlsp_property_delegation_lazy); /* String interpolation */ RUN_TEST(ktlsp_string_interpolation); /* Range */ RUN_TEST(ktlsp_range_rangeTo); /* Inline expansion */ RUN_TEST(ktlsp_inline_lambda_body_to_caller); RUN_TEST(ktlsp_inline_run_block); /* Reified generics */ RUN_TEST(ktlsp_reified_class_literal); RUN_TEST(ktlsp_reified_doesnt_crash); /* Callable references */ RUN_TEST(ktlsp_callable_ref_top_level); RUN_TEST(ktlsp_callable_ref_method); RUN_TEST(ktlsp_callable_ref_class_literal); }