Skip to content
3 changes: 2 additions & 1 deletion Makefile.cbm
Original file line number Diff line number Diff line change
Expand Up @@ -903,7 +903,8 @@ TEST_PY_LSP_STRESS_SRCS = tests/test_py_lsp_stress.c
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_JAVA_LSP_SRCS = tests/test_java_lsp.c tests/test_java_lsp_coverage.c \
tests/test_java_typed_calls.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

Expand Down
180 changes: 167 additions & 13 deletions internal/cbm/lsp/java_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,19 @@ const char *java_resolve_type_name(JavaLSPContext *ctx, const char *name) {
}
}

/* 3. java.lang auto-import. Try registered first, then the conventional QN. */
/* 3. Java package prefix. A type of the own package shadows every
* on-demand import, java.lang's included (JLS 6.4.1). */
if (ctx->package_name && ctx->package_name[0]) {
char buf[JAVA_LSP_BUF];
int n = snprintf(buf, sizeof(buf), "%s.%s", ctx->package_name, name);
if (n > 0 && (size_t)n < sizeof(buf)) {
if (cbm_registry_lookup_type(ctx->registry, buf)) {
return cbm_arena_strdup(ctx->arena, buf);
}
}
}

/* 4. java.lang auto-import. Try registered first, then the conventional QN. */
for (int i = 0; JAVA_LANG_TYPES[i]; i++) {
if (strcmp(JAVA_LANG_TYPES[i], name) == 0) {
char buf[JAVA_LSP_BUF];
Expand All @@ -521,7 +533,7 @@ const char *java_resolve_type_name(JavaLSPContext *ctx, const char *name) {
}
}

/* 4. On-demand imports. */
/* 5. On-demand imports. */
for (int i = 0; i < ctx->import_count; i++) {
if (ctx->import_kinds[i] != CBM_JAVA_IMPORT_ON_DEMAND)
continue;
Expand All @@ -534,17 +546,6 @@ const char *java_resolve_type_name(JavaLSPContext *ctx, const char *name) {
}
}

/* 5. Java package prefix. */
if (ctx->package_name && ctx->package_name[0]) {
char buf[JAVA_LSP_BUF];
int n = snprintf(buf, sizeof(buf), "%s.%s", ctx->package_name, name);
if (n > 0 && (size_t)n < sizeof(buf)) {
if (cbm_registry_lookup_type(ctx->registry, buf)) {
return cbm_arena_strdup(ctx->arena, buf);
}
}
}

/* Exact already-qualified name. */
if (cbm_registry_lookup_type(ctx->registry, name)) {
return cbm_arena_strdup(ctx->arena, name);
Expand Down Expand Up @@ -969,6 +970,12 @@ const CBMRegisteredFunc *java_lookup_method(JavaLSPContext *ctx, const char *cla
return fallback;
}

/* A type declared in the project: the cross registrar and the own-file pass
* stamp namespace_qn on every project type; the JDK seed leaves it NULL. */
static bool java_is_project_type(const CBMRegisteredType *rt) {
return rt && rt->namespace_qn && rt->namespace_qn[0];
}

/* ── Method-invocation evaluation ─────────────────────────────────── */

static const CBMType *java_return_type_of(const CBMRegisteredFunc *f) {
Expand Down Expand Up @@ -1797,11 +1804,135 @@ void java_lsp_process_file(JavaLSPContext *ctx, TSNode root) {

/* ── Call-edge resolution ─────────────────────────────────────────── */

/* True when `callee_qn` names a member (or a constructor) of a registered type
* the project does not declare — a JDK seed type. Such a target has no graph
* node, and the pipeline must not hand it to a same-named project method. */
static bool java_callee_is_external(const JavaLSPContext *ctx, const char *callee_qn) {
if (!ctx->registry || !callee_qn)
return false;
const CBMRegisteredType *self = cbm_registry_lookup_type(ctx->registry, callee_qn);
if (self)
return !java_is_project_type(self);
const char *dot = strrchr(callee_qn, '.');
if (!dot || dot == callee_qn)
return false;
char owner[JAVA_LSP_BUF];
size_t n = (size_t)(dot - callee_qn);
if (n >= sizeof(owner))
return false;
memcpy(owner, callee_qn, n);
owner[n] = '\0';
const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, owner);
return rt && !java_is_project_type(rt);
}

/* `T`, `ID`, `SELF`: a type-variable spelling — one or two characters, or
* capitals, digits and '_' only — that the file imports neither by name nor
* through its package (`import org.jooq.impl.*;` makes `DSL` a type). A type
* variable the resolver failed to bind can reach a receiver qualified by some
* package; it is unknown, not a library type. */
static bool java_is_type_var_spelling(const JavaLSPContext *ctx, const char *pkg, size_t pkg_len,
const char *simple) {
size_t n = strlen(simple);
bool caps = n > 0;
for (size_t i = 0; i < n && caps; i++) {
caps = isupper((unsigned char)simple[i]) || isdigit((unsigned char)simple[i]) ||
simple[i] == '_';
}
if (!caps && n > 2) {
return false;
}
for (int i = 0; i < ctx->import_count; i++) {
const char *target = ctx->import_target_qns[i];
if (ctx->import_kinds[i] == CBM_JAVA_IMPORT_TYPE && ctx->import_local_names[i] &&
strcmp(ctx->import_local_names[i], simple) == 0) {
return false;
}
if (ctx->import_kinds[i] == CBM_JAVA_IMPORT_ON_DEMAND && target &&
strlen(target) == pkg_len && strncmp(target, pkg, pkg_len) == 0) {
return false;
}
}
return true;
}

/* Whether the shared registry holds no project type under `prefix`
* ("org.jooq."), memoised per package. */
static bool java_package_is_external(JavaLSPContext *ctx, const CBMTypeRegistry *base,
const char *prefix) {
for (int i = 0; i < JAVA_EXT_MEMO; i++) {
if (ctx->ext_memo_pkg[i] && strcmp(ctx->ext_memo_pkg[i], prefix) == 0) {
return ctx->ext_memo_val[i];
}
}
CBMArena scratch;
cbm_arena_init(&scratch);
CBMQNPrefixIter it;
bool project = false;
/* The prefix search reads one layer, and the layer above the shared
* registry is a per-file overlay that is often empty: walk the chain. */
for (const CBMTypeRegistry *reg = base; reg && !project; reg = reg->fallback) {
if (!cbm_registry_types_with_qn_prefix(reg, prefix, true, &scratch, &it))
continue;
for (int ti = cbm_qn_prefix_iter_next(&it); ti >= 0 && !project;
ti = cbm_qn_prefix_iter_next(&it)) {
project = java_is_project_type(&reg->types[ti]);
}
}
cbm_arena_destroy(&scratch);
int slot = ctx->ext_memo_next++ % JAVA_EXT_MEMO;
ctx->ext_memo_pkg[slot] = prefix;
ctx->ext_memo_val[slot] = !project;
return !project;
}

/* A receiver type the registry does not know, spelled with a package the
* project declares no type in (`org.jooq.DSLContext` from an import): a
* library type. The package is the segments before the first capitalised
* one, so a nested type (`com.acme.Order.OrderBuilder`, registered flattened
* as `com.acme.OrderBuilder`) is judged by `com.acme`. Cross-file runs only —
* the shared registry is the one that knows every project package, and its
* type order makes the probe a binary search. A path-derived QN
* (project-prefixed) is an unresolved project spelling, never a library. */
static bool java_receiver_is_external(JavaLSPContext *ctx, const char *recv_qn) {
const CBMTypeRegistry *base = ctx->registry ? ctx->registry->fallback : NULL;
if (!base || !recv_qn || !islower((unsigned char)recv_qn[0]))
return false;
const char *type = NULL;
for (const char *p = recv_qn; (p = strchr(p, '.')) != NULL; p++) {
if (isupper((unsigned char)p[1])) {
type = p + 1;
break;
}
}
if (!type)
return false;
size_t pkg_len = (size_t)(type - 1 - recv_qn);
const char *last = strrchr(recv_qn, '.');
if (java_is_type_var_spelling(ctx, recv_qn, pkg_len, last + 1))
return false;
/* The project segment; a file at the repository root has a module QN
* that is the project name alone. */
const char *mod_dot = ctx->module_qn ? strchr(ctx->module_qn, '.') : NULL;
size_t mod_len = mod_dot ? (size_t)(mod_dot - ctx->module_qn)
: (ctx->module_qn ? strlen(ctx->module_qn) : 0);
if (mod_len && strncmp(recv_qn, ctx->module_qn, mod_len) == 0 && recv_qn[mod_len] == '.')
return false;
if (ctx->package_name && strlen(ctx->package_name) == pkg_len &&
strncmp(ctx->package_name, recv_qn, pkg_len) == 0)
return false;
/* "org.jooq." */
return java_package_is_external(ctx, base, cbm_arena_strndup(ctx->arena, recv_qn, pkg_len + 1));
}

static void java_emit_resolved_orig_kind(JavaLSPContext *ctx, const char *callee_qn,
const char *orig, const char *strategy, float confidence,
CBMResolvedKind kind) {
if (!ctx->resolved_calls || !ctx->enclosing_method_qn || !callee_qn)
return;
if (kind == CBM_RESOLVED_INVOCATION && java_callee_is_external(ctx, callee_qn)) {
strategy = "lsp_external";
}
CBMResolvedCall rc = {0};
rc.caller_qn = ctx->enclosing_method_qn;
rc.callee_qn = callee_qn;
Expand Down Expand Up @@ -2145,6 +2276,15 @@ static void resolve_method_call(JavaLSPContext *ctx, TSNode call) {
"lsp_interface_dispatch", 0.80f);
return;
}
/* A library type the registry does not hold: no method of it can be
* looked up, but no project method is its target either. A seeded
* JDK type with no such method stays a diagnostic below, as Rust's
* gate (#2053) needs a positively resolved std method. */
if (!rt && java_receiver_is_external(ctx, recv_qn)) {
java_emit_resolved(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", recv_qn, mname),
"lsp_external", 0.90f);
return;
}
java_emit_unresolved(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", recv_qn, mname),
"no_method_match");
return;
Expand Down Expand Up @@ -3214,6 +3354,9 @@ static void register_local_func_or_type_from_file(JavaLSPContext *ctx, CBMTypeRe
stub.qualified_name = d->qualified_name;
stub.short_name = d->name;
stub.is_interface = (strcmp(d->label, "Interface") == 0);
stub.namespace_qn = (ctx->package_name && ctx->package_name[0]) ? ctx->package_name
: (module_qn && module_qn[0]) ? module_qn
: d->qualified_name;
cbm_registry_add_type(reg, stub);
}

Expand Down Expand Up @@ -3740,6 +3883,11 @@ void cbm_java_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg, const CBM
rt.qualified_name = d->qualified_name;
rt.short_name = d->short_name;
rt.is_interface = (strcmp(d->label, "Interface") == 0) || d->is_interface;
/* Project marker (java_is_project_type) and the package that
* qualifies the type's own member signatures. */
rt.namespace_qn = (d->namespace_name && d->namespace_name[0]) ? d->namespace_name
: (d->def_module_qn && d->def_module_qn[0]) ? d->def_module_qn
: d->qualified_name;
/* Embedded types from "|"-separated text. */
if (d->embedded_types && d->embedded_types[0]) {
int n = 1;
Expand Down Expand Up @@ -3883,6 +4031,9 @@ CBMTypeRegistry *cbm_java_build_cross_registry(CBMArena *arena, CBMLSPDef *defs,
cbm_java_register_lsp_defs(arena, reg, jvm + type_count, total - type_count);
}
cbm_registry_finalize(reg);
/* java_receiver_is_external asks "does the project declare a type in
* this package?" per unresolved receiver: a binary search on this order. */
cbm_registry_build_type_qn_order(reg, arena);
reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */
return reg;
}
Expand Down Expand Up @@ -3958,6 +4109,9 @@ void cbm_run_java_lsp_cross_with_registry(CBMArena *arena, CBMFileResult *result
}
}

/* The per-file fallback when no shared cross registry was built. Its
* registry has no fallback layer, so java_receiver_is_external never fires
* here: library calls keep the textual registry fallback, as before. */
void cbm_run_java_lsp_cross(CBMArena *arena, const char *source, int source_len,
const char *module_qn, CBMLSPDef *defs, int def_count,
const char **import_names, const char **import_qns, int import_count,
Expand Down
8 changes: 8 additions & 0 deletions internal/cbm/lsp/java_lsp.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
#include "../cbm.h"
#include "go_lsp.h" /* CBMLSPDef, CBMResolvedCallArray reused across languages */

#define JAVA_EXT_MEMO 8

/* Java `use`-style import kinds. Mirrors PHP's enum with Java semantics. */
enum {
CBM_JAVA_IMPORT_TYPE = 0, /* import com.foo.Bar; — type import */
Expand Down Expand Up @@ -90,6 +92,12 @@ typedef struct {

/* Debug mode (CBM_LSP_DEBUG env). */
bool debug;

/* java_receiver_is_external's answers for the packages probed so far
* (a few per file), so each costs one registry walk. */
const char *ext_memo_pkg[JAVA_EXT_MEMO];
bool ext_memo_val[JAVA_EXT_MEMO];
int ext_memo_next;
} JavaLSPContext;

/* ── Initialization / configuration ───────────────────────────────── */
Expand Down
12 changes: 12 additions & 0 deletions src/pipeline/lsp_resolve.h
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,18 @@ static inline bool cbm_pipeline_kotlin_external_target(CBMLanguage lang, const c
strncmp(callee_qn, "javax.", strlen("javax.")) == 0;
}

/* Java: the LSP typed the receiver as a type OUTSIDE the project (a JDK or
* library class, `lsp_external`), so the callee is `java.util.Optional.map`
* and no project method. Without this gate the tail/unique-leaf node policy
* and then the textual registry bound such a call to whatever project method
* shares the leaf (`Optional.map` -> `Filters.map`). Such a call gets no
* CALLS edge, as #2053 does for Rust. Pure; unit-tested. */
static inline bool cbm_pipeline_java_external_target(CBMLanguage lang, const CBMResolvedCall *lsp) {
return lang == CBM_LANG_JAVA && lsp && lsp->strategy &&
strcmp(lsp->strategy, "lsp_external") == 0 && lsp->callee_qn &&
strchr(lsp->callee_qn, '.') != NULL;
}

/* Rust (#2053): true when the Rust LSP positively resolved a call to a
* registered symbol that lives OUTSIDE the project — a std/core/alloc seed
* method (`root.join()` on `root: &Path` -> std.path.Path.join) or a seeded
Expand Down
Loading
Loading