diff --git a/internal/cbm/cbm.c b/internal/cbm/cbm.c index 1156a6a73..27d58c69b 100644 --- a/internal/cbm/cbm.c +++ b/internal/cbm/cbm.c @@ -3924,7 +3924,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 851b72e00..51cf7279f 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -7061,6 +7061,24 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec } } + /* Go allows any number of init() functions per package — even several in + * one file — and they all run at start-up. On the flat QN they all + * collided and the graph upsert kept ONE node per package, silently + * dropping the rest (#1910). Disambiguate with the #495 cfg-twin pattern: + * fold the file basename and the init's per-file ordinal into the QN. + * The ordinal (not the line) keeps the QN stable when code is edited + * 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) 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) { + 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 // class body in the header is declaration-only, so without this the // definition is recorded as a free Function. Promote it to a Method whose QN diff --git a/internal/cbm/extract_unified.c b/internal/cbm/extract_unified.c index c42ccebb6..acada15ad 100644 --- a/internal/cbm/extract_unified.c +++ b/internal/cbm/extract_unified.c @@ -1133,6 +1133,23 @@ 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). 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); + /* 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 * extractor bakes it into the def QN. Compose it identically here — otherwise * an in-body call sources to a QN one or more segments short of the def, and diff --git a/internal/cbm/helpers.c b/internal/cbm/helpers.c index 9e310bc6c..d6a8e73b9 100644 --- a/internal/cbm/helpers.c +++ b/internal/cbm/helpers.c @@ -2330,6 +2330,65 @@ 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 diff --git a/internal/cbm/helpers.h b/internal/cbm/helpers.h index 3ffa80bc4..4c96cb5f0 100644 --- a/internal/cbm/helpers.h +++ b/internal/cbm/helpers.h @@ -287,6 +287,13 @@ bool cbm_is_module_level(TSNode node, CBMLanguage lang); // 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 dd9572747..3f053218d 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++) { @@ -3239,9 +3245,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; @@ -3505,6 +3511,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]) { @@ -3647,10 +3654,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; @@ -3676,6 +3683,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]); @@ -3825,8 +3833,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 9e742a715..37a0b787a 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. @@ -112,11 +115,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 @@ -135,6 +141,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 @@ -160,6 +167,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 a057f9a7b..a0a769fd1 100644 --- a/src/pipeline/pass_lsp_cross.c +++ b/src/pipeline/pass_lsp_cross.c @@ -2082,14 +2082,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; @@ -2101,8 +2102,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: @@ -2296,9 +2297,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: { @@ -2432,7 +2433,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); } @@ -2444,8 +2445,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_extraction.c b/tests/test_extraction.c index 19b645d96..ea6e7bedb 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -8143,6 +8143,68 @@ TEST(extract_templ_method_qn_matches_call_scope) { PASS(); } +TEST(extract_go_multiple_init_disambiguated) { + /* Go allows several init() per package (even per file); each gets a + * file+ordinal-suffixed QN (#495 cfg-twin pattern) so none is lost to the + * same-QN upsert. The ordinal — unlike a line number — must not move when + * code is inserted above the function: the QN is node identity, and a + * line-based suffix would churn nodes on every unrelated edit. */ + CBMFileResult *r = extract("package m\n" + "func init() { a() }\n" + "func init() { b() }\n" + "func a() {}\n" + "func b() {}\n", + CBM_LANG_GO, "t", "x.go"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + const char *first = NULL; + const char *second = 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) { + continue; + } + if (!first) { + first = d->qualified_name; + } else { + second = d->qualified_name; + } + } + ASSERT_NOT_NULL(first); + ASSERT_NOT_NULL(second); + ASSERT_STR_EQ(first, "t.init#x.go:1"); + ASSERT_STR_EQ(second, "t.init#x.go:2"); + + /* Stability: an insertion ABOVE both inits must not change either QN. */ + CBMFileResult *r2 = extract("package m\n" + "import \"fmt\"\n" + "var shifted = fmt.Sprint(\"pad\")\n" + "func init() { a() }\n" + "func init() { b() }\n" + "func a() {}\n" + "func b() {}\n", + CBM_LANG_GO, "t", "x.go"); + ASSERT_NOT_NULL(r2); + ASSERT_FALSE(r2->has_error); + int seen = 0; + for (int i = 0; i < r2->defs.count; i++) { + const CBMDefinition *d = &r2->defs.items[i]; + if (!d->name || strcmp(d->name, "init") != 0) { + continue; + } + seen++; + if (seen == 1) { + ASSERT_STR_EQ(d->qualified_name, "t.init#x.go:1"); + } else { + ASSERT_STR_EQ(d->qualified_name, "t.init#x.go:2"); + } + } + ASSERT_EQ(seen, 2); + cbm_free_result(r2); + cbm_free_result(r); + PASS(); +} + /* TS/JS/TSX receiver-aware flag (#592/#606; same intent as the Perl flag above). * A member call x.foo() with a non-this/super receiver is flagged is_method so * the resolver can suppress a weak short-name match (`re.test()` must not bind a @@ -13536,6 +13598,7 @@ SUITE(extraction) { RUN_TEST(extract_go_selector_call_flags_is_method); RUN_TEST(extract_go_method_receiver_qualified_qn); RUN_TEST(extract_templ_method_qn_matches_call_scope); + RUN_TEST(extract_go_multiple_init_disambiguated); RUN_TEST(extract_ts_member_call_flags_is_method); RUN_TEST(extract_ts_this_super_receiver_not_flagged); RUN_TEST(extract_js_member_call_flags_is_method); diff --git a/tests/test_go_lsp.c b/tests/test_go_lsp.c index b79101c07..1f0d58087 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"); @@ -1469,7 +1469,7 @@ TEST(golsp_crossfile_type_alias_method) { CBMResolvedCallArray out = {0}; cbm_run_go_lsp_cross(&arena, go_crossfile_alias_source, (int)strlen(go_crossfile_alias_source), - "test.main", go_crossfile_alias_defs, n, NULL, NULL, 0, NULL, &out); + "test.main", NULL, go_crossfile_alias_defs, n, NULL, NULL, 0, NULL, &out); if (go_crossfile_alias_check(&out) != 0) { cbm_arena_destroy(&arena); return 1; @@ -1488,8 +1488,8 @@ TEST(golsp_crossfile_type_alias_method_prebuilt) { CBMTypeRegistry *reg = cbm_go_build_cross_registry(&arena, go_crossfile_alias_defs, n); ASSERT_NOT_NULL(reg); cbm_run_go_lsp_cross_with_registry(&arena, go_crossfile_alias_source, - (int)strlen(go_crossfile_alias_source), "test.main", reg, - NULL, NULL, 0, NULL, &out); + (int)strlen(go_crossfile_alias_source), "test.main", NULL, + reg, NULL, NULL, 0, NULL, &out); if (go_crossfile_alias_check(&out) != 0) { cbm_arena_destroy(&arena); return 1; @@ -1538,7 +1538,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"); @@ -1554,6 +1554,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) { @@ -1668,4 +1795,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 9183f33b5..b73946cf3 100644 --- a/tests/test_parallel.c +++ b/tests/test_parallel.c @@ -5597,7 +5597,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 94edecc29..303af90f7 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -10834,6 +10834,243 @@ TEST(pipeline_go_receiver_parallel_suppresses_weak_method_edge) { PASS(); } +TEST(pipeline_go_multi_init_nodes_survive) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_init_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + FAIL("tmpdir"); + } + + /* Two files, one package, one init() each — Go runs both at start-up. + * Pre-fix both collapsed onto one QN and the upsert kept one node + * (reproduce-first: RED asserts node count == 2). */ + write_temp_file(tmp, "go.mod", "module example.com/reg\n\ngo 1.22\n"); + write_temp_file(tmp, "reg/a.go", + "package reg\n" + "\n" + "var handlers = map[string]func(){}\n" + "\n" + "func init() { handlers[\"a\"] = handleA }\n" + "\n" + "func handleA() {}\n"); + write_temp_file(tmp, "reg/b.go", + "package reg\n" + "\n" + "func init() { handlers[\"b\"] = handleB }\n" + "\n" + "func handleB() {}\n"); + + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/go_init.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); + + /* Both init functions survive as distinct nodes. */ + cbm_node_t *inits = NULL; + int ic = 0; + cbm_store_find_nodes_by_name(s, project, "init", &inits, &ic); + ASSERT_EQ(ic, 2); + cbm_store_free_nodes(inits, ic); + + cbm_store_close(s); + cbm_pipeline_free(p); + th_rmtree(tmp); + 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 @@ -16976,6 +17213,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. */ @@ -29207,6 +29491,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); @@ -29333,6 +29618,9 @@ SUITE(pipeline) { RUN_TEST(pipeline_go_receiver_parallel_suppresses_weak_method_edge); 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_crossfile_type_alias_method); RUN_TEST(pipeline_go_crossfile_type_alias_method_parallel); RUN_TEST(pipeline_go_rw_usage_never_cross_into_c);