mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-06 16:53:56 +00:00
fix(extract): give each Go init() its own QN
Go allows any number of init() functions per package — even several in one file — and all of them run at start-up. On the flat QN (proj.pkg.init) they all collided and the graph upsert kept ONE node per package, silently dropping the rest: measured on a real Go repo, an event-source package registering each decoder in its own file's init() collapsed 21 init functions into one node (29 in source, 7 in the graph), erasing the whole registration pattern. Disambiguate with the #495 cfg-twin pattern: fold the file basename and line into the QN (proj.pkg.init#a.go:L5). Calling init explicitly is illegal in Go, so nothing ever joins on the plain QN; the call-scope side (compute_func_qn) mirrors the exact formula so init-body calls keep their source attribution instead of degrading to the calls_find_source File fallback. Reproduce-first: pipeline_go_multi_init_nodes_survive is RED without the def-side change (node count 1, expected 2) and GREEN with it; extract_go_multiple_init_disambiguated pins two same-file inits to distinct suffixed QNs. Fixes #1910 Signed-off-by: Ilya Brykau <ilya.brykau@orca.security>
This commit is contained in:
1 parent
dbba757301
commit
adff62e7f2
6 files changed
+188
No files matched your search
@@ -3739,6 +3739,25 @@ 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) computes the
|
||||
* same cbm_go_init_ordinal 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));
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -918,6 +918,22 @@ 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
|
||||
* (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));
|
||||
}
|
||||
|
||||
/* 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
|
||||
|
||||
@@ -1734,6 +1734,42 @@ int cbm_classify_string(const char *str, int len) {
|
||||
* 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];
|
||||
|
||||
@@ -197,6 +197,10 @@ 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
|
||||
|
||||
@@ -4899,6 +4899,68 @@ TEST(extract_go_method_receiver_qualified_qn) {
|
||||
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
|
||||
@@ -6580,6 +6642,7 @@ SUITE(extraction) {
|
||||
RUN_TEST(extract_python_member_call_flags_is_method);
|
||||
RUN_TEST(extract_go_selector_call_flags_is_method);
|
||||
RUN_TEST(extract_go_method_receiver_qualified_qn);
|
||||
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);
|
||||
|
||||
@@ -4837,6 +4837,55 @@ TEST(pipeline_go_receiver_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();
|
||||
}
|
||||
|
||||
/* 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
|
||||
@@ -13261,6 +13310,7 @@ SUITE(pipeline) {
|
||||
RUN_TEST(pipeline_python_receiver_suppresses_weak_method_edge);
|
||||
RUN_TEST(pipeline_go_method_caller_keeps_lsp_join);
|
||||
RUN_TEST(pipeline_go_receiver_suppresses_weak_method_edge);
|
||||
RUN_TEST(pipeline_go_multi_init_nodes_survive);
|
||||
RUN_TEST(pipeline_go_rw_usage_never_cross_into_c);
|
||||
RUN_TEST(pipeline_go_rw_usage_never_cross_into_c_parallel);
|
||||
RUN_TEST(pipeline_go_bare_ref_never_binds_field);
|
||||
|
||||
Reference in new issue
Block a user