mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
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:
1 parent
ae5de7fe0e
commit
8f50841af4
4 files changed
+486
-41
No files matched your search
+85
-40
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user