diff --git a/internal/cbm/cbm.c b/internal/cbm/cbm.c index 23b871dbc..59c2c20c3 100644 --- a/internal/cbm/cbm.c +++ b/internal/cbm/cbm.c @@ -3919,7 +3919,7 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C uint64_t lsp_start = now_ns(); if (!result->lsp_skipped) { if (language == CBM_LANG_GO) { - cbm_run_go_lsp(a, result, source, source_len, root); + cbm_run_go_lsp(a, result, source, source_len, root, rel_path); } if (language == CBM_LANG_C || language == CBM_LANG_CPP || language == CBM_LANG_CUDA) { if (!cbm_run_c_lsp_with_test_owners(a, result, source, source_len, root, diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 8cf268b11..ef86c332d 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -7069,14 +7069,13 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec * above the function — the QN is node identity, and a line suffix would * churn nodes on every unrelated insertion. Nothing ever joins on the * plain QN — calling init explicitly is illegal in Go — and the - * call-scope side (compute_func_qn in extract_unified.c) computes the - * same cbm_go_init_ordinal so init-body calls keep their attribution. */ + * call-scope side (compute_func_qn in extract_unified.c) and the Go LSP + * (process_function in lsp/go_lsp.c) name it through the same + * cbm_go_init_qn, so init-body calls keep their attribution. */ if (ctx->language == CBM_LANG_GO && strcmp(def.label, "Function") == 0 && strcmp(name, "init") == 0) { - const char *base = strrchr(ctx->rel_path, '/'); - base = base ? base + 1 : ctx->rel_path; - def.qualified_name = cbm_arena_sprintf(a, "%s#%s:%d", def.qualified_name, base, - cbm_go_init_ordinal(node, ctx->source)); + def.qualified_name = + cbm_go_init_qn(a, def.qualified_name, ctx->rel_path, node, ctx->source); } // C++/CUDA: out-of-line method definition (`Foo::bar` in a .cc/.cpp). The diff --git a/internal/cbm/extract_unified.c b/internal/cbm/extract_unified.c index 39b20f460..e8edbd448 100644 --- a/internal/cbm/extract_unified.c +++ b/internal/cbm/extract_unified.c @@ -1136,17 +1136,18 @@ static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLan /* Go init(): the def extractor folds the file basename and the init's * per-file ordinal into the QN so every init in a package survives the * upsert (#1910, the #495 cfg-twin pattern; ordinal, not line, so the QN - * is stable under edits above the function). Mirror the exact formula - * here, or init-body calls fall back to File-node attribution + * is stable under edits above the function). Name it through the same + * cbm_go_init_qn, or init-body calls fall back to File-node attribution * (calls_find_source). */ if (ctx->language == CBM_LANG_GO && strcmp(name, "init") == 0 && strcmp(ts_node_type(node), "function_declaration") == 0) { const char *base_qn = cbm_fqn_compute_source_lang(ctx->arena, ctx->project, ctx->rel_path, name, ctx->language); - const char *base = strrchr(ctx->rel_path, '/'); - base = base ? base + 1 : ctx->rel_path; - return cbm_arena_sprintf(ctx->arena, "%s#%s:%d", base_qn, base, - cbm_go_init_ordinal(node, ctx->source)); + /* NULL only on allocation failure: fall through to the flat QN, as + * the receiver branch above does. */ + if (base_qn) { + return cbm_go_init_qn(ctx->arena, base_qn, ctx->rel_path, node, ctx->source); + } } /* Nix: a binding's own attrpath contributes scope (`a.b.fn = …`), and the def diff --git a/internal/cbm/helpers.c b/internal/cbm/helpers.c index ebb653e40..d6a8e73b9 100644 --- a/internal/cbm/helpers.c +++ b/internal/cbm/helpers.c @@ -2330,48 +2330,71 @@ int cbm_classify_string(const char *str, int len) { return NOT_FOUND; } +/* 1-based source-order ordinal of a top-level Go `init` function_declaration + * among the file's `init`s. Unlike a line number, the ordinal is stable under + * edits above the function — QNs are node identity, and a line-based suffix + * would churn nodes on every unrelated insertion (the #495 cfg suffix and the + * GoogleTest derivation are both edit-stable for the same reason). + * One cursor pass over the top-level declarations: ts_node_named_child(root, i) + * is O(i), so an indexed loop is O(n^2) per init on a wide file, and generated + * Go (.pb.go, tables) carries thousands of top-level declarations next to its + * init(). */ +static int go_init_ordinal(TSNode fn_node, const char *source) { + TSNode root = fn_node; + for (TSNode p = ts_node_parent(root); !ts_node_is_null(p); p = ts_node_parent(p)) { + root = p; + } + int ordinal = 0; + TSTreeCursor cur = ts_tree_cursor_new(root); + if (ts_tree_cursor_goto_first_child(&cur)) { + do { + TSNode child = ts_tree_cursor_current_node(&cur); + if (strcmp(ts_node_type(child), "function_declaration") != 0) { + continue; + } + TSNode name = ts_node_child_by_field_name(child, TS_FIELD("name")); + if (ts_node_is_null(name)) { + continue; + } + uint32_t start = ts_node_start_byte(name); + if (ts_node_end_byte(name) - start != 4 || memcmp(source + start, "init", 4) != 0) { + continue; + } + ordinal++; + if (ts_node_eq(child, fn_node)) { + break; + } + } while (ts_tree_cursor_goto_next_sibling(&cur)); + } + ts_tree_cursor_delete(&cur); + return ordinal; +} + +/* Qualified name of a Go `init` function (#1910). Go allows any number of + * init() per package — even several in one file — so the flat `pkg.init` + * collides and the graph upsert keeps one node. The QN folds in the file + * basename and the init's per-file ordinal: `pkg.init#file.go:N`. Every side + * that names an init calls this one function — the definition extractor, the + * call-scope side (compute_func_qn) and the Go LSP's enclosing-function QN — + * so the three cannot drift. A NULL base QN or path keeps base_qn as is. */ +const char *cbm_go_init_qn(CBMArena *a, const char *base_qn, const char *rel_path, TSNode fn_node, + const char *source) { + if (!base_qn || !rel_path) { + return base_qn; + } + const char *base = strrchr(rel_path, '/'); + base = base ? base + 1 : rel_path; + const char *qn = + cbm_arena_sprintf(a, "%s#%s:%d", base_qn, base, go_init_ordinal(fn_node, source)); + return qn ? qn : base_qn; +} + /* Flatten a JS/TS `template_string` node into plain text (issue #1006). * String fragments are kept verbatim; each ${...} substitution becomes the * "{}" placeholder so client URLs built from template literals share the * canonical parameter shape of server-side route paths * (`/things/${id}/x` -> "/things/{}/x"). Returns NULL when the node yields * no text or exceeds the route-sized buffer. */ -/* 1-based source-order ordinal of a top-level Go `init` function_declaration - * among the file's `init`s. Both the definition extractor and compute_func_qn - * fold it into the QN (`pkg.init#file.go:N`, #1910), so it must be computed - * from the tree on both sides with this one formula. Unlike a line number, - * the ordinal is stable under edits above the function — QNs are node - * identity, and a line-based suffix would churn nodes on every unrelated - * insertion (the #495 cfg suffix and the GoogleTest derivation are both - * edit-stable for the same reason). */ -int cbm_go_init_ordinal(TSNode fn_node, const char *source) { - TSNode root = fn_node; - for (TSNode p = ts_node_parent(root); !ts_node_is_null(p); p = ts_node_parent(p)) { - root = p; - } - int ordinal = 0; - uint32_t n = ts_node_named_child_count(root); - for (uint32_t i = 0; i < n; i++) { - TSNode child = ts_node_named_child(root, i); - if (strcmp(ts_node_type(child), "function_declaration") != 0) { - continue; - } - TSNode name = ts_node_child_by_field_name(child, TS_FIELD("name")); - if (ts_node_is_null(name)) { - continue; - } - uint32_t start = ts_node_start_byte(name); - if (ts_node_end_byte(name) - start != 4 || memcmp(source + start, "init", 4) != 0) { - continue; - } - ordinal++; - if (ts_node_eq(child, fn_node)) { - return ordinal; - } - } - return ordinal; -} - const char *cbm_template_string_text(CBMArena *a, TSNode node, const char *source) { enum { TPL_BUF = 512 }; char buf[TPL_BUF]; diff --git a/internal/cbm/helpers.h b/internal/cbm/helpers.h index 13dd1dc2b..4c96cb5f0 100644 --- a/internal/cbm/helpers.h +++ b/internal/cbm/helpers.h @@ -281,16 +281,19 @@ bool cbm_is_loop_node_type(const char *kind); // Is this a module-level node? (not nested inside function/class body) bool cbm_is_module_level(TSNode node, CBMLanguage lang); -/* 1-based source-order ordinal of a Go `init` among its file's top-level - * `init` function_declarations (QN disambiguation, #1910). */ -int cbm_go_init_ordinal(TSNode fn_node, const char *source); - // Same check, but the node's PARENT is supplied directly — avoids the // O(n) ts_node_parent rescan. Use at call sites iterating a known // parent's children (the common case). `parent` is the parent of the // node being classified. bool cbm_is_module_level_p(TSNode parent, CBMLanguage lang); +/* QN of a Go `init` function_declaration (#1910): base_qn + "#:", + * N = its 1-based ordinal among the file's top-level `init`s. The ONE formula + * for the def extractor, compute_func_qn and the Go LSP's enclosing-function + * QN. A NULL base_qn or rel_path returns base_qn unchanged. */ +const char *cbm_go_init_qn(CBMArena *a, const char *base_qn, const char *rel_path, TSNode fn_node, + const char *source); + // --- FQN computation --- // Compute qualified name: project.rel_path_parts.name diff --git a/internal/cbm/lsp/go_lsp.c b/internal/cbm/lsp/go_lsp.c index 34fad4b9e..3de20c0d2 100644 --- a/internal/cbm/lsp/go_lsp.c +++ b/internal/cbm/lsp/go_lsp.c @@ -1994,6 +1994,8 @@ static void process_function(GoLSPContext *ctx, TSNode func_node) { // resolver. Since #1909 receiver-qualified Go method QNs, that shared // form is package_qn.ReceiverType.method for methods (the ONE-formula // contract: cbm_go_receiver_type_name) and package_qn.func for functions. + // A package may hold any number of init() functions, so each one is + // package_qn.init#: (#1910, cbm_go_init_qn — the same formula). ctx->enclosing_func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->package_qn, func_name); if (strcmp(ts_node_type(func_node), "method_declaration") == 0) { TSNode recv = ts_node_child_by_field_name(func_node, "receiver", 8); @@ -2004,6 +2006,9 @@ static void process_function(GoLSPContext *ctx, TSNode func_node) { recv_type, func_name); } } + } else if (strcmp(func_name, "init") == 0) { + ctx->enclosing_func_qn = cbm_go_init_qn(ctx->arena, ctx->enclosing_func_qn, ctx->rel_path, + func_node, ctx->source); } // Push function scope @@ -2261,7 +2266,7 @@ static void go_registry_with_stdlib(CBMTypeRegistry *reg, CBMArena *arena) { // --- Entry point: build registry from file defs + run LSP --- void cbm_run_go_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len, - TSNode root) { + TSNode root, const char *rel_path) { CBMTypeRegistry reg; cbm_registry_init(®, arena); @@ -2619,6 +2624,7 @@ void cbm_run_go_lsp(CBMArena *arena, CBMFileResult *result, const char *source, // Phase 2: Build LSP context and run GoLSPContext lsp_ctx; go_lsp_init(&lsp_ctx, arena, source, source_len, ®, module_qn, &result->resolved_calls); + lsp_ctx.rel_path = rel_path; // Add imports for (int i = 0; i < result->imports.count; i++) { @@ -3218,9 +3224,9 @@ static void extract_type_params_from_ast(CBMArena *arena, CBMTypeRegistry *reg, extern const TSLanguage *tree_sitter_go(void); void cbm_run_go_lsp_cross(CBMArena *arena, const char *source, int source_len, - const char *module_qn, CBMLSPDef *defs, int def_count, - const char **import_names, const char **import_qns, int import_count, - TSTree *cached_tree, CBMResolvedCallArray *out) { + const char *module_qn, const char *rel_path, CBMLSPDef *defs, + int def_count, const char **import_names, const char **import_qns, + int import_count, TSTree *cached_tree, CBMResolvedCallArray *out) { if (!source || source_len <= 0) return; @@ -3483,6 +3489,7 @@ void cbm_run_go_lsp_cross(CBMArena *arena, const char *source, int source_len, // 4. Build LSP context and run GoLSPContext ctx; go_lsp_init(&ctx, arena, source, source_len, ®, module_qn, out); + ctx.rel_path = rel_path; for (int i = 0; i < import_count; i++) { if (import_names[i] && import_qns[i]) { @@ -3624,10 +3631,10 @@ CBMTypeRegistry *cbm_go_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, i } void cbm_run_go_lsp_cross_with_registry(CBMArena *arena, const char *source, int source_len, - const char *module_qn, CBMTypeRegistry *reg, - const char **import_names, const char **import_qns, - int import_count, TSTree *cached_tree, - CBMResolvedCallArray *out) { + const char *module_qn, const char *rel_path, + CBMTypeRegistry *reg, const char **import_names, + const char **import_qns, int import_count, + TSTree *cached_tree, CBMResolvedCallArray *out) { if (!source || source_len <= 0 || !reg) return; @@ -3653,6 +3660,7 @@ void cbm_run_go_lsp_cross_with_registry(CBMArena *arena, const char *source, int GoLSPContext ctx; go_lsp_init(&ctx, arena, source, source_len, reg, module_qn, out); + ctx.rel_path = rel_path; for (int i = 0; i < import_count; i++) { if (import_names[i] && import_qns[i]) { go_lsp_add_import(&ctx, import_names[i], import_qns[i]); @@ -3802,8 +3810,8 @@ void cbm_batch_go_lsp_cross(CBMArena *arena, CBMBatchGoLSPFile *files, int file_ // Delegate to existing per-file function cbm_run_go_lsp_cross(&file_arena, file->source, file->source_len, file->module_qn, - file->defs, file->def_count, file->import_names, file->import_qns, - file->import_count, file->cached_tree, &file_out); + file->rel_path, file->defs, file->def_count, file->import_names, + file->import_qns, file->import_count, file->cached_tree, &file_out); // Copy results to output arena (must outlive per-file arena) if (file_out.count > 0) { diff --git a/internal/cbm/lsp/go_lsp.h b/internal/cbm/lsp/go_lsp.h index 5874cbeef..3b504d007 100644 --- a/internal/cbm/lsp/go_lsp.h +++ b/internal/cbm/lsp/go_lsp.h @@ -22,6 +22,7 @@ typedef struct { // Current function context const char* enclosing_func_qn; const char* package_qn; // module QN for this file + const char* rel_path; // repo-relative file path: init() QN suffix (#1910); NULL = flat // Output: resolved calls accumulate here CBMResolvedCallArray* resolved_calls; @@ -63,8 +64,10 @@ const CBMRegisteredFunc* go_lookup_field_or_method(GoLSPContext* ctx, // Entry point: build registry from file's own defs + run LSP resolution. // Called from cbm_extract_file() after definitions and imports are extracted. +// rel_path is the file's repo-relative path: it names the file's init() +// functions exactly as the def extractor does (cbm_go_init_qn, #1910). void cbm_run_go_lsp(CBMArena* arena, CBMFileResult* result, - const char* source, int source_len, TSNode root); + const char* source, int source_len, TSNode root, const char* rel_path); // Register Go stdlib types and functions into a registry. // Auto-generated by scripts/gen-go-stdlib.go. @@ -108,11 +111,14 @@ typedef struct { // Re-parses source with tree-sitter internally. // defs includes both file-local and cross-file definitions. // import_names/import_qns are parallel arrays of length import_count. +// rel_path: the file's repo-relative path, for its init() QNs (#1910); NULL +// leaves init callers flat (they then miss the caller join). // Results written to out (arena-allocated). void cbm_run_go_lsp_cross( CBMArena* arena, const char* source, int source_len, const char* module_qn, + const char* rel_path, CBMLSPDef* defs, int def_count, const char** import_names, const char** import_qns, int import_count, TSTree* cached_tree, // NULL = parse internally @@ -131,6 +137,7 @@ void cbm_run_go_lsp_cross_with_registry( CBMArena* arena, const char* source, int source_len, const char* module_qn, + const char* rel_path, // see cbm_run_go_lsp_cross CBMTypeRegistry* reg, // pre-built, finalized, READ-ONLY const char** import_names, const char** import_qns, int import_count, TSTree* cached_tree, // NULL = parse internally @@ -156,6 +163,7 @@ typedef struct { const char* source; int source_len; const char* module_qn; + const char* rel_path; // repo-relative path (init() QNs, #1910); NULL = flat TSTree* cached_tree; // from TSTree caching (NULL = parse internally) CBMLSPDef* defs; // combined file-local + cross-file defs int def_count; diff --git a/src/pipeline/pass_lsp_cross.c b/src/pipeline/pass_lsp_cross.c index 5aa955112..ed8af87d9 100644 --- a/src/pipeline/pass_lsp_cross.c +++ b/src/pipeline/pass_lsp_cross.c @@ -2079,14 +2079,15 @@ static CBMRustLSPDef *pxc_lspdefs_to_rust(CBMArena *arena, const CBMLSPDef *defs void cbm_pxc_run_one(CBMLanguage lang, CBMFileResult *r, const char *source, int source_len, const char *module_qn, CBMLSPDef *defs, int def_count, const char **imp_names, const char **imp_qns, int imp_count) { - cbm_pxc_run_one_with_php_bindings(lang, r, source, source_len, module_qn, defs, def_count, + cbm_pxc_run_one_with_php_bindings(lang, r, source, source_len, module_qn, NULL, defs, def_count, imp_names, imp_qns, imp_count, NULL); } void cbm_pxc_run_one_with_php_bindings(CBMLanguage lang, CBMFileResult *r, const char *source, - int source_len, const char *module_qn, CBMLSPDef *defs, - int def_count, const char **imp_names, const char **imp_qns, - int imp_count, const CBMPHPImportBindings *bindings) { + int source_len, const char *module_qn, const char *rel_path, + CBMLSPDef *defs, int def_count, const char **imp_names, + const char **imp_qns, int imp_count, + const CBMPHPImportBindings *bindings) { TSTree *tree = r->cached_tree; /* may be NULL — LSP re-parses then */ CBMArena scratch; @@ -2098,8 +2099,8 @@ void cbm_pxc_run_one_with_php_bindings(CBMLanguage lang, CBMFileResult *r, const switch (lang) { case CBM_LANG_GO: - cbm_run_go_lsp_cross(&scratch, source, source_len, module_qn, defs, def_count, imp_names, - imp_qns, imp_count, tree, &out); + cbm_run_go_lsp_cross(&scratch, source, source_len, module_qn, rel_path, defs, def_count, + imp_names, imp_qns, imp_count, tree, &out); break; case CBM_LANG_C: case CBM_LANG_CPP: @@ -2293,9 +2294,9 @@ void cbm_pxc_dispatch_file_with_php_bindings( * lsp_unresolved entries -- no parse, no AST walk -- then the * AST walk against the overlay (chained to the sealed base). */ cbm_go_fast_resolve_qualified_calls(result, prebuilt, imp_keys, imp_vals, imp_count); - cbm_run_go_lsp_cross_with_registry(&scratch, source, source_len, def_module, &overlay, - imp_keys, imp_vals, imp_count, result->cached_tree, - &out); + cbm_run_go_lsp_cross_with_registry(&scratch, source, source_len, def_module, rel, + &overlay, imp_keys, imp_vals, imp_count, + result->cached_tree, &out); used_prebuilt = true; break; case CBM_LANG_PYTHON: { @@ -2429,7 +2430,7 @@ void cbm_pxc_dispatch_file_with_php_bindings( pxc_append_synthetic_calls(&result->arena, &result->calls, &synthetic_calls); pxc_scratch_give(PXC_SCRATCH_DISPATCH, &scratch); } else { - cbm_pxc_run_one_with_php_bindings(lang, result, source, source_len, def_module, + cbm_pxc_run_one_with_php_bindings(lang, result, source, source_len, def_module, rel, file_defs, file_def_count, imp_keys, imp_vals, imp_count, bindings); } @@ -2441,8 +2442,9 @@ void cbm_pxc_dispatch_file_with_php_bindings( cbm_pxc_run_one_ts(result, source, source_len, def_module, file_defs, file_def_count, imp_keys, imp_vals, imp_count, js, jsx, dts); } else { - cbm_pxc_run_one_with_php_bindings(lang, result, source, source_len, def_module, file_defs, - file_def_count, imp_keys, imp_vals, imp_count, bindings); + cbm_pxc_run_one_with_php_bindings(lang, result, source, source_len, def_module, rel, + file_defs, file_def_count, imp_keys, imp_vals, imp_count, + bindings); } free(filtered); } diff --git a/src/pipeline/pass_lsp_cross.h b/src/pipeline/pass_lsp_cross.h index bcbe30425..51964bc41 100644 --- a/src/pipeline/pass_lsp_cross.h +++ b/src/pipeline/pass_lsp_cross.h @@ -216,10 +216,14 @@ void cbm_pxc_run_one(CBMLanguage lang, CBMFileResult *r, const char *source, int const char *module_qn, CBMLSPDef *defs, int def_count, const char **imp_names, const char **imp_qns, int imp_count); +/* rel_path: the file's repo-relative path. Go needs it to name the file's + * init() functions as the def extractor does (#1910); cbm_pxc_run_one passes + * NULL (Go init() callers then stay flat). */ void cbm_pxc_run_one_with_php_bindings(CBMLanguage lang, CBMFileResult *r, const char *source, - int source_len, const char *module_qn, CBMLSPDef *defs, - int def_count, const char **imp_names, const char **imp_qns, - int imp_count, const CBMPHPImportBindings *bindings); + int source_len, const char *module_qn, const char *rel_path, + CBMLSPDef *defs, int def_count, const char **imp_names, + const char **imp_qns, int imp_count, + const CBMPHPImportBindings *bindings); /* TS / JS / JSX / TSX variant with explicit dialect flags. */ void cbm_pxc_run_one_ts(CBMFileResult *r, const char *source, int source_len, const char *module_qn, diff --git a/tests/test_c_lsp.c b/tests/test_c_lsp.c index a4e46c7bc..fc7bf2f1e 100644 --- a/tests/test_c_lsp.c +++ b/tests/test_c_lsp.c @@ -16000,8 +16000,8 @@ TEST(seal_go_shared_registry_readonly) { const char *src = "package main\nfunc helper(x int) int { return x + 1 }\n" "func caller() int { return helper(1) }\n"; CBMResolvedCallArray out = {0}; - cbm_run_go_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test.mod", reg, NULL, NULL, - 0, NULL, &out); + cbm_run_go_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test.mod", NULL, reg, NULL, + NULL, 0, NULL, &out); ASSERT_EQ(reg->func_count, fb); ASSERT_EQ(reg->type_count, tb); cbm_arena_destroy(&arena); diff --git a/tests/test_go_lsp.c b/tests/test_go_lsp.c index e852f4aca..900c3c882 100644 --- a/tests/test_go_lsp.c +++ b/tests/test_go_lsp.c @@ -1063,7 +1063,7 @@ TEST(golsp_crossfile_method_dispatch) { cbm_arena_init(&arena); CBMResolvedCallArray out = {0}; - cbm_run_go_lsp_cross(&arena, source, (int)strlen(source), "test.main", defs, 4, imp_names, + cbm_run_go_lsp_cross(&arena, source, (int)strlen(source), "test.main", NULL, defs, 4, imp_names, imp_qns, 1, NULL, &out); ASSERT_GTE(find_resolved_arr(&out, "doQuery", "Connect"), 0); @@ -1109,7 +1109,7 @@ TEST(golsp_crossfile_return_type_chain) { cbm_arena_init(&arena); CBMResolvedCallArray out = {0}; - cbm_run_go_lsp_cross(&arena, source, (int)strlen(source), "test.main", defs, 4, imp_names, + cbm_run_go_lsp_cross(&arena, source, (int)strlen(source), "test.main", NULL, defs, 4, imp_names, imp_qns, 1, NULL, &out); ASSERT_GTE(find_resolved_arr(&out, "showUser", "Name"), 0); @@ -1149,7 +1149,7 @@ TEST(golsp_crossfile_interface_dispatch) { cbm_arena_init(&arena); CBMResolvedCallArray out = {0}; - cbm_run_go_lsp_cross(&arena, source, (int)strlen(source), "test.main", defs, 4, imp_names, + cbm_run_go_lsp_cross(&arena, source, (int)strlen(source), "test.main", NULL, defs, 4, imp_names, imp_qns, 1, NULL, &out); int idx = find_resolved_arr_confident(&out, "handler", "Bind"); @@ -1205,7 +1205,7 @@ TEST(golsp_crossfile_interface_field_chain) { cbm_arena_init(&arena); CBMResolvedCallArray out = {0}; - cbm_run_go_lsp_cross(&arena, source, (int)strlen(source), "test.main", defs, 6, imp_names, + cbm_run_go_lsp_cross(&arena, source, (int)strlen(source), "test.main", NULL, defs, 6, imp_names, imp_qns, 1, NULL, &out); int idx = find_resolved_arr_confident(&out, "process", "Bind"); @@ -1245,7 +1245,7 @@ TEST(golsp_crossfile_map_index) { cbm_arena_init(&arena); CBMResolvedCallArray out = {0}; - cbm_run_go_lsp_cross(&arena, source, (int)strlen(source), "test.main", defs, 3, imp_names, + cbm_run_go_lsp_cross(&arena, source, (int)strlen(source), "test.main", NULL, defs, 3, imp_names, imp_qns, 1, NULL, &out); int idx = find_resolved_arr_confident(&out, "dispatch", "ServeHTTP"); @@ -1275,7 +1275,7 @@ TEST(golsp_crossfile_stdlib_interface) { cbm_arena_init(&arena); CBMResolvedCallArray out = {0}; - cbm_run_go_lsp_cross(&arena, source, (int)strlen(source), "test.main", defs, 1, imp_names, + cbm_run_go_lsp_cross(&arena, source, (int)strlen(source), "test.main", NULL, defs, 1, imp_names, imp_qns, 1, NULL, &out); ASSERT_GTE(find_resolved_arr_confident(&out, "process", "Done"), 0); @@ -1349,7 +1349,7 @@ TEST(golsp_crossfile_aliased_field_requal) { 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, + cbm_run_go_lsp_cross_with_registry(&arena, source, (int)strlen(source), "test.main", NULL, reg, imp_names, imp_qns, 2, NULL, &out); int svc_idx = find_resolved_arr_confident(&out, "callSvc", "Svc.Ping"); @@ -1417,7 +1417,7 @@ TEST(golsp_crossfile_local_interface_single_impl) { cbm_arena_init(&arena); CBMResolvedCallArray out = {0}; - cbm_run_go_lsp_cross(&arena, source, (int)strlen(source), "test.main", defs, 5, imp_names, + cbm_run_go_lsp_cross(&arena, source, (int)strlen(source), "test.main", NULL, defs, 5, imp_names, imp_qns, 1, NULL, &out); int idxGet = find_resolved_arr_confident(&out, "process", "Get"); @@ -1433,6 +1433,133 @@ TEST(golsp_crossfile_local_interface_single_impl) { PASS(); } +/* ── Go init(): per-file caller QN (#1910) ──────────────────────── */ + +/* Every init() of a package is its own node, pkg.init#: + * (cbm_go_init_qn). The LSP's enclosing-function QN must be exactly that QN: + * the pipeline joins LSP rows to textual calls by caller QN, so a flat + * pkg.init loses every LSP resolution inside an init body. Two inits in one + * file pin the ordinal; the method name exists on both types, so only the + * typed receiver tells the targets apart. */ +TEST(golsp_init_caller_qn_matches_def) { + const char *source = "package main\n\n" + "type Codec struct{}\n\n" + "func (c *Codec) register() {}\n\n" + "type Store struct{}\n\n" + "func (s *Store) register() {}\n\n" + "func init() {\n\tc := &Codec{}\n\tc.register()\n}\n\n" + "func init() {\n\ts := &Store{}\n\ts.register()\n}\n"; + CBMFileResult *r = extract_go(source); + ASSERT_NOT_NULL(r); + const char *init1 = NULL; + const char *init2 = NULL; + for (int i = 0; i < r->defs.count; i++) { + const CBMDefinition *d = &r->defs.items[i]; + if (d->name && strcmp(d->name, "init") == 0) { + if (!init1) { + init1 = d->qualified_name; + } else { + init2 = d->qualified_name; + } + } + } + ASSERT_NOT_NULL(init1); + ASSERT_NOT_NULL(init2); + ASSERT_NOT_NULL(strstr(init1, ".init#main.go:1")); + ASSERT_NOT_NULL(strstr(init2, ".init#main.go:2")); + + int a = require_resolved(r, "init", "Codec.register"); + ASSERT_GTE(a, 0); + ASSERT_STR_EQ(r->resolved_calls.items[a].caller_qn, init1); + int b = require_resolved(r, "init", "Store.register"); + ASSERT_GTE(b, 0); + ASSERT_STR_EQ(r->resolved_calls.items[b].caller_qn, init2); + + /* The join itself: each textual register() call (raw callee text + * "c.register" / "s.register") finds its own init's row. */ + int joined = 0; + for (int i = 0; i < r->calls.count; i++) { + const CBMCall *c = &r->calls.items[i]; + if (!c->callee_name || !strstr(c->callee_name, ".register")) { + continue; + } + const CBMResolvedCall *rc = cbm_pipeline_find_lsp_resolution(&r->resolved_calls, c, false); + ASSERT_NOT_NULL(rc); + ASSERT_STR_EQ(rc->caller_qn, c->enclosing_func_qn); + ASSERT_NOT_NULL( + strstr(rc->callee_qn, strcmp(c->enclosing_func_qn, init1) == 0 ? "Codec" : "Store")); + joined++; + } + ASSERT_EQ(joined, 2); + cbm_free_result(r); + PASS(); +} + +/* Cross-file twin: both cross entry points (per-file defs, and the shared + * Tier-2 registry the pipeline uses) name the init caller from rel_path. */ +TEST(golsp_crossfile_init_caller_qn_per_file) { + const char *source = "package reg\n\n" + "func init() {\n\tc := NewCodec()\n\tc.Register()\n}\n\n" + "func init() {\n\ts := NewStore()\n\ts.Register()\n}\n"; + CBMLSPDef defs[] = { + {.qualified_name = "test.reg.NewCodec", + .short_name = "NewCodec", + .label = "Function", + .def_module_qn = "test.reg", + .return_types = "*Codec"}, + {.qualified_name = "test.reg.Codec", + .short_name = "Codec", + .label = "Class", + .def_module_qn = "test.reg"}, + {.qualified_name = "test.reg.Codec.Register", + .short_name = "Register", + .label = "Method", + .def_module_qn = "test.reg", + .receiver_type = "test.reg.Codec"}, + {.qualified_name = "test.reg.NewStore", + .short_name = "NewStore", + .label = "Function", + .def_module_qn = "test.reg", + .return_types = "*Store"}, + {.qualified_name = "test.reg.Store", + .short_name = "Store", + .label = "Class", + .def_module_qn = "test.reg"}, + {.qualified_name = "test.reg.Store.Register", + .short_name = "Register", + .label = "Method", + .def_module_qn = "test.reg", + .receiver_type = "test.reg.Store"}, + }; + + CBMArena arena; + cbm_arena_init(&arena); + CBMResolvedCallArray out = {0}; + cbm_run_go_lsp_cross(&arena, source, (int)strlen(source), "test.reg", "reg/b.go", defs, 6, NULL, + NULL, 0, NULL, &out); + int c1 = find_resolved_arr_confident(&out, "init", "Codec.Register"); + ASSERT_GTE(c1, 0); + ASSERT_STR_EQ(out.items[c1].caller_qn, "test.reg.init#b.go:1"); + int s1 = find_resolved_arr_confident(&out, "init", "Store.Register"); + ASSERT_GTE(s1, 0); + ASSERT_STR_EQ(out.items[s1].caller_qn, "test.reg.init#b.go:2"); + + CBMResolvedCallArray shared_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.reg", "reg/b.go", + reg, NULL, NULL, 0, NULL, &shared_out); + int c2 = find_resolved_arr_confident(&shared_out, "init", "Codec.Register"); + ASSERT_GTE(c2, 0); + ASSERT_STR_EQ(shared_out.items[c2].caller_qn, "test.reg.init#b.go:1"); + int s2 = find_resolved_arr_confident(&shared_out, "init", "Store.Register"); + ASSERT_GTE(s2, 0); + ASSERT_STR_EQ(shared_out.items[s2].caller_qn, "test.reg.init#b.go:2"); + + cbm_arena_destroy(&arena); + PASS(); +} + /* ── Suite ─────────────────────────────────────────────────────── */ SUITE(go_lsp) { @@ -1545,4 +1672,8 @@ SUITE(go_lsp) { RUN_TEST(golsp_crossfile_map_index); RUN_TEST(golsp_crossfile_stdlib_interface); RUN_TEST(golsp_crossfile_local_interface_single_impl); + + /* Go init(): per-file caller QN (#1910) */ + RUN_TEST(golsp_init_caller_qn_matches_def); + RUN_TEST(golsp_crossfile_init_caller_qn_per_file); } diff --git a/tests/test_parallel.c b/tests/test_parallel.c index a641e7e0e..993c646eb 100644 --- a/tests/test_parallel.c +++ b/tests/test_parallel.c @@ -5476,7 +5476,7 @@ TEST(parallel_php_external_row_replaces_only_its_exact_occurrence) { const char *keys[] = {"LocalRequest"}; const char *vals[] = {project_type}; cbm_pxc_run_one_with_php_bindings(CBM_LANG_PHP, r, source, (int)strlen(source), "p.main", - defs, 2, keys, vals, 1, &bindings); + NULL, defs, 2, keys, vals, 1, &bindings); int external_count = 0, project_count = 0; for (int i = 0; i < r->resolved_calls.count; i++) { const CBMResolvedCall *row = &r->resolved_calls.items[i]; diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 8b413066d..9f32c3d06 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -10298,6 +10298,194 @@ TEST(pipeline_go_multi_init_nodes_survive) { PASS(); } +/* Edges of `edge_type` whose source and target QNs end with the given + * suffixes (go_calls_between for any edge type, without the properties). */ +static int go_edges_between(cbm_store_t *s, const char *project, const char *edge_type, + const char *caller_suffix, const char *callee_suffix) { + cbm_edge_t *edges = NULL; + int edge_count = 0; + cbm_store_find_edges_by_type(s, project, edge_type, &edges, &edge_count); + size_t cs = strlen(caller_suffix); + size_t ts = strlen(callee_suffix); + int hits = 0; + for (int i = 0; i < edge_count; i++) { + cbm_node_t src = {0}; + cbm_node_t tgt = {0}; + if (cbm_store_find_node_by_id(s, edges[i].source_id, &src) == CBM_STORE_OK && + cbm_store_find_node_by_id(s, edges[i].target_id, &tgt) == CBM_STORE_OK && + src.qualified_name && tgt.qualified_name) { + size_t sl = strlen(src.qualified_name); + size_t tl = strlen(tgt.qualified_name); + if (sl >= cs && tl >= ts && strcmp(src.qualified_name + sl - cs, caller_suffix) == 0 && + strcmp(tgt.qualified_name + tl - ts, callee_suffix) == 0) { + hits++; + } + } + cbm_node_free_fields(&src); + cbm_node_free_fields(&tgt); + } + if (edges) { + cbm_store_free_edges(edges, edge_count); + } + return hits; +} + +/* Fixture for the #1910 Go init() LSP-join probes (sequential and parallel + * twins). Three init() in two files of one package -- two in b.go, so the + * per-file ordinal is pinned too -- each calling a method that only the Go + * LSP can bind: the receiver's type comes from a constructor in another + * file, and the method name also exists on a second package type and on + * other.Decoy, so no short-name fallback lands on the right target. a.go's + * init also passes a handler by value (add("a", handleA): a CALL_REFERENCE, + * which exists only as an LSP row). pad_files > 0 adds filler Go files past + * MIN_FILES_FOR_PARALLEL. */ +static void write_go_init_lsp_fixture(const char *tmp, int pad_files) { + write_temp_file(tmp, "go.mod", "module example.com/fxinit\n\ngo 1.22\n"); + write_temp_file(tmp, "reg/codec.go", + "package reg\n" + "\n" + "type Codec struct{ n int }\n" + "\n" + "func NewCodec() *Codec { return &Codec{} }\n" + "\n" + "func (c *Codec) register() { c.n++ }\n"); + write_temp_file(tmp, "reg/store.go", + "package reg\n" + "\n" + "type Store struct{ open bool }\n" + "\n" + "func NewStore() *Store { return &Store{} }\n" + "\n" + "func (s *Store) register() { s.open = true }\n" + "\n" + "func (s *Store) warm() {}\n"); + write_temp_file(tmp, "reg/a.go", + "package reg\n" + "\n" + "var handlers = map[string]func(){}\n" + "\n" + "func handleA() {}\n" + "\n" + "func add(name string, fn func()) { handlers[name] = fn }\n" + "\n" + "func init() {\n" + "\tc := NewCodec()\n" + "\tc.register()\n" + "\tadd(\"a\", handleA)\n" + "}\n"); + write_temp_file(tmp, "reg/b.go", + "package reg\n" + "\n" + "func init() {\n" + "\ts := NewStore()\n" + "\ts.register()\n" + "}\n" + "\n" + "func init() {\n" + "\ts := NewStore()\n" + "\ts.warm()\n" + "}\n"); + write_temp_file(tmp, "other/other.go", + "package other\n" + "\n" + "type Decoy struct{}\n" + "\n" + "func (d *Decoy) register() {}\n" + "\n" + "func (d *Decoy) warm() {}\n"); + for (int i = 0; i < pad_files; i++) { + char name[64]; + char body[128]; + snprintf(name, sizeof(name), "pad/filler%d.go", i); + snprintf(body, sizeof(body), "package pad\n\nfunc filler%d() int { return %d }\n", i, i); + write_temp_file(tmp, name, body); + } +} + +/* Each init's LSP-resolved edges start at that init's own node. */ +static int assert_go_init_lsp_edges(cbm_store_t *s, const char *project) { + const char *lsp = "\"strategy\":\"lsp_"; + ASSERT_GTE(go_calls_between(s, project, ".reg.init#a.go:1", ".reg.Codec.register", lsp), 1); + ASSERT_GTE(go_calls_between(s, project, ".reg.init#b.go:1", ".reg.Store.register", lsp), 1); + ASSERT_GTE(go_calls_between(s, project, ".reg.init#b.go:2", ".reg.Store.warm", lsp), 1); + ASSERT_GTE(go_edges_between(s, project, "CALL_REFERENCE", ".reg.init#a.go:1", ".reg.handleA"), + 1); + /* No cross-wiring between the inits, and nothing onto the decoy. */ + ASSERT_EQ(go_calls_between(s, project, ".reg.init#a.go:1", ".reg.Store.register", NULL), 0); + ASSERT_EQ(go_calls_between(s, project, ".reg.init#b.go:1", ".reg.Codec.register", NULL), 0); + ASSERT_EQ(go_calls_between(s, project, ".reg.init#b.go:2", ".reg.Store.register", NULL), 0); + ASSERT_EQ(go_calls_between(s, project, ".reg.init#a.go:1", ".other.Decoy.register", NULL), 0); + ASSERT_EQ(go_calls_between(s, project, ".reg.init#b.go:1", ".other.Decoy.register", NULL), 0); + ASSERT_EQ(go_calls_between(s, project, ".reg.init#b.go:2", ".other.Decoy.warm", NULL), 0); + return 0; +} + +/* #1910: the def extractor and compute_func_qn name each init + * pkg.init#:; the Go LSP must name its caller the same way + * (cbm_go_init_qn), or every LSP resolution inside an init body loses the + * exact caller-QN join and falls back to the registry resolver. */ +TEST(pipeline_go_init_lsp_edges_per_file) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_init_lsp_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + FAIL("tmpdir"); + } + write_go_init_lsp_fixture(tmp, 0); + + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/go_init_lsp.db", tmp); + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + ASSERT_NOT_NULL(p); + ASSERT_EQ(cbm_pipeline_run(p), 0); + const char *project = cbm_pipeline_project_name(p); + + cbm_store_t *s = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(s); + ASSERT_EQ(assert_go_init_lsp_edges(s, project), 0); + + cbm_store_close(s); + cbm_pipeline_free(p); + th_rmtree(tmp); + PASS(); +} + +/* Parallel twin: >= MIN_FILES_FOR_PARALLEL (50) files plus CBM_WORKERS=4 + * take the parallel resolver (resolve_worker's Tier-2 dispatch). */ +TEST(pipeline_go_init_lsp_edges_per_file_parallel) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_init_lsp_par_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + FAIL("tmpdir"); + } + write_go_init_lsp_fixture(tmp, 52); + + char *old_workers = getenv("CBM_WORKERS"); + char *saved = old_workers ? strdup(old_workers) : NULL; + cbm_setenv("CBM_WORKERS", "4", 1); + + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/go_init_lsp_par.db", tmp); + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + ASSERT_NOT_NULL(p); + ASSERT_EQ(cbm_pipeline_run(p), 0); + const char *project = cbm_pipeline_project_name(p); + + cbm_store_t *s = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(s); + ASSERT_EQ(assert_go_init_lsp_edges(s, project), 0); + + cbm_store_close(s); + cbm_pipeline_free(p); + if (saved) { + cbm_setenv("CBM_WORKERS", saved, 1); + free(saved); + } else { + cbm_unsetenv("CBM_WORKERS"); + } + th_rmtree(tmp); + PASS(); +} + /* Fixture for the #1928 cross-language reference-guard probes (sequential and * parallel twins). pad_files > 0 adds filler files to push the index over the * parallel-pipeline threshold, since USAGE/WRITES/READS have one resolver per @@ -16256,6 +16444,53 @@ TEST(pipeline_python_init_import_probe_failure_falls_back) { PASS(); } +/* #1910: with no shared Go registry (the incremental route can pass NULL + * cross_registries) the dispatch takes the per-file fallback, which must name + * each init() caller from the file path exactly as the def extractor does. */ +TEST(pipeline_go_init_dispatch_fallback_names_init_caller) { + const char *source = "package reg\n\n" + "func init() {\n\tc := NewCodec()\n\tc.Register()\n}\n\n" + "func init() {\n\tc := NewCodec()\n\tc.Register()\n}\n"; + CBMLSPDef defs[] = { + {.qualified_name = "test.reg.NewCodec", + .short_name = "NewCodec", + .label = "Function", + .def_module_qn = "test.reg", + .return_types = "*Codec", + .lang = CBM_LANG_GO}, + {.qualified_name = "test.reg.Codec", + .short_name = "Codec", + .label = "Class", + .def_module_qn = "test.reg", + .lang = CBM_LANG_GO}, + {.qualified_name = "test.reg.Codec.Register", + .short_name = "Register", + .label = "Method", + .def_module_qn = "test.reg", + .receiver_type = "test.reg.Codec", + .lang = CBM_LANG_GO}, + }; + CBMFileResult result = {0}; + cbm_arena_init(&result.arena); + cbm_pxc_dispatch_file(CBM_LANG_GO, &result, source, (int)strlen(source), "reg/b.go", "test.reg", + NULL, NULL, defs, 3, NULL, NULL, 0, NULL, NULL); + int first = 0; + int second = 0; + for (int i = 0; i < result.resolved_calls.count; i++) { + const CBMResolvedCall *rc = &result.resolved_calls.items[i]; + if (!rc->caller_qn || !rc->callee_qn || rc->confidence <= 0 || + strcmp(rc->callee_qn, "test.reg.Codec.Register") != 0) { + continue; + } + first += strcmp(rc->caller_qn, "test.reg.init#b.go:1") == 0; + second += strcmp(rc->caller_qn, "test.reg.init#b.go:2") == 0; + } + cbm_arena_destroy(&result.arena); + ASSERT_EQ(first, 1); + ASSERT_EQ(second, 1); + PASS(); +} + /* #1277: typed instance fields of a Python class imported from another file. * Counts CALLS edges caller -> callee (QN suffixes) whose properties name an * LSP strategy. */ @@ -28171,6 +28406,7 @@ SUITE(pipeline) { RUN_TEST(pipeline_python_init_register_scope_failure_parallel); RUN_TEST(pipeline_python_init_import_filter_precedence); RUN_TEST(pipeline_python_init_import_probe_failure_falls_back); + RUN_TEST(pipeline_go_init_dispatch_fallback_names_init_caller); RUN_TEST(pipeline_issue1527_python_raw_memo_failure_sequential); RUN_TEST(pipeline_issue1527_python_raw_memo_failure_parallel); RUN_TEST(pipeline_issue1527_python_cross_memo_failure_sequential); @@ -28291,6 +28527,8 @@ SUITE(pipeline) { RUN_TEST(pipeline_go_multi_return_receiver_is_typed); RUN_TEST(pipeline_go_multi_return_receiver_is_typed_parallel); RUN_TEST(pipeline_go_multi_init_nodes_survive); + RUN_TEST(pipeline_go_init_lsp_edges_per_file); + RUN_TEST(pipeline_go_init_lsp_edges_per_file_parallel); RUN_TEST(pipeline_go_rw_usage_never_cross_into_c); RUN_TEST(pipeline_go_rw_usage_never_cross_into_c_parallel); RUN_TEST(pipeline_rust_call_never_binds_another_language);