Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile.cbm
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,7 @@ TEST_PY_LSP_SCALE_SRCS = tests/test_py_lsp_scale.c

TEST_TS_LSP_SRCS = tests/test_ts_lsp.c
TEST_JAVA_LSP_SRCS = tests/test_java_lsp.c tests/test_java_lsp_coverage.c
TEST_KOTLIN_LSP_SRCS = tests/test_kotlin_lsp.c
TEST_KOTLIN_LSP_SRCS = tests/test_kotlin_lsp.c tests/test_kotlin_typed_calls.c
TEST_RUST_LSP_SRCS = tests/test_rust_lsp.c

TEST_INTEGRATION_SRCS = tests/test_integration.c tests/test_incremental.c tests/test_lang_contract.c tests/test_edge_imports.c tests/test_edge_structural.c tests/test_lsp_resolution_probe.c tests/test_node_creation_probe.c tests/test_edge_types_probe.c tests/test_convergence_probe.c tests/test_matrix_known_classes.c tests/test_matrix_new_constructs.c tests/test_grammar_probe_a.c tests/test_grammar_probe_b.c tests/test_grammar_probe_c.c tests/test_grammar_probe_d.c tests/test_grammar_probe_e.c tests/test_grammar_probe_f.c tests/test_grammar_probe_g.c tests/test_conditional_variants.c tests/test_spawns.c
Expand Down
170 changes: 156 additions & 14 deletions internal/cbm/lsp/kotlin_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -395,9 +395,47 @@ static const CBMType *kt_unwrap_nullable(const CBMType *t) {
return t;
}

/* A kotlin.Any member (toString / equals / hashCode) resolved on a stdlib or
* JDK type -- `count.toString()` on `val count: Int` binds kotlin.Int.toString
* from the stdlib seed. That target has no graph node, so the call join fell
* back to a name guess and bound the call to whichever project class declares
* a same-named override (#1555: detekt `defaultValue.toString()` ->
* Entity.toString). The member is the universal kotlin.Any one, an external
* builtin node: emit that, no project edge. `kotlin.` and `java.` are reserved
* packages (the compilers reject project declarations in them), so the prefix
* proves the owner is not a project type; project QNs in this resolver are
* package-spelled and keep their declared override. */
static bool kt_is_any_member(const char *name) {
return strcmp(name, "toString") == 0 || strcmp(name, "equals") == 0 ||
strcmp(name, "hashCode") == 0;
}

static const char *kt_external_any_member(KotlinLSPContext *ctx, const char *callee_qn) {
const char *dot = strrchr(callee_qn, '.');
if (!dot || !kt_is_any_member(dot + 1)) {
return NULL;
}
if (strncmp(callee_qn, "kotlin.Any.", strlen("kotlin.Any.")) == 0) {
return NULL; /* already the builtin */
}
if (strncmp(callee_qn, "kotlin.", strlen("kotlin.")) != 0 &&
strncmp(callee_qn, "java.", strlen("java.")) != 0) {
return NULL; /* a project override: a real node */
}
return kt_join_dot(ctx->arena, "kotlin.Any", dot + 1);
}

