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
1 change: 1 addition & 0 deletions internal/cbm/lsp/go_lsp.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ typedef struct {
const char* method_names_str; // "|"-separated method names for interfaces (e.g. "Get|Put|Delete")
bool is_interface;
CBMLanguage lang; // language of the file that defined this — used by Tier 2 per-language registry build to filter all_defs
const char* namespace_name; // declared namespace/package for source-root-independent JVM filtering
} CBMLSPDef;

// Parse source, build registry from defs + stdlib, run LSP.
Expand Down
80 changes: 80 additions & 0 deletions internal/cbm/lsp/kotlin_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -4151,6 +4151,85 @@ void cbm_run_kotlin_lsp(CBMArena *arena, CBMFileResult *result, const char *sour
* a call site in another file resolves to the right node. Types and functions
* keep their full project-qualified QN; functions carry receiver_type so the
* sole-definer fallback can tell a top-level fun from a method. */
static const char *kt_cross_builtin_return_qn(const char *name) {
if (!name) {
return NULL;
}
if (strcmp(name, "String") == 0) {
return "kotlin.String";
}
if (strcmp(name, "Int") == 0 || strcmp(name, "Integer") == 0) {
return "kotlin.Int";
}
if (strcmp(name, "Long") == 0) {
return "kotlin.Long";
}
if (strcmp(name, "Float") == 0) {
return "kotlin.Float";
}
if (strcmp(name, "Double") == 0) {
return "kotlin.Double";
}
if (strcmp(name, "Boolean") == 0 || strcmp(name, "Bool") == 0) {
return "kotlin.Boolean";
}
if (strcmp(name, "Char") == 0 || strcmp(name, "Character") == 0) {
return "kotlin.Char";
}
if (strcmp(name, "Byte") == 0) {
return "kotlin.Byte";
}
if (strcmp(name, "Short") == 0) {
return "kotlin.Short";
}
if (strcmp(name, "Unit") == 0 || strcmp(name, "Void") == 0 || strcmp(name, "void") == 0) {
return "kotlin.Unit";
}
if (strcmp(name, "Any") == 0 || strcmp(name, "Object") == 0) {
return "kotlin.Any";
}
return NULL;
}

static const CBMType *kt_cross_return_type(CBMArena *arena, const CBMLSPDef *d) {
if (!arena || !d || !d->return_types || !d->return_types[0]) {
return NULL;
}
const char *text = d->return_types;
const char *bar = strchr(text, '|');
const char *first = bar ? cbm_arena_strndup(arena, text, (size_t)(bar - text)) : text;
if (!first || !first[0]) {
return NULL;
}
if (!strchr(first, '.')) {
const char *builtin = kt_cross_builtin_return_qn(first);
if (builtin) {
first = builtin;
} else if (d->namespace_name && d->namespace_name[0]) {
first = kt_join_dot(arena, d->namespace_name, first);
}
}
return cbm_type_named(arena, first);
}

static const CBMType *kt_cross_func_sig_with_return(CBMArena *arena, const CBMLSPDef *d) {
const CBMType *ret = kt_cross_return_type(arena, d);
if (!ret || cbm_type_is_unknown(ret)) {
return NULL;
}
const char **empty_pn = (const char **)cbm_arena_alloc(arena, sizeof(*empty_pn));
const CBMType **empty_pt = (const CBMType **)cbm_arena_alloc(arena, sizeof(*empty_pt));
const CBMType **rets = (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(*rets));
if (!empty_pn || !empty_pt || !rets) {
return NULL;
}
empty_pn[0] = NULL;
empty_pt[0] = NULL;
rets[0] = ret;
rets[1] = NULL;
return cbm_type_func(arena, empty_pn, empty_pt, rets);
}

static void kt_register_cross_def(CBMTypeRegistry *reg, CBMArena *arena, const CBMLSPDef *d) {
if (!d->qualified_name || !d->short_name || !d->label) {
return;
Expand Down Expand Up @@ -4200,6 +4279,7 @@ static void kt_register_cross_def(CBMTypeRegistry *reg, CBMArena *arena, const C
/* receiver_type distinguishes a top-level fun (NULL) from a method
* (set) — the sole-definer fallback only matches top-level funs. */
rf.receiver_type = d->receiver_type;
rf.signature = kt_cross_func_sig_with_return(arena, d);
cbm_registry_add_func(reg, rf);
}
}
Expand Down
180 changes: 150 additions & 30 deletions src/pipeline/lsp_resolve.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,25 +64,78 @@ static inline const char *cbm_lsp_bare_segment(const char *name) {
return seg;
}

/* Tail helper: return the start of the final two dot-separated segments
* ("Class.method") or NULL when the QN is too short. */
static inline const char *cbm_pipeline_qn_class_method_tail(const char *qn) {
if (!qn) {
return NULL;
}
const char *last = strrchr(qn, '.');
if (!last || last == qn) {
return NULL;
}
const char *second = last;
while (second > qn) {
second--;
if (*second == '.') {
if (second == qn) {
return qn;
}
return second + 1;
}
}
return qn;
}

static inline const char *cbm_pipeline_call_callee_leaf(const char *callee_name) {
return cbm_lsp_bare_segment(callee_name);
}

/* Gate for the unique-`Class.method`-tail fallbacks below. Tail-matching by
* leaf is safe where class-per-file package semantics hold — the JVM
* languages (Java/Kotlin): the declared `package` is ground truth, a class
* name is unique within a package, and mixed Gradle/Maven source roots
* (`src/main/java` + `src/main/kotlin`) legitimately produce path-derived
* module QNs that disagree with the package-shaped QNs the LSP emits, so
* the tail is the only reliable join key. In other languages the same-name
* guarantee does not exist (Python/TS re-export shims, Go internal clones,
* C++ template instantiations), and a single wrong-module coincidence
* would fabricate a CALLS edge — so the fallbacks stay off there. */
static inline bool cbm_pipeline_lsp_allow_tail_match(CBMLanguage lang) {
return lang == CBM_LANG_JAVA || lang == CBM_LANG_KOTLIN;
}

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;
}

/* Look up the highest-confidence LSP-resolved call entry whose caller QN
* matches the textual call's enclosing function and whose callee QN
* short-name matches the textual callee. Returns a pointer into `arr`
* or NULL if no qualifying entry exists.
*
* Match rule: the LSP emits CBMResolvedCall entries whose caller_qn
* matches the call's enclosing function and whose callee_qn ends with
* the textual callee_name as the last dot-separated segment. The
* pointer returned aliases into `arr` and stays valid as long as the
* Match rule:
* 1. exact caller_qn + callee short-name match wins first;
* 2. if no exact caller match exists AND allow_tail_match is set
* (JVM callers only, see cbm_pipeline_lsp_allow_tail_match), a
* unique Class.method tail match between rc->caller_qn and
* call->enclosing_func_qn may win;
* 3. ambiguous tails return NULL so the registry fallback stays in
* control.
*
* The pointer returned aliases into `arr` and stays valid as long as the
* underlying CBMFileResult is alive. */
static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution(
const CBMResolvedCallArray *arr, const CBMCall *call) {
const CBMResolvedCallArray *arr, const CBMCall *call, bool allow_tail_match) {
if (!arr || arr->count == 0 || !call) {
return NULL;
}
if (!call->enclosing_func_qn || !call->callee_name) {
return NULL;
}
const CBMResolvedCall *best = NULL;

const CBMResolvedCall *best_exact = NULL;
for (int i = 0; i < arr->count; i++) {
const CBMResolvedCall *rc = &arr->items[i];
if (!rc->caller_qn || !rc->callee_qn) {
Expand Down Expand Up @@ -124,53 +177,120 @@ static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution(
continue;
}
}
if (!best || rc->confidence > best->confidence) {
best = rc;
if (!best_exact || rc->confidence > best_exact->confidence) {
best_exact = rc;
}
}
if (best_exact) {
return best_exact;
}
if (!allow_tail_match) {
return NULL;
}

const char *call_tail = cbm_pipeline_qn_class_method_tail(call->enclosing_func_qn);
if (!call_tail) {
return NULL;
}

const CBMResolvedCall *best_tail = NULL;
for (int i = 0; i < arr->count; i++) {
const CBMResolvedCall *rc = &arr->items[i];
if (!rc->caller_qn || !rc->callee_qn) {
continue;
}
if (rc->confidence < CBM_LSP_CONFIDENCE_FLOOR) {
continue;
}
const char *short_name = strrchr(rc->callee_qn, '.');
short_name = short_name ? short_name + SKIP_ONE : rc->callee_qn;
const char *call_leaf = cbm_pipeline_call_callee_leaf(call->callee_name);
if (!call_leaf || strcmp(short_name, call_leaf) != 0) {
continue;
}
if (!cbm_pipeline_qn_class_method_tail_eq(rc->caller_qn, call_tail)) {
continue;
}
if (best_tail) {
return NULL;
}
best_tail = rc;
}
return best;
return best_tail;
}

/* Resolve an LSP-emitted callee_qn to a graph-buffer node.
*
* Per-file LSPs (notably py_lsp) sometimes emit `callee_qn` as the raw
* import-module path the source code uses (e.g. `greeter.Greeter` from
* `from greeter import Greeter`) rather than the project-qualified QN
* the gbuf actually stores (`<project>.greeter.Greeter`). This is
* unavoidable at the per-file LSP layer: the LSP cannot tell in-project
* imports (qualify) from external imports (don't qualify, e.g. `os.path`)
* without consulting the gbuf, which is built downstream.
*
* The fallback rule: try the LSP-emitted QN as-is first; on miss, retry
* with `<project>.<callee_qn>`. If that also misses, the target is
* external/unknown and the caller drops the edge — same as today.
* Per-file LSPs sometimes emit `callee_qn` as the raw package-shaped
* import path the source code uses rather than the project-qualified QN
* the gbuf actually stores. The fallback rule is:
* 1. try the LSP-emitted QN as-is;
* 2. retry with `<project>.<callee_qn>` when needed;
* 3. if both fail AND allow_tail_match is set (JVM callers only, see
* cbm_pipeline_lsp_allow_tail_match), use the exact node-name index
* to narrow candidates by short method name and accept exactly one
* Function/Method whose qualified_name has the same Class.method
* tail.
*
* Returns the matching node, or NULL if neither lookup hits. */
static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node(const cbm_gbuf_t *gbuf,
const char *project_name,
const char *callee_qn) {
const char *callee_qn,
bool allow_tail_match) {
if (!gbuf || !callee_qn) {
return NULL;
}
const cbm_gbuf_node_t *direct = cbm_gbuf_find_by_qn(gbuf, callee_qn);
if (direct) {
return direct;
}
if (!project_name || !project_name[0]) {
if (project_name && project_name[0]) {
size_t proj_len = strlen(project_name);
if (!(strncmp(callee_qn, project_name, proj_len) == 0 && callee_qn[proj_len] == '.')) {
char buf[CBM_SZ_1K];
int written = snprintf(buf, sizeof(buf), "%s.%s", project_name, callee_qn);
if (written > 0 && (size_t)written < sizeof(buf)) {
const cbm_gbuf_node_t *prefixed = cbm_gbuf_find_by_qn(gbuf, buf);
if (prefixed) {
return prefixed;
}
}
}
}
if (!allow_tail_match) {
return NULL;
}
/* Skip the prefix retry if callee_qn is already project-qualified —
* avoids producing nonsense like `proj.proj.foo.Bar`. */
size_t proj_len = strlen(project_name);
if (strncmp(callee_qn, project_name, proj_len) == 0 && callee_qn[proj_len] == '.') {

const char *short_name = strrchr(callee_qn, '.');
short_name = short_name ? short_name + SKIP_ONE : callee_qn;
const char *callee_tail = cbm_pipeline_qn_class_method_tail(callee_qn);
if (!callee_tail) {
return NULL;
}
char buf[CBM_SZ_1K];
int written = snprintf(buf, sizeof(buf), "%s.%s", project_name, callee_qn);
if (written < 0 || (size_t)written >= sizeof(buf)) {
const cbm_gbuf_node_t **hits = NULL;
int hit_count = 0;
if (cbm_gbuf_find_by_name(gbuf, short_name, &hits, &hit_count) != 0 || hit_count == 0) {
return NULL;
}
return cbm_gbuf_find_by_qn(gbuf, buf);

const cbm_gbuf_node_t *match = NULL;
for (int i = 0; i < hit_count; i++) {
const cbm_gbuf_node_t *cand = hits[i];
if (!cand || !cand->label || !cand->qualified_name) {
continue;
}
if (strcmp(cand->label, "Function") != 0 && strcmp(cand->label, "Method") != 0) {
continue;
}
if (!cbm_pipeline_qn_class_method_tail_eq(cand->qualified_name, callee_tail)) {
continue;
}
if (match) {
return NULL;
}
match = cand;
}
return match;
}

#endif /* CBM_PIPELINE_LSP_RESOLVE_H */
8 changes: 5 additions & 3 deletions src/pipeline/pass_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -428,11 +428,13 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
return 0;
}

/* LSP-resolved calls take precedence over registry-textual matching. */
const CBMResolvedCall *lsp = cbm_pipeline_find_lsp_resolution(lsp_calls, call);
/* LSP-resolved calls take precedence over registry-textual matching.
* Unique-tail fallbacks are JVM-only (see cbm_pipeline_lsp_allow_tail_match). */
bool allow_tail = cbm_pipeline_lsp_allow_tail_match(lang);
const CBMResolvedCall *lsp = cbm_pipeline_find_lsp_resolution(lsp_calls, call, allow_tail);
if (lsp) {
const cbm_gbuf_node_t *target_node =
cbm_pipeline_lsp_target_node(ctx->gbuf, ctx->project_name, lsp->callee_qn);
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
Loading
Loading