Files
deusdata--codebase-memory-mcp/tests/test_kotlin_lsp.c
T
wehub-resource-sync 41cb1c0170
OpenSSF Scorecard / scorecard (push) Failing after 0s
DCO / dco (push) Failing after 0s
CodeQL SAST / analyze (push) Failing after 1s
Deploy Pages / deploy (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:28:05 +08:00

1193 lines
48 KiB
C

/*
* 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 <string.h>
/* ── 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<Int>): List<Int> = 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<Int>.sumPlus(n: Int): Int = this.sum() + n\n"
"fun calc(xs: List<Int>): 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<T>(val value: T) {\n"
" fun unwrap(): T = value\n"
"}\n"
"fun get(b: Box<String>): 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<String, Int>()\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>): 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<User>\n"
"}\n"
"class User(val id: Int, val name: String)\n"
"class InMemoryRepo : UserRepository {\n"
" private val data = mutableListOf<User>()\n"
" override fun findById(id: Int): User? = data.find { it.id == id }\n"
" override fun all(): List<User> = 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<String>) {\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<Int>) {\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, Int>): 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 <reified T> typeName(): String = T::class.toString()\n"
"fun main(): String = typeName<String>()\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 `<reified T>` 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 <reified T> classify(x: Any): Boolean = x is T\n"
"fun pickFirst(): Boolean = classify<String>(\"hi\")\n"
"fun pickSecond(): Boolean = classify<Int>(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);
}