fix(go): resolve cross-package field-chain calls via type dispatch

Go calls written through an aliased struct field (h.svc.Method(ctx,
req)) previously fell through to fuzzy suffix matching because:

- the per-file Go dispatch walk was bypassed, so field selectors were
  never descended;
- struct fields extracted as flat "Field" defs were dropped by the LSP
  label map, so every Go struct registered with zero fields;
- receiver types written through import aliases (svc.Svc) produced
  qualified names that exist nowhere in the project registry.

Changes:

- restore the Go walk in cbm_pxc_dispatch_file and descend field
  selectors through the shared prebuilt registry, emitting
  lsp_type_dispatch edges;
- fold per-field defs into their owning struct inside
  cbm_pxc_collect_all_defs (one site covers both the prebuilt-registry
  path and the per-file fallback);
- re-qualify the last alias segment through the calling file's import
  map when the direct receiver-type lookup misses
  (go_requalify_via_imports).

Tests:

- tests/test_parallel.c: cross-package field chain resolves end to end
  with an lsp_type_dispatch edge in the parallel resolver;
- tests/test_go_lsp.c: alias-typed struct field re-qualifies through
  the import map and lands on the real receiver type.

Signed-off-by: xkchok <143325001+xkchok@users.noreply.github.com>
This commit is contained in:
xkchok committed 2026-09-01 15:51:50 +08:00
1 parent ae5de7fe0e
commit 8f50841af4
4 files changed
+486 -41

No files matched your search

