1193 lines
48 KiB
C
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);
|
|
}
|