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
125 changes: 85 additions & 40 deletions internal/cbm/lsp/go_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
81 changes: 80 additions & 1 deletion src/pipeline/pass_lsp_cross.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]) {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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: {
Expand Down
94 changes: 94 additions & 0 deletions tests/test_go_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading