Skip to content
Open
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
3 changes: 2 additions & 1 deletion Makefile.cbm
Original file line number Diff line number Diff line change
Expand Up @@ -845,7 +845,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
TEST_RUST_LSP_SRCS = tests/test_rust_lsp.c

Expand Down
153 changes: 153 additions & 0 deletions internal/cbm/lsp/java_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,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 +1803,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 +2275,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 +3353,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 +3882,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 +4030,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 +4108,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
13 changes: 13 additions & 0 deletions src/pipeline/lsp_resolve.h
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,19 @@ 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
17 changes: 12 additions & 5 deletions src/pipeline/pass_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -870,14 +870,21 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
* Service/route classification below still runs. Mirrors pass_parallel.c. */
bool php_vendor_call = cbm_pipeline_php_vendor_call(php_vendor, result, call) ||
cbm_pipeline_lsp_external_receiver(lsp);
/* A Java call on a JDK/library type is never bound to a project method
* found by name (no tail match, no registry guess). MUST match
* pass_parallel.c. */
bool java_external = cbm_pipeline_java_external_target(lang, lsp);
if (lsp) {
bool exact_external_target = call->requires_lsp_resolution &&
cbm_pipeline_kotlin_external_target(lang, lsp->callee_qn);
const cbm_gbuf_node_t *target_node =
exact_external_target ? cbm_pipeline_lsp_target_node_strict(
ctx->gbuf, ctx->project_name, lsp->callee_qn, allow_tail)
: cbm_pipeline_lsp_target_node(ctx->gbuf, ctx->project_name,
lsp->callee_qn, allow_tail);
java_external ? cbm_pipeline_lsp_target_node_strict(ctx->gbuf, ctx->project_name,
lsp->callee_qn, false)
: exact_external_target
? cbm_pipeline_lsp_target_node_strict(ctx->gbuf, ctx->project_name,
lsp->callee_qn, allow_tail)
: cbm_pipeline_lsp_target_node(ctx->gbuf, ctx->project_name, lsp->callee_qn,
allow_tail);
if (target_node && source_node->id != target_node->id) {
cbm_resolution_t res = {0};
/* Use the gbuf node's QN so downstream edge props show the canonical
Expand Down Expand Up @@ -966,7 +973,7 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
bool rust_external = lsp && cbm_pipeline_rust_external_target(
lang, lsp->strategy, lsp->callee_qn, ctx->project_name);
cbm_resolution_t res = {0};
if (!rust_external && !php_vendor_call) {
if (!rust_external && !php_vendor_call && !java_external) {
res = cbm_registry_resolve_call_lang(ctx->registry, call->callee_name, module_qn, imp_keys,
imp_vals, imp_count, lang);
/* Cross-language veto: a name-only guess never binds another
Expand Down
1 change: 1 addition & 0 deletions src/pipeline/pass_lsp_cross.c
Original file line number Diff line number Diff line change
Expand Up @@ -2020,6 +2020,7 @@ int cbm_pipeline_pass_lsp_cross(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *
cross_registries.c = cbm_c_build_cross_registry(xa, all_defs, def_count);
cross_registries.cs = cbm_cs_build_cross_registry(xa, all_defs, def_count);
cross_registries.ts = cbm_ts_build_cross_registry(xa, all_defs, def_count);
cross_registries.java = cbm_java_build_cross_registry(xa, all_defs, def_count);
}

int processed = 0;
Expand Down
10 changes: 8 additions & 2 deletions src/pipeline/pass_parallel.c
Original file line number Diff line number Diff line change
Expand Up @@ -3223,13 +3223,19 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
memory_order_relaxed);
_rc_t0 = extract_now_ns();
const cbm_gbuf_node_t *lsp_target = NULL;
/* Mirrors pass_calls.c: a Java call on a JDK/library type is never
* bound to a project method found by name. */
bool java_external = cbm_pipeline_java_external_target(lang, lsp);
if (lsp) {
/* Canonicalise to the gbuf node's QN so res.qualified_name matches
* the gbuf even when the cross-file fallback had to prefix the
* project name. */
bool exact_external_target = call->requires_lsp_resolution &&
cbm_pipeline_kotlin_external_target(lang, lsp->callee_qn);
lsp_target = exact_external_target
lsp_target = java_external
? cbm_pipeline_lsp_target_node_strict(rc->main_gbuf, rc->project_name,
lsp->callee_qn, false)
: exact_external_target
? cbm_pipeline_lsp_target_node_strict(rc->main_gbuf, rc->project_name,
lsp->callee_qn, allow_tail)
: cbm_pipeline_lsp_target_node(rc->main_gbuf, rc->project_name,
Expand Down Expand Up @@ -3266,7 +3272,7 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
bool rust_external = lsp && cbm_pipeline_rust_external_target(
lang, lsp->strategy, lsp->callee_qn, rc->project_name);
if ((!res.qualified_name || !res.qualified_name[0]) && !call->requires_lsp_resolution &&
!rust_external && !php_vendor_call) {
!rust_external && !php_vendor_call && !java_external) {
res = cbm_registry_resolve_call_lang(rc->registry, call->callee_name, module_qn,
imp_keys, imp_vals, imp_count, lang);
}
Expand Down
Loading
Loading