+85 -40
View File
@@ -866,11 +866,53 @@ const CBMType* go_eval_builtin_call(GoLSPContext* ctx, const char* name, TSNode
// --- go_lookup_field: struct field lookup with embedding recursion ---
// --- Import-alias re-qualification ------------------------------------
//
// parse_field_defs_into_type qualifies struct field type texts as
// "<def_module>.<text>". When the author wrote the text through an import
// alias ("Svc:svc.Svc" in module test.main), that yields a QN
// ("test.main.svc.Svc") that exists nowhere in the project-wide registry —
// the real QN is "<import_qn>.Svc" and only the calling file's import map
// can say so. On an exact-QN miss this rewrites the alias segment through
// the file's imports; returns NULL when no alias segment is involved.
static const char *go_requalify_via_imports(GoLSPContext *ctx, const char *type_qn) {
if (!ctx || !type_qn || !type_qn[0] || ctx->import_count <= 0) return NULL;
for (int j = 0; j < ctx->import_count; j++) {
const char *alias = ctx->import_local_names[j];
const char *alias_qn = ctx->import_package_qns[j];
if (!alias || !alias[0] || !alias_qn || strchr(alias, '.')) continue;
size_t alias_len = strlen(alias);
/* Last occurrence of a "<dot>alias<dot>" segment in type_qn. */
const char *hit = NULL;
for (const char *p = type_qn;;) {
p = strstr(p, ".");
if (!p) break;
p++;
if (strncmp(p, alias, alias_len) == 0 && p[alias_len] == '.') {
hit = p;
p += alias_len;
}
}
if (hit) {
const char *rest = hit + alias_len + 1; /* past "<alias>." */
return cbm_arena_sprintf(ctx->arena, "%s.%s", alias_qn, rest);
}
}
return NULL;
}
static const CBMType* go_lookup_field(GoLSPContext* ctx,
const char* type_qn, const char* field_name, int depth) {
if (!type_qn || !field_name || depth > 5) return NULL;
const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry, type_qn);
if (!rt && depth == 0) {
/* Import-alias re-qualification: field texts from cross-package defs
* may embed an alias segment only this file's import map resolves. */
const char* alt_qn = go_requalify_via_imports(ctx, type_qn);
if (alt_qn) rt = cbm_registry_lookup_type(ctx->registry, alt_qn);
}
if (!rt) return NULL;
// Follow alias chain
@@ -907,6 +949,18 @@ static const CBMRegisteredFunc* go_lookup_field_or_method_depth(GoLSPContext* ct
const CBMRegisteredFunc* f = cbm_registry_lookup_method(ctx->registry, type_qn, member_name);
if (f) return f;
/* Import-alias re-qualification fallback: NAMED receivers built from
* cross-package field type texts can carry a "<module>.svc.Svc" QN;
* retry the method set on the import-resolved QN (see
* go_requalify_via_imports). */
if (depth == 0) {
const char* alt_qn = go_requalify_via_imports(ctx, type_qn);
if (alt_qn) {
f = go_lookup_field_or_method_depth(ctx, alt_qn, member_name, depth + 1);
if (f) return f;
}
}
const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry, type_qn);
if (rt) {
// Follow type alias chain
@@ -1430,20 +1484,32 @@ static void resolve_calls_in_node_inner(GoLSPContext* ctx, TSNode node) {
if (base && base->kind == CBM_TYPE_POINTER) base = cbm_type_deref(base);
if (base && base->kind == CBM_TYPE_NAMED) {
const char *recv_qn = base->data.named.qualified_name;
const CBMRegisteredType *receiver_type = cbm_registry_lookup_type(
ctx->registry, base->data.named.qualified_name);
ctx->registry, recv_qn);
const char *alt_qn = NULL;
/* Re-qualify NAMED receivers that embed an import
* alias segment (cross-package field type texts) —
* the real type only exists under the import QN. */
if (!receiver_type) {
alt_qn = go_requalify_via_imports(ctx, recv_qn);
if (alt_qn) {
receiver_type = cbm_registry_lookup_type(ctx->registry, alt_qn);
if (receiver_type) recv_qn = alt_qn;
}
}
/* Registered interface receivers must reach the
* interface-resolution branch below. Their semantic
* method registrations are signatures, not concrete
* dispatch targets. */
if (!receiver_type || !receiver_type->is_interface) {
const CBMRegisteredFunc *method = go_lookup_field_or_method(
ctx, base->data.named.qualified_name, field_name);
ctx, recv_qn, field_name);
if (method) {
const char *strategy = "lsp_type_dispatch";
if (method->receiver_type &&
strcmp(method->receiver_type,
base->data.named.qualified_name) != 0) {
recv_qn) != 0) {
strategy = "lsp_embed_dispatch";
}
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) {
if (!is_iface && base->kind == CBM_TYPE_NAMED) {
const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry,
base->data.named.qualified_name);
if (!rt) {
const char* alt_qn = go_requalify_via_imports(
ctx, base->data.named.qualified_name);
if (alt_qn)
rt = cbm_registry_lookup_type(ctx->registry, alt_qn);
}
if (rt && rt->is_interface) {
is_iface = true;
iface_qn = base->data.named.qualified_name;
iface_qn = rt->qualified_name;
}
}
if (is_iface) {
@@ -1787,42 +1859,15 @@ static void process_function(GoLSPContext* ctx, TSNode func_node) {
char* func_name = lsp_node_text(ctx, name_node);
if (!func_name || !func_name[0]) return;
// For methods, the enclosing-function QN must include the receiver type
// (package.Type.Method), matching how the textual extractor and the
// registry qualify the method. Building it as package.Method (no receiver)
// here made the LSP-resolved call's caller_qn disagree with the textual
// call's enclosing_func_qn, so cbm_pipeline_find_lsp_resolution never
// joined them — every call inside a method body silently lost its
// type-aware LSP strategy. Derive the bare receiver type name the same way
// the receiver binding below does.
char* recv_type_name = NULL;
{
TSNode recv0 = ts_node_child_by_field_name(func_node, "receiver", 8);
if (!ts_node_is_null(recv0)) {
uint32_t rnc0 = ts_node_child_count(recv0);
for (uint32_t i = 0; i < rnc0 && !recv_type_name; i++) {
TSNode rp = ts_node_child(recv0, i);
if (ts_node_is_null(rp) || !ts_node_is_named(rp)) continue;
if (strcmp(ts_node_type(rp), "parameter_declaration") != 0) continue;
TSNode rtype = ts_node_child_by_field_name(rp, "type", 4);
if (ts_node_is_null(rtype)) continue;
// Unwrap a pointer receiver (*Type) to the bare type identifier.
const char* rtk = ts_node_type(rtype);
if (strcmp(rtk, "pointer_type") == 0 && ts_node_named_child_count(rtype) > 0) {
rtype = ts_node_named_child(rtype, 0);
}
char* tn = lsp_node_text(ctx, rtype);
if (tn && tn[0]) recv_type_name = tn;
}
}
}
if (recv_type_name) {
ctx->enclosing_func_qn =
cbm_arena_sprintf(ctx->arena, "%s.%s.%s", ctx->package_qn, recv_type_name, func_name);
} else {
ctx->enclosing_func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->package_qn, func_name);
}
// Enclosing-function QN must be the BARE package.Func form (no receiver
// type segment). The textual call events (extract_unified.c) source calls
// as package_qn.func_name — methods included — and the defs pass creates
// the graph Method node under the same QN, so any other form breaks the
// caller-QN join in cbm_pipeline_find_lsp_resolution and the LSP-resolved
// call silently falls back to the registry short-name resolver. The
// receiver type still reaches the registry via the def's parent_class /
// method->receiver_type; it just does not appear in the caller QN.
ctx->enclosing_func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->package_qn, func_name);
// Push function scope
CBMScope* saved_scope = ctx->current_scope;
+80 -1
View File
@@ -413,6 +413,73 @@ static int pxc_build_lsp_def(CBMArena *arena, const CBMDefinition *src, const ch
return 0;
}
/* Go: fold per-field "Field" definitions into their owning struct's
* field_defs. extract_defs.c emits one flat CBMDefinition per struct field
* (label "Field", parent_class = owning struct QN, name = field name,
* return_type = raw type text). Those rows are dropped by pxc_build_lsp_def
* (pxc_map_label excludes "Field"), so without this fold every Go struct
* registers with zero fields and field-chain calls (h.svc.Handle) can
* never resolve. Fields are always declared in the same file as their struct,
* so scanning the file's own defs covers every case. Runs inside
* cbm_pxc_collect_all_defs — one site covers both the prebuilt-registry path
* and the per-file fallback, since both consume all_defs. */
static void pxc_fold_go_struct_fields(CBMArena *arena, const CBMFileResult *result,
CBMLSPDef *defs, int start, int end) {
if (!arena || !result || !defs || start >= end) {
return;
}
for (int si = start; si < end; si++) {
CBMLSPDef *dst = &defs[si];
if (!dst->label || strcmp(dst->label, "Struct") != 0 || !dst->qualified_name) {
continue;
}
int count = 0;
size_t total = 0; /* "name:type" bytes; separators and NUL added below */
for (int di = 0; di < result->defs.count; di++) {
const CBMDefinition *fd = &result->defs.items[di];
if (!fd->label || !fd->parent_class || !fd->name || !fd->name[0] || !fd->return_type ||
!fd->return_type[0] || strcmp(fd->label, "Field") != 0 ||
strcmp(fd->parent_class, dst->qualified_name) != 0) {
continue;
}
total += strlen(fd->name) + 1 + strlen(fd->return_type);
count++;
}
if (count == 0) {
continue;
}
/* count - 1 separators + NUL. */
size_t bufsz = total + (size_t)(count - 1) + 1;
char *buf = (char *)cbm_arena_alloc(arena, bufsz);
if (!buf) {
continue;
}
char *p = buf;
int written = 0;
for (int di = 0; di < result->defs.count; di++) {
const CBMDefinition *fd = &result->defs.items[di];
if (!fd->label || !fd->parent_class || !fd->name || !fd->name[0] || !fd->return_type ||
!fd->return_type[0] || strcmp(fd->label, "Field") != 0 ||
strcmp(fd->parent_class, dst->qualified_name) != 0) {
continue;
}
size_t n = strlen(fd->name);
memcpy(p, fd->name, n);
p += n;
*p++ = ':';
n = strlen(fd->return_type);
memcpy(p, fd->return_type, n);
p += n;
if (written + 1 < count) {
*p++ = '|';
}
written++;
}
*p = '\0';
dst->field_defs = buf;
}
}
/* Carry one Rust type-level impl independently of any method definition.
* `impl Trait for Type {}` is semantically meaningful even when the block is
* 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
if (out_def_starts) {
out_def_starts[fi] = idx;
}
const int file_start = idx;
if (!cache[fi])
continue;
if (!def_modules[fi]) {
@@ -510,6 +578,9 @@ CBMLSPDef *cbm_pxc_collect_all_defs(const cbm_pipeline_ctx_t *ctx, CBMFileResult
}
}
cbm_pxc_free_import_map(imp_keys, imp_vals, imp_count); /* NULL-safe */
if (files[fi].language == CBM_LANG_GO) {
pxc_fold_go_struct_fields(&cache[fi]->arena, cache[fi], defs, file_start, idx);
}
if (files[fi].language == CBM_LANG_RUST) {
for (int ii = 0; ii < cache[fi]->impl_traits.count; ii++) {
if (pxc_build_rust_impl_relation(
@@ -1218,8 +1289,16 @@ void cbm_pxc_dispatch_file(CBMLanguage lang, CBMFileResult *result, const char *
switch (lang) {
case CBM_LANG_GO:
/* Tier 3 (metadata-driven): pure lookup over the Tier-1
* lsp_unresolved entries — no parse, no AST walk. */
* lsp_unresolved entries — no parse, no AST walk. Then the
* AST walk on the shared Tier-2 registry (mirroring every
* other language) so NAMED receivers evaluated against
* project-wide defs also resolve. The walk variant below is
* read-only — the sealed registry is safe for parallel
* workers. */
cbm_go_fast_resolve_qualified_calls(result, prebuilt, imp_keys, imp_vals, imp_count);
cbm_run_go_lsp_cross_with_registry(&result->arena, source, source_len, def_module,
prebuilt, imp_keys, imp_vals, imp_count,
result->cached_tree, &result->resolved_calls);
used_prebuilt = true;
break;
case CBM_LANG_PYTHON: {
+94
View File
@@ -1285,6 +1285,99 @@ TEST(golsp_crossfile_stdlib_interface) {
PASS();
}
/* Cross-package receiver-method resolution when struct field type texts are
* written through import aliases. parse_field_defs_into_type qualifies the
* text with the defining module ("Svc:svc.Svc" in module test.main becomes
* "test.main.svc.Svc") — a QN that exists nowhere in the project-wide
* registry, so the dispatch must re-qualify the alias segment through the
* calling file's import map and land on the real receiver type. Mirrors a
* common cross-module shape: a service struct field typed from an sdk module
* and an interface-typed client field from an api module. */
TEST(golsp_crossfile_aliased_field_requal) {
const char *source = "package main\n\n"
"func callSvc(h *Handler) error {\n"
"\th.Svc.Ping()\n\treturn nil\n}\n\n"
"func callPb(h *Holder) error {\n"
"\th.C.Ping()\n\treturn nil\n}\n";
CBMLSPDef defs[] = {
/* myapp/svc — concrete service struct */
{.qualified_name = "myapp/svc.Svc",
.short_name = "Svc",
.label = "Struct",
.def_module_qn = "myapp/svc"},
/* test.main — receiver structs; field texts use import aliases, the
* trigger for the wrong qualification */
{.qualified_name = "test.main.Handler",
.short_name = "Handler",
.label = "Struct",
.def_module_qn = "test.main",
.field_defs = "Svc:svc.Svc"},
{.qualified_name = "test.main.Holder",
.short_name = "Holder",
.label = "Struct",
.def_module_qn = "test.main",
.field_defs = "C:pb.Client"},
/* myapp/pb — client interface */
{.qualified_name = "myapp/pb.Client",
.short_name = "Client",
.label = "Interface",
.def_module_qn = "myapp/pb",
.is_interface = true,
.method_names_str = "Ping"},
/* methods, after their receiver types (extraction order) */
{.qualified_name = "myapp/svc.Svc.Ping",
.short_name = "Ping",
.label = "Method",
.def_module_qn = "myapp/svc",
.receiver_type = "myapp/svc.Svc",
.return_types = "error"},
{.qualified_name = "myapp/pb.Client.Ping",
.short_name = "Ping",
.label = "Method",
.def_module_qn = "myapp/pb",
.receiver_type = "myapp/pb.Client",
.return_types = "error"},
};
const char *imp_names[] = {"svc", "pb"};
const char *imp_qns[] = {"myapp/svc", "myapp/pb"};
CBMArena arena;
cbm_arena_init(&arena);
CBMResolvedCallArray out = {0};
CBMTypeRegistry *reg = cbm_go_build_cross_registry(&arena, defs, 6);
ASSERT_NOT_NULL(reg);
cbm_run_go_lsp_cross_with_registry(&arena, source, (int)strlen(source), "test.main", reg,
imp_names, imp_qns, 2, NULL, &out);
int svc_idx = find_resolved_arr_confident(&out, "callSvc", "Svc.Ping");
if (svc_idx < 0) {
printf(" cross-registry diagnostics (%d records):\n", out.count);
for (int i = 0; i < out.count; i++) {
const CBMResolvedCall *rc = &out.items[i];
printf(" %s -> %s [%s %.2f]\n", rc->caller_qn ? rc->caller_qn : "(null)",
rc->callee_qn ? rc->callee_qn : "(null)",
rc->strategy ? rc->strategy : "(null)", rc->confidence);
}
}
ASSERT_GTE(svc_idx, 0);
ASSERT_STR_EQ(out.items[svc_idx].callee_qn, "myapp/svc.Svc.Ping");
ASSERT_STR_EQ(out.items[svc_idx].strategy, "lsp_type_dispatch");
ASSERT_TRUE(out.items[svc_idx].confidence >= 0.9f);
int pb_idx = find_resolved_arr_confident(&out, "callPb", "Client.Ping");
ASSERT_GTE(pb_idx, 0);
ASSERT_STR_EQ(out.items[pb_idx].callee_qn, "myapp/pb.Client.Ping");
ASSERT_TRUE(strcmp(out.items[pb_idx].strategy, "lsp_interface_dispatch") == 0 ||
strcmp(out.items[pb_idx].strategy, "lsp_type_dispatch") == 0);
ASSERT_TRUE(out.items[pb_idx].confidence >= 0.8f);
cbm_arena_destroy(&arena);
PASS();
}
TEST(golsp_crossfile_local_interface_single_impl) {
const char *source =
"package main\n\n"
@@ -1448,6 +1541,7 @@ SUITE(go_lsp) {
RUN_TEST(golsp_crossfile_return_type_chain);
RUN_TEST(golsp_crossfile_interface_dispatch);
RUN_TEST(golsp_crossfile_interface_field_chain);
RUN_TEST(golsp_crossfile_aliased_field_requal);
RUN_TEST(golsp_crossfile_map_index);
RUN_TEST(golsp_crossfile_stdlib_interface);
RUN_TEST(golsp_crossfile_local_interface_single_impl);
+227
View File
@@ -3203,6 +3203,232 @@ TEST(parallel_python_lsp_override_emits_lsp_strategy_edges) {
PASS();
}
/* ── Go cross-package field-chain fixture (field_defs fold) ─────── */
/* Production's sequential driver seeds Folder nodes via pass_structure; the
* compact harness (run_sequential_with_lsp_cross_*) does not. Go imports
* resolve to Folder nodes, so a Go import-map fixture must seed File and
* Folder nodes itself — replicating the production shape. */
static cbm_gbuf_t *run_go_field_chain_sequential(const char *project, const char *repo_path,
cbm_file_info_t *files, int file_count) {
cbm_gbuf_t *gbuf = cbm_gbuf_new(project, repo_path);
cbm_registry_t *reg = cbm_registry_new();
CBMFileResult **cache = (CBMFileResult **)calloc((size_t)file_count, sizeof(CBMFileResult *));
if (!gbuf || !reg || !cache) {
cbm_gbuf_free(gbuf);
cbm_registry_free(reg);
free(cache);
return NULL;
}
atomic_int cancelled;
atomic_init(&cancelled, 0);
cbm_pipeline_ctx_t ctx = {
.project_name = project,
.repo_path = repo_path,
.gbuf = gbuf,
.registry = reg,
.cancelled = &cancelled,
.result_cache = cache,
};
seed_test_file_nodes(gbuf, project, files, file_count);
char *svc_dir_qn = cbm_pipeline_fqn_folder(project, "svc");
if (svc_dir_qn) {
cbm_gbuf_upsert_node(gbuf, "Folder", "svc", svc_dir_qn, "svc", 0, 0, "{}");
free(svc_dir_qn);
}
cbm_init();
cbm_pipeline_pass_definitions(&ctx, files, file_count);
cbm_pipeline_pass_lsp_cross(&ctx, files, file_count, cache);
cbm_pipeline_pass_calls(&ctx, files, file_count);
cbm_pipeline_pass_usages(&ctx, files, file_count);
cbm_pipeline_pass_semantic(&ctx, files, file_count);
/* CBM_GO_FIELD_DIAG dump. NOTE: resolved-call records may borrow QN
* strings across file results (cross-file resolution in pass_lsp_cross),
* so every result must stay alive while ANY is inspected. Print all
* first, free all second. */
if (getenv("CBM_GO_FIELD_DIAG")) {
for (int i = 0; i < file_count; i++) {
if (!cache[i]) {
continue;
}
const CBMFileResult *r = cache[i];
printf(" [diag] file %s defs=%d imports=%d calls=%d resolved=%d\n", files[i].rel_path,
r->defs.count, r->imports.count, r->calls.count, r->resolved_calls.count);
for (int j = 0; j < r->resolved_calls.count; j++) {
const CBMResolvedCall *rc = &r->resolved_calls.items[j];
printf(" [diag] rc caller=%s callee=%s strategy=%s conf=%.2f kind=%d span=[%u,%u)\n",
rc->caller_qn ? rc->caller_qn : "?", rc->callee_qn ? rc->callee_qn : "?",
rc->strategy ? rc->strategy : "?", rc->confidence, (int)rc->kind,
(unsigned)rc->site_start_byte, (unsigned)rc->site_end_byte);
}
for (int j = 0; j < r->calls.count; j++) {
const CBMCall *c = &r->calls.items[j];
printf(" [diag] call callee=%s enclosing=%s span=[%u,%u) req=%d\n",
c->callee_name ? c->callee_name : "?", c->enclosing_func_qn ? c->enclosing_func_qn : "?",
(unsigned)c->site_start_byte, (unsigned)c->site_end_byte, (int)c->requires_lsp_resolution);
}
for (int j = 0; j < r->defs.count; j++) {
const CBMDefinition *d = &r->defs.items[j];
printf(" [diag] def label=%s qn=%s parent=%s ret=%s\n", d->label ? d->label : "?",
d->qualified_name ? d->qualified_name : "?", d->parent_class ? d->parent_class : "?",
d->return_type ? d->return_type : "?");
}
}
}
for (int i = 0; i < file_count; i++) {
cbm_free_result(cache[i]);
}
free(cache);
cbm_registry_free(reg);
if (ctx.seq_cross_arena_live) {
cbm_arena_destroy(&ctx.seq_cross_arena);
ctx.seq_cross_arena_live = false;
}
if (ctx.seq_cross_def_modules) {
for (int i = 0; i < ctx.seq_cross_def_module_count; i++) {
free(ctx.seq_cross_def_modules[i]);
}
free(ctx.seq_cross_def_modules);
ctx.seq_cross_def_modules = NULL;
}
return gbuf;
}
/* Cross-package field chains (h.S.Ping()) resolve end to end through the
* shared prebuilt Go registry. Regression for the field_defs fold: Go struct
* fields are extracted as flat "Field" definitions that pxc_map_label drops,
* so structs registered with zero fields and field chains never resolved.
* Mirrors the real-world handler shape — an app package holds an aliased
* cross-package field ("S *s.Svc") and app.Call calls through it. */
TEST(parallel_go_cross_package_field_chain_resolves) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_gofold_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
char svc_path[512];
char app_path[512];
snprintf(svc_path, sizeof(svc_path), "%s/acme-order/internal/service/order_service.go", tmpdir);
snprintf(app_path, sizeof(app_path), "%s/acme-order/internal/handler/order_handler.go", tmpdir);
if (th_write_file(svc_path, "package service\n"
"\n"
"import (\n"
" \"context\"\n"
" pb \"acme-sdk/apis/order\"\n"
")\n"
"\n"
"type OrderService struct{}\n"
"\n"
"func (s *OrderService) PlaceOrder(ctx context.Context, req *pb.PlaceOrderReq) (*pb.PlaceOrderRsp, error) {\n"
" return nil, nil\n"
"}\n"
"\n"
"func (s *OrderService) ListOrders(ctx context.Context, req *pb.ListOrdersReq) (*pb.ListOrdersRsp, error) {\n"
" return nil, nil\n"
"}\n") != 0 ||
th_write_file(app_path, "package handler\n"
"\n"
"import (\n"
" \"context\"\n"
"\n"
" pb \"acme-sdk/apis/order\"\n"
"\n"
" \"acme-order/internal/service\"\n"
")\n"
"\n"
"type OrderHandler struct {\n"
" pb.UnimplementedOrderServer\n"
" cartSvc *service.CartService\n"
" userSvc *service.UserService\n"
" orderSvc *service.OrderService\n"
" inventorySvc *service.InventoryService\n"
" paymentSvc *service.PaymentService\n"
" shipmentSvc *service.ShipmentService\n"
" couponSvc *service.CouponService\n"
" searchSvc *service.SearchService\n"
"}\n"
"\n"
"func (h *OrderHandler) ListOrders(ctx context.Context, req *pb.ListOrdersReq) (*pb.ListOrdersRsp, error) {\n"
" return h.orderSvc.ListOrders(ctx, req)\n"
"}\n"
"\n"
"func (h *OrderHandler) PlaceOrder(ctx context.Context, req *pb.PlaceOrderReq) (*pb.PlaceOrderRsp, error) {\n"
" return h.orderSvc.PlaceOrder(ctx, req)\n"
"}\n"
"\n"
"func (h *OrderHandler) UpdateCart(ctx context.Context, req *pb.UpdateCartReq) (*pb.UpdateCartRsp, error) {\n"
" return h.cartSvc.UpdateCart(ctx, req)\n"
"}\n") != 0) {
th_rmtree(tmpdir);
FAIL("write fixture failed");
}
cbm_file_info_t files[2] = {0};
files[0].path = svc_path;
files[0].rel_path = (char *)"acme-order/internal/service/order_service.go";
files[0].language = CBM_LANG_GO;
files[1].path = app_path;
files[1].rel_path = (char *)"acme-order/internal/handler/order_handler.go";
files[1].language = CBM_LANG_GO;
cbm_gbuf_t *gbuf = run_go_field_chain_sequential("go_field_fold", tmpdir, files, 2);
ASSERT_NOT_NULL(gbuf);
const cbm_gbuf_edge_t *edge = find_call_edge_to_target_fragment(gbuf, "handler.PlaceOrder", ".service.PlaceOrder");
const bool found = edge != NULL;
const bool dispatch =
edge && edge->properties_json &&
strstr(edge->properties_json, "\"strategy\":\"lsp_type_dispatch\"");
if (!found || !dispatch) {
printf(" go field chain diagnostic: found=%d dispatch=%d\n", found, dispatch);
if (edge && edge->properties_json) {
printf(" go field chain props: %s\n", edge->properties_json);
}
/* Dump all callable nodes and CALLS edges for cross-referencing */
{
const cbm_gbuf_node_t **nodes = NULL;
int ncount = 0;
if (cbm_gbuf_find_by_label(gbuf, "Function", &nodes, &ncount) == 0) {
for (int i = 0; i < ncount; i++) {
printf(" node Function: %s\n", nodes[i]->qualified_name);
}
}
if (cbm_gbuf_find_by_label(gbuf, "Method", &nodes, &ncount) == 0) {
for (int i = 0; i < ncount; i++) {
printf(" node Method: %s\n", nodes[i]->qualified_name);
}
}
cbm_gbuf_edge_visitor_fn edge_dump = NULL;
(void)edge_dump;
/* print every CALLS edge with endpoints */
const cbm_gbuf_edge_t **all = NULL;
int ecount = 0;
if (cbm_gbuf_find_edges_by_type(gbuf, "CALLS", &all, &ecount) == 0) {
for (int i = 0; i < ecount; i++) {
const cbm_gbuf_node_t *src =
all[i] ? cbm_gbuf_find_by_id(gbuf, all[i]->source_id) : NULL;
const cbm_gbuf_node_t *dst =
all[i] ? cbm_gbuf_find_by_id(gbuf, all[i]->target_id) : NULL;
printf(" CALLS edge: %s -> %s props=%s\n",
src ? src->qualified_name : "?",
dst ? dst->qualified_name : "?",
all[i]->properties_json ? all[i]->properties_json : "{}");
}
}
}
}
cbm_gbuf_free(gbuf);
th_rmtree(tmpdir);
ASSERT_TRUE(found);
ASSERT_TRUE(dispatch);
PASS();
}
/* Cross-file regression for the QN-mismatch bug: py_lsp's per-file mode
* emits resolved_calls.callee_qn as the raw import-module path (e.g.
* `greeter.Greeter` from `from greeter import Greeter`) rather than the
@@ -4108,6 +4334,7 @@ SUITE(parallel) {
RUN_TEST(parallel_cpp_preprocessed_coordinate_collision_preserves_hidden_target);
RUN_TEST(parallel_cuda_preprocessed_coordinate_collision_preserves_hidden_target);
RUN_TEST(parallel_python_lsp_override_cross_file_emits_lsp_strategy_edges);
RUN_TEST(parallel_go_cross_package_field_chain_resolves);
RUN_TEST(parallel_cross_file_reread_preserves_unretained_edges);
RUN_TEST(parallel_java_kotlin_lsp_override_cross_file_emits_lsp_strategy_edges);
RUN_TEST(parallel_lsp_tail_match_fallbacks_gated_to_jvm);