Skip to content

Commit a824d82

Browse files
authored
Merge pull request #1939 from xkchok/fix/go-cross-package-field-dispatch
fix(go): resolve cross-package field-chain calls via type dispatch
2 parents 5a5be74 + e11d590 commit a824d82

4 files changed

Lines changed: 487 additions & 41 deletions

File tree

internal/cbm/lsp/go_lsp.c

Lines changed: 85 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -866,11 +866,53 @@ const CBMType* go_eval_builtin_call(GoLSPContext* ctx, const char* name, TSNode
866866

867867
// --- go_lookup_field: struct field lookup with embedding recursion ---
868868

869+
// --- Import-alias re-qualification ------------------------------------
870+
//
871+
// parse_field_defs_into_type qualifies struct field type texts as
872+
// "<def_module>.<text>". When the author wrote the text through an import
873+
// alias ("Svc:svc.Svc" in module test.main), that yields a QN
874+
// ("test.main.svc.Svc") that exists nowhere in the project-wide registry —
875+
// the real QN is "<import_qn>.Svc" and only the calling file's import map
876+
// can say so. On an exact-QN miss this rewrites the alias segment through
877+
// the file's imports; returns NULL when no alias segment is involved.
878+
879+
static const char *go_requalify_via_imports(GoLSPContext *ctx, const char *type_qn) {
880+
if (!ctx || !type_qn || !type_qn[0] || ctx->import_count <= 0) return NULL;
881+
for (int j = 0; j < ctx->import_count; j++) {
882+
const char *alias = ctx->import_local_names[j];
883+
const char *alias_qn = ctx->import_package_qns[j];
884+
if (!alias || !alias[0] || !alias_qn || strchr(alias, '.')) continue;
885+
size_t alias_len = strlen(alias);
886+
/* Last occurrence of a "<dot>alias<dot>" segment in type_qn. */
887+
const char *hit = NULL;
888+
for (const char *p = type_qn;;) {
889+
p = strstr(p, ".");
890+
if (!p) break;
891+
p++;
892+
if (strncmp(p, alias, alias_len) == 0 && p[alias_len] == '.') {
893+
hit = p;
894+
p += alias_len;
895+
}
896+
}
897+
if (hit) {
898+
const char *rest = hit + alias_len + 1; /* past "<alias>." */
899+
return cbm_arena_sprintf(ctx->arena, "%s.%s", alias_qn, rest);
900+
}
901+
}
902+
return NULL;
903+
}
904+
869905
static const CBMType* go_lookup_field(GoLSPContext* ctx,
870906
const char* type_qn, const char* field_name, int depth) {
871907
if (!type_qn || !field_name || depth > 5) return NULL;
872908

873909
const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry, type_qn);
910+
if (!rt && depth == 0) {
911+
/* Import-alias re-qualification: field texts from cross-package defs
912+
* may embed an alias segment only this file's import map resolves. */
913+
const char* alt_qn = go_requalify_via_imports(ctx, type_qn);
914+
if (alt_qn) rt = cbm_registry_lookup_type(ctx->registry, alt_qn);
915+
}
874916
if (!rt) return NULL;
875917

876918
// Follow alias chain
@@ -907,6 +949,18 @@ static const CBMRegisteredFunc* go_lookup_field_or_method_depth(GoLSPContext* ct
907949
const CBMRegisteredFunc* f = cbm_registry_lookup_method(ctx->registry, type_qn, member_name);
908950
if (f) return f;
909951

952+
/* Import-alias re-qualification fallback: NAMED receivers built from
953+
* cross-package field type texts can carry a "<module>.svc.Svc" QN;
954+
* retry the method set on the import-resolved QN (see
955+
* go_requalify_via_imports). */
956+
if (depth == 0) {
957+
const char* alt_qn = go_requalify_via_imports(ctx, type_qn);
958+
if (alt_qn) {
959+
f = go_lookup_field_or_method_depth(ctx, alt_qn, member_name, depth + 1);
960+
if (f) return f;
961+
}
962+
}
963+
910964
const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry, type_qn);
911965
if (rt) {
912966
// Follow type alias chain
@@ -1430,20 +1484,32 @@ static void resolve_calls_in_node_inner(GoLSPContext* ctx, TSNode node) {
14301484
if (base && base->kind == CBM_TYPE_POINTER) base = cbm_type_deref(base);
14311485

14321486
if (base && base->kind == CBM_TYPE_NAMED) {
1487+
const char *recv_qn = base->data.named.qualified_name;
14331488
const CBMRegisteredType *receiver_type = cbm_registry_lookup_type(
1434-
ctx->registry, base->data.named.qualified_name);
1489+
ctx->registry, recv_qn);
1490+
const char *alt_qn = NULL;
1491+
/* Re-qualify NAMED receivers that embed an import
1492+
* alias segment (cross-package field type texts) —
1493+
* the real type only exists under the import QN. */
1494+
if (!receiver_type) {
1495+
alt_qn = go_requalify_via_imports(ctx, recv_qn);
1496+
if (alt_qn) {
1497+
receiver_type = cbm_registry_lookup_type(ctx->registry, alt_qn);
1498+
if (receiver_type) recv_qn = alt_qn;
1499+
}
1500+
}
14351501
/* Registered interface receivers must reach the
14361502
* interface-resolution branch below. Their semantic
14371503
* method registrations are signatures, not concrete
14381504
* dispatch targets. */
14391505
if (!receiver_type || !receiver_type->is_interface) {
14401506
const CBMRegisteredFunc *method = go_lookup_field_or_method(
1441-
ctx, base->data.named.qualified_name, field_name);
1507+
ctx, recv_qn, field_name);
14421508
if (method) {
14431509
const char *strategy = "lsp_type_dispatch";
14441510
if (method->receiver_type &&
14451511
strcmp(method->receiver_type,
1446-
base->data.named.qualified_name) != 0) {
1512+
recv_qn) != 0) {
14471513
strategy = "lsp_embed_dispatch";
14481514
}
14491515
emit_resolved_call(ctx, method->qualified_name, strategy, 0.95f,
@@ -1460,9 +1526,15 @@ static void resolve_calls_in_node_inner(GoLSPContext* ctx, TSNode node) {
14601526
if (!is_iface && base->kind == CBM_TYPE_NAMED) {
14611527
const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry,
14621528
base->data.named.qualified_name);
1529+
if (!rt) {
1530+
const char* alt_qn = go_requalify_via_imports(
1531+
ctx, base->data.named.qualified_name);
1532+
if (alt_qn)
1533+
rt = cbm_registry_lookup_type(ctx->registry, alt_qn);
1534+
}
14631535
if (rt && rt->is_interface) {
14641536
is_iface = true;
1465-
iface_qn = base->data.named.qualified_name;
1537+
iface_qn = rt->qualified_name;
14661538
}
14671539
}
14681540
if (is_iface) {
@@ -1787,42 +1859,15 @@ static void process_function(GoLSPContext* ctx, TSNode func_node) {
17871859
char* func_name = lsp_node_text(ctx, name_node);
17881860
if (!func_name || !func_name[0]) return;
17891861

1790-
// For methods, the enclosing-function QN must include the receiver type
1791-
// (package.Type.Method), matching how the textual extractor and the
1792-
// registry qualify the method. Building it as package.Method (no receiver)
1793-
// here made the LSP-resolved call's caller_qn disagree with the textual
1794-
// call's enclosing_func_qn, so cbm_pipeline_find_lsp_resolution never
1795-
// joined them — every call inside a method body silently lost its
1796-
// type-aware LSP strategy. Derive the bare receiver type name the same way
1797-
// the receiver binding below does.
1798-
char* recv_type_name = NULL;
1799-
{
1800-
TSNode recv0 = ts_node_child_by_field_name(func_node, "receiver", 8);
1801-
if (!ts_node_is_null(recv0)) {
1802-
uint32_t rnc0 = ts_node_child_count(recv0);
1803-
for (uint32_t i = 0; i < rnc0 && !recv_type_name; i++) {
1804-
TSNode rp = ts_node_child(recv0, i);
1805-
if (ts_node_is_null(rp) || !ts_node_is_named(rp)) continue;
1806-
if (strcmp(ts_node_type(rp), "parameter_declaration") != 0) continue;
1807-
TSNode rtype = ts_node_child_by_field_name(rp, "type", 4);
1808-
if (ts_node_is_null(rtype)) continue;
1809-
// Unwrap a pointer receiver (*Type) to the bare type identifier.
1810-
const char* rtk = ts_node_type(rtype);
1811-
if (strcmp(rtk, "pointer_type") == 0 && ts_node_named_child_count(rtype) > 0) {
1812-
rtype = ts_node_named_child(rtype, 0);
1813-
}
1814-
char* tn = lsp_node_text(ctx, rtype);
1815-
if (tn && tn[0]) recv_type_name = tn;
1816-
}
1817-
}
1818-
}
1819-
1820-
if (recv_type_name) {
1821-
ctx->enclosing_func_qn =
1822-
cbm_arena_sprintf(ctx->arena, "%s.%s.%s", ctx->package_qn, recv_type_name, func_name);
1823-
} else {
1824-
ctx->enclosing_func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->package_qn, func_name);
1825-
}
1862+
// Enclosing-function QN must be the BARE package.Func form (no receiver
1863+
// type segment). The textual call events (extract_unified.c) source calls
1864+
// as package_qn.func_name — methods included — and the defs pass creates
1865+
// the graph Method node under the same QN, so any other form breaks the
1866+
// caller-QN join in cbm_pipeline_find_lsp_resolution and the LSP-resolved
1867+
// call silently falls back to the registry short-name resolver. The
1868+
// receiver type still reaches the registry via the def's parent_class /
1869+
// method->receiver_type; it just does not appear in the caller QN.
1870+
ctx->enclosing_func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->package_qn, func_name);
18261871

18271872
// Push function scope
18281873
CBMScope* saved_scope = ctx->current_scope;

src/pipeline/pass_lsp_cross.c

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,73 @@ static int pxc_build_lsp_def(CBMArena *arena, const CBMDefinition *src, const ch
413413
return 0;
414414
}
415415

416+
/* Go: fold per-field "Field" definitions into their owning struct's
417+
* field_defs. extract_defs.c emits one flat CBMDefinition per struct field
418+
* (label "Field", parent_class = owning struct QN, name = field name,
419+
* return_type = raw type text). Those rows are dropped by pxc_build_lsp_def
420+
* (pxc_map_label excludes "Field"), so without this fold every Go struct
421+
* registers with zero fields and field-chain calls (h.svc.Handle) can
422+
* never resolve. Fields are always declared in the same file as their struct,
423+
* so scanning the file's own defs covers every case. Runs inside
424+
* cbm_pxc_collect_all_defs — one site covers both the prebuilt-registry path
425+
* and the per-file fallback, since both consume all_defs. */
426+
static void pxc_fold_go_struct_fields(CBMArena *arena, const CBMFileResult *result, CBMLSPDef *defs,
427+
int start, int end) {
428+
if (!arena || !result || !defs || start >= end) {
429+
return;
430+
}
431+
for (int si = start; si < end; si++) {
432+
CBMLSPDef *dst = &defs[si];
433+
if (!dst->label || strcmp(dst->label, "Struct") != 0 || !dst->qualified_name) {
434+
continue;
435+
}
436+
int count = 0;
437+
size_t total = 0; /* "name:type" bytes; separators and NUL added below */
438+
for (int di = 0; di < result->defs.count; di++) {
439+
const CBMDefinition *fd = &result->defs.items[di];
440+
if (!fd->label || !fd->parent_class || !fd->name || !fd->name[0] || !fd->return_type ||
441+
!fd->return_type[0] || strcmp(fd->label, "Field") != 0 ||
442+
strcmp(fd->parent_class, dst->qualified_name) != 0) {
443+
continue;
444+
}
445+
total += strlen(fd->name) + 1 + strlen(fd->return_type);
446+
count++;
447+
}
448+
if (count == 0) {
449+
continue;
450+
}
451+
/* count - 1 separators + NUL. */
452+
size_t bufsz = total + (size_t)(count - 1) + 1;
453+
char *buf = (char *)cbm_arena_alloc(arena, bufsz);
454+
if (!buf) {
455+
continue;
456+
}
457+
char *p = buf;
458+
int written = 0;
459+
for (int di = 0; di < result->defs.count; di++) {
460+
const CBMDefinition *fd = &result->defs.items[di];
461+
if (!fd->label || !fd->parent_class || !fd->name || !fd->name[0] || !fd->return_type ||
462+
!fd->return_type[0] || strcmp(fd->label, "Field") != 0 ||
463+
strcmp(fd->parent_class, dst->qualified_name) != 0) {
464+
continue;
465+
}
466+
size_t n = strlen(fd->name);
467+
memcpy(p, fd->name, n);
468+
p += n;
469+
*p++ = ':';
470+
n = strlen(fd->return_type);
471+
memcpy(p, fd->return_type, n);
472+
p += n;
473+
if (written + 1 < count) {
474+
*p++ = '|';
475+
}
476+
written++;
477+
}
478+
*p = '\0';
479+
dst->field_defs = buf;
480+
}
481+
}
482+
416483
/* Carry one Rust type-level impl independently of any method definition.
417484
* `impl Trait for Type {}` is semantically meaningful even when the block is
418485
* empty (the trait may provide defaults), so attaching the relation only to
@@ -472,6 +539,7 @@ CBMLSPDef *cbm_pxc_collect_all_defs(const cbm_pipeline_ctx_t *ctx, CBMFileResult
472539
if (out_def_starts) {
473540
out_def_starts[fi] = idx;
474541
}
542+
const int file_start = idx;
475543
if (!cache[fi])
476544
continue;
477545
if (!def_modules[fi]) {
@@ -510,6 +578,9 @@ CBMLSPDef *cbm_pxc_collect_all_defs(const cbm_pipeline_ctx_t *ctx, CBMFileResult
510578
}
511579
}
512580
cbm_pxc_free_import_map(imp_keys, imp_vals, imp_count); /* NULL-safe */
581+
if (files[fi].language == CBM_LANG_GO) {
582+
pxc_fold_go_struct_fields(&cache[fi]->arena, cache[fi], defs, file_start, idx);
583+
}
513584
if (files[fi].language == CBM_LANG_RUST) {
514585
for (int ii = 0; ii < cache[fi]->impl_traits.count; ii++) {
515586
if (pxc_build_rust_impl_relation(
@@ -1218,8 +1289,16 @@ void cbm_pxc_dispatch_file(CBMLanguage lang, CBMFileResult *result, const char *
12181289
switch (lang) {
12191290
case CBM_LANG_GO:
12201291
/* Tier 3 (metadata-driven): pure lookup over the Tier-1
1221-
* lsp_unresolved entries — no parse, no AST walk. */
1292+
* lsp_unresolved entries — no parse, no AST walk. Then the
1293+
* AST walk on the shared Tier-2 registry (mirroring every
1294+
* other language) so NAMED receivers evaluated against
1295+
* project-wide defs also resolve. The walk variant below is
1296+
* read-only — the sealed registry is safe for parallel
1297+
* workers. */
12221298
cbm_go_fast_resolve_qualified_calls(result, prebuilt, imp_keys, imp_vals, imp_count);
1299+
cbm_run_go_lsp_cross_with_registry(&result->arena, source, source_len, def_module,
1300+
prebuilt, imp_keys, imp_vals, imp_count,
1301+
result->cached_tree, &result->resolved_calls);
12231302
used_prebuilt = true;
12241303
break;
12251304
case CBM_LANG_PYTHON: {

tests/test_go_lsp.c

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,6 +1285,99 @@ TEST(golsp_crossfile_stdlib_interface) {
12851285
PASS();
12861286
}
12871287

1288+
/* Cross-package receiver-method resolution when struct field type texts are
1289+
* written through import aliases. parse_field_defs_into_type qualifies the
1290+
* text with the defining module ("Svc:svc.Svc" in module test.main becomes
1291+
* "test.main.svc.Svc") — a QN that exists nowhere in the project-wide
1292+
* registry, so the dispatch must re-qualify the alias segment through the
1293+
* calling file's import map and land on the real receiver type. Mirrors a
1294+
* common cross-module shape: a service struct field typed from an sdk module
1295+
* and an interface-typed client field from an api module. */
1296+
TEST(golsp_crossfile_aliased_field_requal) {
1297+
const char *source = "package main\n\n"
1298+
"func callSvc(h *Handler) error {\n"
1299+
"\th.Svc.Ping()\n\treturn nil\n}\n\n"
1300+
"func callPb(h *Holder) error {\n"
1301+
"\th.C.Ping()\n\treturn nil\n}\n";
1302+
1303+
CBMLSPDef defs[] = {
1304+
/* myapp/svc — concrete service struct */
1305+
{.qualified_name = "myapp/svc.Svc",
1306+
.short_name = "Svc",
1307+
.label = "Struct",
1308+
.def_module_qn = "myapp/svc"},
1309+
/* test.main — receiver structs; field texts use import aliases, the
1310+
* trigger for the wrong qualification */
1311+
{.qualified_name = "test.main.Handler",
1312+
.short_name = "Handler",
1313+
.label = "Struct",
1314+
.def_module_qn = "test.main",
1315+
.field_defs = "Svc:svc.Svc"},
1316+
{.qualified_name = "test.main.Holder",
1317+
.short_name = "Holder",
1318+
.label = "Struct",
1319+
.def_module_qn = "test.main",
1320+
.field_defs = "C:pb.Client"},
1321+
/* myapp/pb — client interface */
1322+
{.qualified_name = "myapp/pb.Client",
1323+
.short_name = "Client",
1324+
.label = "Interface",
1325+
.def_module_qn = "myapp/pb",
1326+
.is_interface = true,
1327+
.method_names_str = "Ping"},
1328+
/* methods, after their receiver types (extraction order) */
1329+
{.qualified_name = "myapp/svc.Svc.Ping",
1330+
.short_name = "Ping",
1331+
.label = "Method",
1332+
.def_module_qn = "myapp/svc",
1333+
.receiver_type = "myapp/svc.Svc",
1334+
.return_types = "error"},
1335+
{.qualified_name = "myapp/pb.Client.Ping",
1336+
.short_name = "Ping",
1337+
.label = "Method",
1338+
.def_module_qn = "myapp/pb",
1339+
.receiver_type = "myapp/pb.Client",
1340+
.return_types = "error"},
1341+
};
1342+
const char *imp_names[] = {"svc", "pb"};
1343+
const char *imp_qns[] = {"myapp/svc", "myapp/pb"};
1344+
1345+
CBMArena arena;
1346+
cbm_arena_init(&arena);
1347+
CBMResolvedCallArray out = {0};
1348+
1349+
CBMTypeRegistry *reg = cbm_go_build_cross_registry(&arena, defs, 6);
1350+
ASSERT_NOT_NULL(reg);
1351+
1352+
cbm_run_go_lsp_cross_with_registry(&arena, source, (int)strlen(source), "test.main", reg,
1353+
imp_names, imp_qns, 2, NULL, &out);
1354+
1355+
int svc_idx = find_resolved_arr_confident(&out, "callSvc", "Svc.Ping");
1356+
if (svc_idx < 0) {
1357+
printf(" cross-registry diagnostics (%d records):\n", out.count);
1358+
for (int i = 0; i < out.count; i++) {
1359+
const CBMResolvedCall *rc = &out.items[i];
1360+
printf(" %s -> %s [%s %.2f]\n", rc->caller_qn ? rc->caller_qn : "(null)",
1361+
rc->callee_qn ? rc->callee_qn : "(null)",
1362+
rc->strategy ? rc->strategy : "(null)", rc->confidence);
1363+
}
1364+
}
1365+
ASSERT_GTE(svc_idx, 0);
1366+
ASSERT_STR_EQ(out.items[svc_idx].callee_qn, "myapp/svc.Svc.Ping");
1367+
ASSERT_STR_EQ(out.items[svc_idx].strategy, "lsp_type_dispatch");
1368+
ASSERT_TRUE(out.items[svc_idx].confidence >= 0.9f);
1369+
1370+
int pb_idx = find_resolved_arr_confident(&out, "callPb", "Client.Ping");
1371+
ASSERT_GTE(pb_idx, 0);
1372+
ASSERT_STR_EQ(out.items[pb_idx].callee_qn, "myapp/pb.Client.Ping");
1373+
ASSERT_TRUE(strcmp(out.items[pb_idx].strategy, "lsp_interface_dispatch") == 0 ||
1374+
strcmp(out.items[pb_idx].strategy, "lsp_type_dispatch") == 0);
1375+
ASSERT_TRUE(out.items[pb_idx].confidence >= 0.8f);
1376+
1377+
cbm_arena_destroy(&arena);
1378+
PASS();
1379+
}
1380+
12881381
TEST(golsp_crossfile_local_interface_single_impl) {
12891382
const char *source =
12901383
"package main\n\n"
@@ -1448,6 +1541,7 @@ SUITE(go_lsp) {
14481541
RUN_TEST(golsp_crossfile_return_type_chain);
14491542
RUN_TEST(golsp_crossfile_interface_dispatch);
14501543
RUN_TEST(golsp_crossfile_interface_field_chain);
1544+
RUN_TEST(golsp_crossfile_aliased_field_requal);
14511545
RUN_TEST(golsp_crossfile_map_index);
14521546
RUN_TEST(golsp_crossfile_stdlib_interface);
14531547
RUN_TEST(golsp_crossfile_local_interface_single_impl);

0 commit comments

Comments
 (0)