/* Append a typed resolution result to the output array. */
static void kt_emit_resolved_kind(KotlinLSPContext *ctx, const char *callee_qn,
const char *strategy, float confidence, CBMResolvedKind kind) {
if (kind == CBM_RESOLVED_INVOCATION && callee_qn) {
const char *any_qn = kt_external_any_member(ctx, callee_qn);
if (any_qn) {
callee_qn = any_qn;
strategy = "lsp_kt_any";
confidence = confidence < KT_CONF_PARTIAL ? confidence : KT_CONF_PARTIAL;
}
}
if (ctx->debug) {
fprintf(stderr, "[kotlin_lsp] EMIT %s -> %s [%s %.2f] (resolved_calls=%p enc=%s)\n",
ctx->enclosing_func_qn ? ctx->enclosing_func_qn : "(null)",
Expand Down Expand Up @@ -1127,20 +1165,26 @@ const char *kotlin_resolve_function_name(KotlinLSPContext *ctx, const char *name
* package/import/bare/default-import lookups miss, so it never overrides a
* more specific match; in the per-file pass the registry holds just this
* file's defs, so the candidate is the file's own sole top-level fun. */
if (ctx->registry && ctx->registry->funcs) {
/* The cross-file walk runs on an own-file overlay chained to the cross
* registry (#1555), so the candidates span the whole chain; a function
* registered in both layers under one QN is one candidate. */
{
const char *only_qn = NULL;
int matches = 0;
for (int i = 0; i < ctx->registry->func_count && matches < 2; i++) {
const CBMRegisteredFunc *f = &ctx->registry->funcs[i];
if (!f->qualified_name || !f->short_name) {
continue;
}
if (f->receiver_type) { /* method / extension — not a bare top-level fun */
continue;
}
if (strcmp(f->short_name, name) == 0) {
only_qn = f->qualified_name;
matches++;
for (const CBMTypeRegistry *r = ctx->registry; r && matches < 2; r = r->fallback) {
for (int i = 0; r->funcs && i < r->func_count && matches < 2; i++) {
const CBMRegisteredFunc *f = &r->funcs[i];
if (!f->qualified_name || !f->short_name) {
continue;
}
if (f->receiver_type) { /* method / extension — not a bare top-level fun */
continue;
}
if (strcmp(f->short_name, name) == 0 &&
!(only_qn && strcmp(only_qn, f->qualified_name) == 0)) {
only_qn = f->qualified_name;
matches++;
}
}
}
if (matches == 1 && only_qn) {
Expand Down Expand Up @@ -1335,6 +1379,22 @@ const CBMRegisteredFunc *kotlin_lookup_method(KotlinLSPContext *ctx, const char
const CBMRegisteredFunc *rf =
cbm_registry_lookup_method_aliased(ctx->registry, cur_qn, method_name);
if (rf) {
/* A member wins over an extension of the same name (Kotlin
* resolution). The file's own extension functions carry the
* extended class as receiver_type, so the receiver lookup can
* return `fun Builder.add(..)` for a call meant for the member
* `Builder.add`; the member's QN nests under the class. */
size_t cur_len = strlen(cur_qn);
bool is_member = rf->qualified_name &&
strncmp(rf->qualified_name, cur_qn, cur_len) == 0 &&
rf->qualified_name[cur_len] == '.';
if (!is_member) {
const CBMRegisteredFunc *member = cbm_registry_lookup_func(
ctx->registry, kt_join_dot(ctx->arena, cur_qn, method_name));
if (member) {
return member;
}
}
return rf;
}

Expand Down Expand Up @@ -2907,6 +2967,37 @@ const CBMType *kotlin_eval_expr_type(KotlinLSPContext *ctx, TSNode node) {
return result ? result : cbm_type_unknown();
}

/* `handshake()` inside a class whose constructor declares `private val
* handshake: Handshake?` next to `override fun handshake()` (okhttp
* RealConnection): the property is in the method scope (class fields are bound
* there), but a value of a known type without an `invoke` operator is no call
* candidate, so Kotlin resolves the call to the member function. Once #1555
* typed the class's constructor vals, the lexical-shadow rule below failed
* such calls closed and they fell to a name guess. An unknown or callable
* binding still shadows, as before. */
static bool kt_value_binding_yields_to_member(KotlinLSPContext *ctx, const CBMType *bound,
const char *name) {
if (!bound || cbm_type_is_unknown(bound) || bound->kind == CBM_TYPE_CALLABLE ||
bound->kind == CBM_TYPE_FUNC || !ctx->this_type || cbm_type_is_unknown(ctx->this_type)) {
return false;
}
const CBMType *value = kt_unwrap_nullable(bound);
const char *value_qn = kt_type_qn_of(value);
if (value_qn && kotlin_lookup_method(ctx, value_qn, "invoke")) {
return false; /* an invokable value: the call may be `value.invoke()` */
}
const char *this_qn = kt_type_qn_of(ctx->this_type);
if (!this_qn) {
return false;
}
const CBMRegisteredFunc *rf = kotlin_lookup_method(ctx, this_qn, name);
if (!rf || !rf->qualified_name) {
return false;
}
size_t len = strlen(this_qn);
return strncmp(rf->qualified_name, this_qn, len) == 0 && rf->qualified_name[len] == '.';
}

static const CBMType *kt_eval_constructor_or_func_call(KotlinLSPContext *ctx, TSNode call_node,
const char *callee_text) {
/* callee_text is a bare identifier — it may be:
Expand Down Expand Up @@ -2944,7 +3035,10 @@ static const CBMType *kt_eval_constructor_or_func_call(KotlinLSPContext *ctx, TS
bound->data.func.return_types[0]) {
return bound->data.func.return_types[0];
}
return cbm_type_unknown();
if (!kt_value_binding_yields_to_member(ctx, bound, callee_text)) {
return cbm_type_unknown();
}
/* A known non-invokable value yields to the member function: step 1. */
}

/* 1. `this`-method dispatch: if we're inside a class method or DSL
Expand All @@ -2967,6 +3061,37 @@ static const CBMType *kt_eval_constructor_or_func_call(KotlinLSPContext *ctx, TS
}
}

/* 1b. Implicit receivers form a chain, innermost first: inside a lambda
* whose receiver (or `it` type, which kt_process_lambda also installs as
* `this`) is another type, and inside a member extension whose extension
* receiver is another type, the enclosing class's members stay callable
* bare. Once #1555 typed more receivers, `repo.let { ping() }` resolved
* against Repo only and fell to a name guess. A call naming a class is a
* constructor call, left to step 2 (a secondary constructor is registered
* as a same-named member and would otherwise be taken for a method). */
const char *ctor_cls = kotlin_resolve_class_name(ctx, callee_text);
bool is_ctor_call =
ctor_cls && ctx->registry && cbm_registry_lookup_type(ctx->registry, ctor_cls) != NULL;
if (ctx->enclosing_class_qn && !is_ctor_call) {
const char *this_qn = (ctx->this_type && !cbm_type_is_unknown(ctx->this_type))
? kt_type_qn_of(ctx->this_type)
: NULL;
if (!this_qn || strcmp(this_qn, ctx->enclosing_class_qn) != 0) {
const CBMRegisteredFunc *rf =
kotlin_lookup_method(ctx, ctx->enclosing_class_qn, callee_text);
if (rf && rf->qualified_name) {
kt_emit_resolved_at(ctx, rf->qualified_name, "lsp_kt_this", KT_CONF_THIS,
call_node);
if (rf->signature && rf->signature->kind == CBM_TYPE_FUNC &&
rf->signature->data.func.return_types &&
rf->signature->data.func.return_types[0]) {
return rf->signature->data.func.return_types[0];
}
return cbm_type_unknown();
}
}
}

/* 2. Class? */
const char *cls_qn = kotlin_resolve_class_name(ctx, callee_text);
if (cls_qn && ctx->registry) {
Expand Down Expand Up @@ -4533,6 +4658,11 @@ void kotlin_lsp_process_file(KotlinLSPContext *ctx, TSNode root) {

/* 2. Collect top-level definitions for intra-file resolution. */
kt_collect_top_level_decls(ctx, root);
/* Cross-file: index the own-file overlay now that its declarations are
* in; later additions stay visible through the post-finalize tail. */
if (ctx->overlay_index_arena && ctx->registry) {
cbm_registry_finalize_into((CBMTypeRegistry *)ctx->registry, ctx->overlay_index_arena);
}

/* 3. Walk for call resolution. */
kt_walk_top_level_for_resolution(ctx, root, NULL);
Expand Down Expand Up @@ -5436,12 +5566,24 @@ void cbm_run_kotlin_lsp_cross(CBMArena *arena, const char *source, int source_le
project_name = module_qn;
}

/* Own-file overlay (#1555). The cross registry above already holds this
* file's classes, built from extraction defs whose properties carry no
* type (primary-constructor vals none at all). The walk re-registers each
* class from the AST with typed fields; registered into `reg` under the
* same QN that entry was shadowed by the earlier one, so every
* `repository.inserir()` on an injected property fell to the name guess.
* In an overlay checked first, the AST entry wins. */
CBMTypeRegistry overlay;
cbm_registry_init(&overlay, arena);
overlay.fallback = &reg;

KotlinLSPContext ctx;
/* Match the per-file pass: until a source package declaration overrides
* it, the path-derived module is the callable-QN prefix. This preserves
* exact caller identity for default-package functions across files. */
kotlin_lsp_init(&ctx, arena, source, source_len, &reg, module_qn ? module_qn : "",
kotlin_lsp_init(&ctx, arena, source, source_len, &overlay, module_qn ? module_qn : "",
module_qn ? module_qn : "", project_name, /*rel_path=*/NULL, out);
ctx.overlay_index_arena = &idx_arena;

/* Unique short type name -> registered QN, for receiver annotations that
* name a type defined in another file without an import. Two types
Expand Down
7 changes: 7 additions & 0 deletions internal/cbm/lsp/kotlin_lsp.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ typedef struct KotlinLSPContext {
* each field's REAL def QN here (kotlin class properties are minted with
* module-level QNs, so composing <class>.<member> would miss the node). */
const void *cross_field_map;
/* Cross-file only (NULL per-file): when set, `registry` is a per-file
* overlay chained to the cross registry, and kotlin_lsp_process_file
* indexes the overlay into this scratch arena once the file's own
* declarations are collected. The overlay holds the AST registrations
* (typed primary-constructor and body properties), so they are found
* before the extraction-built entries of the same QN (#1555). */
CBMArena *overlay_index_arena;
} KotlinLSPContext;

/* Initialize a KotlinLSPContext for processing one file. */
Expand Down
18 changes: 18 additions & 0 deletions src/pipeline/lsp_resolve.h
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,24 @@ static inline bool cbm_pipeline_node_is_callable_target(const cbm_gbuf_node_t *n
strcmp(node->label, "Constructor") == 0 || strcmp(node->label, "Class") == 0;
}

/* The Kotlin resolver claims a property READ on a typed receiver with an exact
* row (lsp_kt_property_ref) so that the raw-name fallback cannot borrow a
* same-named function. That row names a property, never a function value. Its
* QN is not always materialized under that spelling (a property of a nested
* class); the graph lookup then finds nothing, or its unique-callable step
* picks a same-named function elsewhere (#1555: okhttp `server.webSocket` -> a
* test function `webSocket`, a fabricated CALL_REFERENCE). Such a row is not
* admissible: the caller falls back by name, restricted to non-callable
* targets, which binds the property's own node as before the receiver was
* typed and can never borrow a function. */
static inline bool cbm_pipeline_reference_target_admissible(const CBMResolvedCall *ref,
const cbm_gbuf_node_t *tgt) {
if (!ref || !ref->strategy || strcmp(ref->strategy, "lsp_kt_property_ref") != 0) {
return true;
}
return tgt && !cbm_pipeline_node_is_callable_target(tgt);
}

static inline int cbm_pipeline_qn_class_method_tail_eq(const char *qn, const char *tail) {
const char *qt = cbm_pipeline_qn_class_method_tail(qn);
return qt && tail && strcmp(qt, tail) == 0;
Expand Down
63 changes: 63 additions & 0 deletions src/pipeline/pass_lsp_cross.c
Original file line number Diff line number Diff line change
Expand Up @@ -1285,6 +1285,68 @@ static void pxc_supersede_occurrence(CBMArena *dst_arena, CBMResolvedCallArray *
dst_calls->count = kept;
}

/* The Kotlin universal-member placeholder: `x.hashCode()` / `toString()` /
* `equals()` on a receiver the resolver could not type is emitted on kotlin.Any
* (strategy lsp_kt_any, reduced confidence) -- the "no narrower receiver known"
* answer. */
static bool pxc_is_kt_any_placeholder(const CBMResolvedCall *rc) {
return rc->strategy && strcmp(rc->strategy, "lsp_kt_any") == 0;
}

static char *pxc_occurrence_key(CBMArena *keys, const CBMResolvedCall *rc) {
return cbm_arena_sprintf(keys, "%u\x1f%s\x1f%u:%u\x1f%u", (unsigned)rc->kind, rc->caller_qn,
rc->site_start_byte, rc->site_end_byte, (unsigned)rc->source_origin);
}

/* #1555: the per-file pass sees only the file's own declarations, so a receiver
* typed by another file (`val type: ProtoType`) is unknown there and
* `type.hashCode()` gets the kotlin.Any placeholder. The cross pass types the
* receiver and answers with the declared override (`ProtoType.hashCode`), or
* with kotlin.Any again when no class on the receiver's chain declares one. Two
* distinct targets at one occurrence are a tombstone in the call join
* (pass_parallel.c LSP_IDX_AMBIGUOUS), so the occurrence fell to the name-based
* registry guess (wire: `type.hashCode` -> AllTypes.hashCode). A placeholder is
* therefore dropped when a cross row resolves the same occurrence; the cross row
* is then the only evidence. One hash pass per side keeps this linear. */
static void pxc_drop_superseded_kt_any(CBMArena *keys, CBMResolvedCallArray *dst_calls,
const CBMResolvedCallArray *src_out) {
bool has_placeholder = false;
for (int i = 0; i < dst_calls->count && !has_placeholder; i++) {
has_placeholder = pxc_is_kt_any_placeholder(&dst_calls->items[i]);
}
if (!has_placeholder) {
return;
}
CBMHashTable *resolved = cbm_ht_create((uint32_t)src_out->count + 1);
for (int j = 0; j < src_out->count; j++) {
const CBMResolvedCall *src = &src_out->items[j];
if (!src->caller_qn || !src->callee_qn || pxc_is_kt_any_placeholder(src) ||
src->site_end_byte <= src->site_start_byte) {
continue;
}
char *k = pxc_occurrence_key(keys, src);
if (k) {
cbm_ht_set(resolved, k, (void *)(uintptr_t)1);
}
}
int kept = 0;
for (int i = 0; i < dst_calls->count; i++) {
const CBMResolvedCall *cur = &dst_calls->items[i];
if (pxc_is_kt_any_placeholder(cur) && cur->caller_qn) {
char *k = pxc_occurrence_key(keys, cur);
if (k && cbm_ht_get(resolved, k)) {
continue;
}
}
if (kept != i) {
dst_calls->items[kept] = *cur;
}
kept++;
}
dst_calls->count = kept;
cbm_ht_free(resolved);
}

static void pxc_append_results(CBMArena *dst_arena, CBMResolvedCallArray *dst_calls,
const CBMResolvedCallArray *src_out) {
if (!dst_calls || !src_out)
Expand Down Expand Up @@ -1312,6 +1374,7 @@ static void pxc_append_results(CBMArena *dst_arena, CBMResolvedCallArray *dst_ca
}
pxc_supersede_occurrence(dst_arena, dst_calls, src);
}
pxc_drop_superseded_kt_any(&keys, dst_calls, src_out);

for (int i = 0; i < dst_calls->count; i++) {
const CBMResolvedCall *rc = &dst_calls->items[i];
Expand Down
Loading
Loading