fix(java-lsp): resolve interface calls to sole concrete impl before type_dispatch

A call through an interface-typed receiver (s.area() where s:Shape) hit
java_lookup_method on the interface's OWN abstract method and emitted
lsp_type_dispatch on Shape.area, never reaching the sole-implementer
resolution — so lsp_interface_resolve / lsp_interface_dispatch were never
produced.

Resolve interface dispatch first when the receiver is an interface: a sole
concrete in-project implementer -> lsp_interface_resolve (to that impl's
method, synthesizing the QN when the method registry lacks it); two-or-more
-> lsp_interface_dispatch; ZERO in-project impls -> fall through to
type_dispatch on the interface method, keeping JDK interface calls
(List/Stream/Predicate) strict.

The sole-impl detection is factored into java_find_sole_impl and hardened:
consult the method REGISTRY (the dispatch path's source of truth) since the
per-type method-name array is often empty, match supertypes by full QN OR
bare name (duplicate registry entries list a supertype short in one entry and
fully-qualified in another), and dedup implementers by QN so one class with
two registry entries is not miscounted as two implementers.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-06-28 03:55:50 +02:00
parent c18e3dc304
commit 14def05cfe
+115 -56
View File
@@ -1813,6 +1813,108 @@ static void java_emit_unresolved(JavaLSPContext *ctx, const char *expr_text, con
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
}
/* Find a sole concrete in-project implementer of interface `iface_qn` that
* declares method `mname`. Returns the implementer's QN when exactly ONE
* exists (else NULL), and sets *out_count to the number found (capped at 2,
* so 2 means "two or more"). Walks the registered-type parent chain to
* confirm true subtyping. Mirrors the inline detection that used to live in
* resolve_method_call so both the f-found and f-absent interface paths share
* identical semantics. */
static const char *java_find_sole_impl(JavaLSPContext *ctx, const char *iface_qn, const char *mname,
int *out_count) {
const char *first = NULL; /* first distinct impl QN seen */
int distinct = 0; /* distinct impl classes (capped at 2) */
const char *iface_dot = strrchr(iface_qn, '.');
const char *iface_bare = iface_dot ? iface_dot + 1 : iface_qn;
for (int ti = 0; ti < ctx->registry->type_count && distinct < 2; ti++) {
const CBMRegisteredType *cand = &ctx->registry->types[ti];
if (cand->is_interface || !cand->qualified_name || cand->alias_of)
continue;
/* Does cand declare `mname`? The method-name array is often empty for
* fixture classes; the method REGISTRY is the authoritative source the
* dispatch path already uses, so consult it first and fall back to the
* name array. */
bool has = cbm_registry_lookup_method(ctx->registry, cand->qualified_name, mname) != NULL;
if (!has && cand->method_names) {
for (int mi = 0; cand->method_names[mi]; mi++) {
if (strcmp(cand->method_names[mi], mname) == 0) {
has = true;
break;
}
}
}
if (!has)
continue;
/* Subtype check: walk cand's supertype chain, matching iface by FULL
* QN or BARE name. The registry holds duplicate type entries whose
* `embedded_types` list a supertype sometimes by short name ("Shape")
* and sometimes by full QN ("proj.Shape"); a full-QN-only comparison
* silently misses the short-name form, so compare both. */
const char *cur = cand->qualified_name;
bool subtype = false;
for (int hops = 0; hops < JAVA_LSP_MAX_INHERIT_HOPS && cur && !subtype; hops++) {
const CBMRegisteredType *ct = cbm_registry_lookup_type(ctx->registry, cur);
if (!ct || !ct->embedded_types)
break;
const char *next = NULL;
for (int pi = 0; ct->embedded_types[pi]; pi++) {
const char *e = ct->embedded_types[pi];
const char *edot = strrchr(e, '.');
const char *ebare = edot ? edot + 1 : e;
if (strcmp(e, iface_qn) == 0 || strcmp(ebare, iface_bare) == 0) {
subtype = true;
break;
}
if (!next)
next = e; /* first supertype → continue the walk upward */
}
cur = next;
}
if (!subtype)
continue;
/* Count DISTINCT impl classes: the registry duplicates entries per
* class, so dedup by QN — two entries of one class must not read as
* two implementers. */
if (!first) {
first = cand->qualified_name;
distinct = 1;
} else if (strcmp(first, cand->qualified_name) != 0) {
distinct = 2;
}
}
if (out_count)
*out_count = distinct;
return distinct == 1 ? first : NULL;
}
/* Emit the resolution for an interface-typed receiver `iface_qn` calling
* `mname`: a sole concrete in-project impl → lsp_interface_resolve (resolved
* to that impl's method, with a synthesized QN when the method isn't in the
* method registry); two-or-more impls → lsp_interface_dispatch on a synthesized
* iface-qualified target. Returns true when it emitted (caller should return),
* false when there is NO in-project implementer (impl_count == 0) so the caller
* can fall back to dispatching on the interface's own method — this keeps JDK
* interface calls (List/Stream/Predicate, no in-project impl) resolving via the
* strict type_dispatch path instead of being downgraded to interface_dispatch. */
static bool java_emit_interface_resolution(JavaLSPContext *ctx, const char *iface_qn,
const char *mname) {
int impl_count = 0;
const char *sole_impl = java_find_sole_impl(ctx, iface_qn, mname, &impl_count);
if (impl_count == 1 && sole_impl) {
const CBMRegisteredFunc *cf = cbm_registry_lookup_method(ctx->registry, sole_impl, mname);
const char *target =
cf ? cf->qualified_name : cbm_arena_sprintf(ctx->arena, "%s.%s", sole_impl, mname);
java_emit_resolved(ctx, target, "lsp_interface_resolve", 0.85f);
return true;
}
if (impl_count >= 2) {
java_emit_resolved(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", iface_qn, mname),
"lsp_interface_dispatch", 0.80f);
return true;
}
return false; /* impl_count == 0: caller falls back to type_dispatch. */
}
static void resolve_method_call(JavaLSPContext *ctx, TSNode call) {
TSNode obj = ts_node_child_by_field_name(call, "object", 6);
TSNode name_node = ts_node_child_by_field_name(call, "name", 4);
@@ -1929,6 +2031,15 @@ static void resolve_method_call(JavaLSPContext *ctx, TSNode call) {
if (recv_qn) {
const CBMRegisteredFunc *f = java_lookup_method(ctx, recv_qn, mname, arity);
if (f) {
/* When the receiver is an interface, java_lookup_method finds the
* interface's OWN (abstract/default) method. Prefer resolving to a
* sole concrete in-project implementer first; only fall through to
* type_dispatch on the interface method when there is NO in-project
* impl (e.g. JDK List/Stream/Predicate), keeping those strict. */
const CBMRegisteredType *rt0 = cbm_registry_lookup_type(ctx->registry, recv_qn);
if (rt0 && rt0->is_interface && java_emit_interface_resolution(ctx, recv_qn, mname)) {
return;
}
const char *strategy = "lsp_type_dispatch";
if (f->receiver_type && strcmp(f->receiver_type, recv_qn) != 0) {
strategy = "lsp_inherited_dispatch";
@@ -1936,64 +2047,12 @@ static void resolve_method_call(JavaLSPContext *ctx, TSNode call) {
java_emit_resolved(ctx, f->qualified_name, strategy, 0.95f);
return;
}
/* Interface dispatch: walk all registered types implementing the
* interface and find a sole concrete impl. */
/* Interface dispatch with no directly-registered method: resolve to a
* sole concrete impl, else a synthesized iface-qualified dispatch. */
const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, recv_qn);
if (rt && rt->is_interface) {
const char *sole_impl = NULL;
int impl_count = 0;
for (int ti = 0; ti < ctx->registry->type_count && impl_count < 2; ti++) {
const CBMRegisteredType *cand = &ctx->registry->types[ti];
if (cand->is_interface || !cand->qualified_name || cand->alias_of)
continue;
bool has = false;
if (cand->method_names) {
for (int mi = 0; cand->method_names[mi]; mi++) {
if (strcmp(cand->method_names[mi], mname) == 0) {
has = true;
break;
}
}
}
if (!has)
continue;
/* Walk parent chain to confirm it's actually a subtype of rt. */
const char *cur = cand->qualified_name;
bool subtype = false;
for (int hops = 0; hops < JAVA_LSP_MAX_INHERIT_HOPS && cur; hops++) {
if (strcmp(cur, recv_qn) == 0) {
subtype = true;
break;
}
const CBMRegisteredType *par = cbm_registry_lookup_type(ctx->registry, cur);
if (!par || !par->embedded_types || !par->embedded_types[0])
break;
/* Walk all parents — pick the first match. */
bool advanced = false;
for (int pi = 0; par->embedded_types[pi]; pi++) {
if (strcmp(par->embedded_types[pi], recv_qn) == 0) {
subtype = true;
cur = NULL;
break;
}
}
if (subtype)
break;
if (!advanced)
cur = par->embedded_types[0];
}
if (subtype) {
sole_impl = cand->qualified_name;
impl_count++;
}
}
if (impl_count == 1 && sole_impl) {
const CBMRegisteredFunc *cf =
cbm_registry_lookup_method(ctx->registry, sole_impl, mname);
if (cf) {
java_emit_resolved(ctx, cf->qualified_name, "lsp_interface_resolve", 0.85f);
return;
}
if (java_emit_interface_resolution(ctx, recv_qn, mname)) {
return;
}
java_emit_resolved(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", recv_qn, mname),
"lsp_interface_dispatch", 0.80f);