Skip to content

Commit 5f6461e

Browse files
authored
Merge pull request DeusData#818 from DeusData/distill/684-jvm-callgraph
fix(jvm): resolve mixed Java/Kotlin callgraph edges via declared packages
2 parents 85349a8 + 7b15209 commit 5f6461e

8 files changed

Lines changed: 753 additions & 140 deletions

File tree

‎internal/cbm/lsp/go_lsp.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ typedef struct {
8686
const char* method_names_str; // "|"-separated method names for interfaces (e.g. "Get|Put|Delete")
8787
bool is_interface;
8888
CBMLanguage lang; // language of the file that defined this — used by Tier 2 per-language registry build to filter all_defs
89+
const char* namespace_name; // declared namespace/package for source-root-independent JVM filtering
8990
} CBMLSPDef;
9091

9192
// Parse source, build registry from defs + stdlib, run LSP.

‎internal/cbm/lsp/kotlin_lsp.c‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4151,6 +4151,85 @@ void cbm_run_kotlin_lsp(CBMArena *arena, CBMFileResult *result, const char *sour
41514151
* a call site in another file resolves to the right node. Types and functions
41524152
* keep their full project-qualified QN; functions carry receiver_type so the
41534153
* sole-definer fallback can tell a top-level fun from a method. */
4154+
static const char *kt_cross_builtin_return_qn(const char *name) {
4155+
if (!name) {
4156+
return NULL;
4157+
}
4158+
if (strcmp(name, "String") == 0) {
4159+
return "kotlin.String";
4160+
}
4161+
if (strcmp(name, "Int") == 0 || strcmp(name, "Integer") == 0) {
4162+
return "kotlin.Int";
4163+
}
4164+
if (strcmp(name, "Long") == 0) {
4165+
return "kotlin.Long";
4166+
}
4167+
if (strcmp(name, "Float") == 0) {
4168+
return "kotlin.Float";
4169+
}
4170+
if (strcmp(name, "Double") == 0) {
4171+
return "kotlin.Double";
4172+
}
4173+
if (strcmp(name, "Boolean") == 0 || strcmp(name, "Bool") == 0) {
4174+
return "kotlin.Boolean";
4175+
}
4176+
if (strcmp(name, "Char") == 0 || strcmp(name, "Character") == 0) {
4177+
return "kotlin.Char";
4178+
}
4179+
if (strcmp(name, "Byte") == 0) {
4180+
return "kotlin.Byte";
4181+
}
4182+
if (strcmp(name, "Short") == 0) {
4183+
return "kotlin.Short";
4184+
}
4185+
if (strcmp(name, "Unit") == 0 || strcmp(name, "Void") == 0 || strcmp(name, "void") == 0) {
4186+
return "kotlin.Unit";
4187+
}
4188+
if (strcmp(name, "Any") == 0 || strcmp(name, "Object") == 0) {
4189+
return "kotlin.Any";
4190+
}
4191+
return NULL;
4192+
}
4193+
4194+
static const CBMType *kt_cross_return_type(CBMArena *arena, const CBMLSPDef *d) {
4195+
if (!arena || !d || !d->return_types || !d->return_types[0]) {
4196+
return NULL;
4197+
}
4198+
const char *text = d->return_types;
4199+
const char *bar = strchr(text, '|');
4200+
const char *first = bar ? cbm_arena_strndup(arena, text, (size_t)(bar - text)) : text;
4201+
if (!first || !first[0]) {
4202+
return NULL;
4203+
}
4204+
if (!strchr(first, '.')) {
4205+
const char *builtin = kt_cross_builtin_return_qn(first);
4206+
if (builtin) {
4207+
first = builtin;
4208+
} else if (d->namespace_name && d->namespace_name[0]) {
4209+
first = kt_join_dot(arena, d->namespace_name, first);
4210+
}
4211+
}
4212+
return cbm_type_named(arena, first);
4213+
}
4214+
4215+
static const CBMType *kt_cross_func_sig_with_return(CBMArena *arena, const CBMLSPDef *d) {
4216+
const CBMType *ret = kt_cross_return_type(arena, d);
4217+
if (!ret || cbm_type_is_unknown(ret)) {
4218+
return NULL;
4219+
}
4220+
const char **empty_pn = (const char **)cbm_arena_alloc(arena, sizeof(*empty_pn));
4221+
const CBMType **empty_pt = (const CBMType **)cbm_arena_alloc(arena, sizeof(*empty_pt));
4222+
const CBMType **rets = (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(*rets));
4223+
if (!empty_pn || !empty_pt || !rets) {
4224+
return NULL;
4225+
}
4226+
empty_pn[0] = NULL;
4227+
empty_pt[0] = NULL;
4228+
rets[0] = ret;
4229+
rets[1] = NULL;
4230+
return cbm_type_func(arena, empty_pn, empty_pt, rets);
4231+
}
4232+
41544233
static void kt_register_cross_def(CBMTypeRegistry *reg, CBMArena *arena, const CBMLSPDef *d) {
41554234
if (!d->qualified_name || !d->short_name || !d->label) {
41564235
return;
@@ -4200,6 +4279,7 @@ static void kt_register_cross_def(CBMTypeRegistry *reg, CBMArena *arena, const C
42004279
/* receiver_type distinguishes a top-level fun (NULL) from a method
42014280
* (set) — the sole-definer fallback only matches top-level funs. */
42024281
rf.receiver_type = d->receiver_type;
4282+
rf.signature = kt_cross_func_sig_with_return(arena, d);
42034283
cbm_registry_add_func(reg, rf);
42044284
}
42054285
}

‎src/pipeline/lsp_resolve.h‎

Lines changed: 150 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -64,25 +64,78 @@ static inline const char *cbm_lsp_bare_segment(const char *name) {
6464
return seg;
6565
}
6666

67+
/* Tail helper: return the start of the final two dot-separated segments
68+
* ("Class.method") or NULL when the QN is too short. */
69+
static inline const char *cbm_pipeline_qn_class_method_tail(const char *qn) {
70+
if (!qn) {
71+
return NULL;
72+
}
73+
const char *last = strrchr(qn, '.');
74+
if (!last || last == qn) {
75+
return NULL;
76+
}
77+
const char *second = last;
78+
while (second > qn) {
79+
second--;
80+
if (*second == '.') {
81+
if (second == qn) {
82+
return qn;
83+
}
84+
return second + 1;
85+
}
86+
}
87+
return qn;
88+
}
89+
90+
static inline const char *cbm_pipeline_call_callee_leaf(const char *callee_name) {
91+
return cbm_lsp_bare_segment(callee_name);
92+
}
93+
94+
/* Gate for the unique-`Class.method`-tail fallbacks below. Tail-matching by
95+
* leaf is safe where class-per-file package semantics hold — the JVM
96+
* languages (Java/Kotlin): the declared `package` is ground truth, a class
97+
* name is unique within a package, and mixed Gradle/Maven source roots
98+
* (`src/main/java` + `src/main/kotlin`) legitimately produce path-derived
99+
* module QNs that disagree with the package-shaped QNs the LSP emits, so
100+
* the tail is the only reliable join key. In other languages the same-name
101+
* guarantee does not exist (Python/TS re-export shims, Go internal clones,
102+
* C++ template instantiations), and a single wrong-module coincidence
103+
* would fabricate a CALLS edge — so the fallbacks stay off there. */
104+
static inline bool cbm_pipeline_lsp_allow_tail_match(CBMLanguage lang) {
105+
return lang == CBM_LANG_JAVA || lang == CBM_LANG_KOTLIN;
106+
}
107+
108+
static inline int cbm_pipeline_qn_class_method_tail_eq(const char *qn, const char *tail) {
109+
const char *qt = cbm_pipeline_qn_class_method_tail(qn);
110+
return qt && tail && strcmp(qt, tail) == 0;
111+
}
112+
67113
/* Look up the highest-confidence LSP-resolved call entry whose caller QN
68114
* matches the textual call's enclosing function and whose callee QN
69115
* short-name matches the textual callee. Returns a pointer into `arr`
70116
* or NULL if no qualifying entry exists.
71117
*
72-
* Match rule: the LSP emits CBMResolvedCall entries whose caller_qn
73-
* matches the call's enclosing function and whose callee_qn ends with
74-
* the textual callee_name as the last dot-separated segment. The
75-
* pointer returned aliases into `arr` and stays valid as long as the
118+
* Match rule:
119+
* 1. exact caller_qn + callee short-name match wins first;
120+
* 2. if no exact caller match exists AND allow_tail_match is set
121+
* (JVM callers only, see cbm_pipeline_lsp_allow_tail_match), a
122+
* unique Class.method tail match between rc->caller_qn and
123+
* call->enclosing_func_qn may win;
124+
* 3. ambiguous tails return NULL so the registry fallback stays in
125+
* control.
126+
*
127+
* The pointer returned aliases into `arr` and stays valid as long as the
76128
* underlying CBMFileResult is alive. */
77129
static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution(
78-
const CBMResolvedCallArray *arr, const CBMCall *call) {
130+
const CBMResolvedCallArray *arr, const CBMCall *call, bool allow_tail_match) {
79131
if (!arr || arr->count == 0 || !call) {
80132
return NULL;
81133
}
82134
if (!call->enclosing_func_qn || !call->callee_name) {
83135
return NULL;
84136
}
85-
const CBMResolvedCall *best = NULL;
137+
138+
const CBMResolvedCall *best_exact = NULL;
86139
for (int i = 0; i < arr->count; i++) {
87140
const CBMResolvedCall *rc = &arr->items[i];
88141
if (!rc->caller_qn || !rc->callee_qn) {
@@ -124,53 +177,120 @@ static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution(
124177
continue;
125178
}
126179
}
127-
if (!best || rc->confidence > best->confidence) {
128-
best = rc;
180+
if (!best_exact || rc->confidence > best_exact->confidence) {
181+
best_exact = rc;
182+
}
183+
}
184+
if (best_exact) {
185+
return best_exact;
186+
}
187+
if (!allow_tail_match) {
188+
return NULL;
189+
}
190+
191+
const char *call_tail = cbm_pipeline_qn_class_method_tail(call->enclosing_func_qn);
192+
if (!call_tail) {
193+
return NULL;
194+
}
195+
196+
const CBMResolvedCall *best_tail = NULL;
197+
for (int i = 0; i < arr->count; i++) {
198+
const CBMResolvedCall *rc = &arr->items[i];
199+
if (!rc->caller_qn || !rc->callee_qn) {
200+
continue;
201+
}
202+
if (rc->confidence < CBM_LSP_CONFIDENCE_FLOOR) {
203+
continue;
129204
}
205+
const char *short_name = strrchr(rc->callee_qn, '.');
206+
short_name = short_name ? short_name + SKIP_ONE : rc->callee_qn;
207+
const char *call_leaf = cbm_pipeline_call_callee_leaf(call->callee_name);
208+
if (!call_leaf || strcmp(short_name, call_leaf) != 0) {
209+
continue;
210+
}
211+
if (!cbm_pipeline_qn_class_method_tail_eq(rc->caller_qn, call_tail)) {
212+
continue;
213+
}
214+
if (best_tail) {
215+
return NULL;
216+
}
217+
best_tail = rc;
130218
}
131-
return best;
219+
return best_tail;
132220
}
133221

134222
/* Resolve an LSP-emitted callee_qn to a graph-buffer node.
135223
*
136-
* Per-file LSPs (notably py_lsp) sometimes emit `callee_qn` as the raw
137-
* import-module path the source code uses (e.g. `greeter.Greeter` from
138-
* `from greeter import Greeter`) rather than the project-qualified QN
139-
* the gbuf actually stores (`<project>.greeter.Greeter`). This is
140-
* unavoidable at the per-file LSP layer: the LSP cannot tell in-project
141-
* imports (qualify) from external imports (don't qualify, e.g. `os.path`)
142-
* without consulting the gbuf, which is built downstream.
143-
*
144-
* The fallback rule: try the LSP-emitted QN as-is first; on miss, retry
145-
* with `<project>.<callee_qn>`. If that also misses, the target is
146-
* external/unknown and the caller drops the edge — same as today.
224+
* Per-file LSPs sometimes emit `callee_qn` as the raw package-shaped
225+
* import path the source code uses rather than the project-qualified QN
226+
* the gbuf actually stores. The fallback rule is:
227+
* 1. try the LSP-emitted QN as-is;
228+
* 2. retry with `<project>.<callee_qn>` when needed;
229+
* 3. if both fail AND allow_tail_match is set (JVM callers only, see
230+
* cbm_pipeline_lsp_allow_tail_match), use the exact node-name index
231+
* to narrow candidates by short method name and accept exactly one
232+
* Function/Method whose qualified_name has the same Class.method
233+
* tail.
147234
*
148235
* Returns the matching node, or NULL if neither lookup hits. */
149236
static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node(const cbm_gbuf_t *gbuf,
150237
const char *project_name,
151-
const char *callee_qn) {
238+
const char *callee_qn,
239+
bool allow_tail_match) {
152240
if (!gbuf || !callee_qn) {
153241
return NULL;
154242
}
155243
const cbm_gbuf_node_t *direct = cbm_gbuf_find_by_qn(gbuf, callee_qn);
156244
if (direct) {
157245
return direct;
158246
}
159-
if (!project_name || !project_name[0]) {
247+
if (project_name && project_name[0]) {
248+
size_t proj_len = strlen(project_name);
249+
if (!(strncmp(callee_qn, project_name, proj_len) == 0 && callee_qn[proj_len] == '.')) {
250+
char buf[CBM_SZ_1K];
251+
int written = snprintf(buf, sizeof(buf), "%s.%s", project_name, callee_qn);
252+
if (written > 0 && (size_t)written < sizeof(buf)) {
253+
const cbm_gbuf_node_t *prefixed = cbm_gbuf_find_by_qn(gbuf, buf);
254+
if (prefixed) {
255+
return prefixed;
256+
}
257+
}
258+
}
259+
}
260+
if (!allow_tail_match) {
160261
return NULL;
161262
}
162-
/* Skip the prefix retry if callee_qn is already project-qualified —
163-
* avoids producing nonsense like `proj.proj.foo.Bar`. */
164-
size_t proj_len = strlen(project_name);
165-
if (strncmp(callee_qn, project_name, proj_len) == 0 && callee_qn[proj_len] == '.') {
263+
264+
const char *short_name = strrchr(callee_qn, '.');
265+
short_name = short_name ? short_name + SKIP_ONE : callee_qn;
266+
const char *callee_tail = cbm_pipeline_qn_class_method_tail(callee_qn);
267+
if (!callee_tail) {
166268
return NULL;
167269
}
168-
char buf[CBM_SZ_1K];
169-
int written = snprintf(buf, sizeof(buf), "%s.%s", project_name, callee_qn);
170-
if (written < 0 || (size_t)written >= sizeof(buf)) {
270+
const cbm_gbuf_node_t **hits = NULL;
271+
int hit_count = 0;
272+
if (cbm_gbuf_find_by_name(gbuf, short_name, &hits, &hit_count) != 0 || hit_count == 0) {
171273
return NULL;
172274
}
173-
return cbm_gbuf_find_by_qn(gbuf, buf);
275+
276+
const cbm_gbuf_node_t *match = NULL;
277+
for (int i = 0; i < hit_count; i++) {
278+
const cbm_gbuf_node_t *cand = hits[i];
279+
if (!cand || !cand->label || !cand->qualified_name) {
280+
continue;
281+
}
282+
if (strcmp(cand->label, "Function") != 0 && strcmp(cand->label, "Method") != 0) {
283+
continue;
284+
}
285+
if (!cbm_pipeline_qn_class_method_tail_eq(cand->qualified_name, callee_tail)) {
286+
continue;
287+
}
288+
if (match) {
289+
return NULL;
290+
}
291+
match = cand;
292+
}
293+
return match;
174294
}
175295

176296
#endif /* CBM_PIPELINE_LSP_RESOLVE_H */

‎src/pipeline/pass_calls.c‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -428,11 +428,13 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
428428
return 0;
429429
}
430430

431-
/* LSP-resolved calls take precedence over registry-textual matching. */
432-
const CBMResolvedCall *lsp = cbm_pipeline_find_lsp_resolution(lsp_calls, call);
431+
/* LSP-resolved calls take precedence over registry-textual matching.
432+
* Unique-tail fallbacks are JVM-only (see cbm_pipeline_lsp_allow_tail_match). */
433+
bool allow_tail = cbm_pipeline_lsp_allow_tail_match(lang);
434+
const CBMResolvedCall *lsp = cbm_pipeline_find_lsp_resolution(lsp_calls, call, allow_tail);
433435
if (lsp) {
434436
const cbm_gbuf_node_t *target_node =
435-
cbm_pipeline_lsp_target_node(ctx->gbuf, ctx->project_name, lsp->callee_qn);
437+
cbm_pipeline_lsp_target_node(ctx->gbuf, ctx->project_name, lsp->callee_qn, allow_tail);
436438
if (target_node && source_node->id != target_node->id) {
437439
cbm_resolution_t res = {0};
438440
/* Use the gbuf node's QN so downstream edge props show the canonical

0 commit comments

Comments
 (0)