mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-29 07:13:44 +08:00
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:
+115
-56
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user