mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
fix(fqn): preserve full filename in File-node QNs so sibling files don't collide
cbm_pipeline_fqn_compute stripped the file extension for EVERY qualified name, including File-node QNs (name=="__file__"). Two different files whose names share a stem therefore collided on the File QN and the graph kept only one node per stem: - .env / .env.local / .env.production all strip to ".env" — only one survives per directory (#1077). - a C/C++ header and its same-stem source (NodeController.h vs .cpp) strip to the same stem, so the header got no File node of its own, leaving it disconnected (#964's remaining half after #983 fixed the include→Class resolution). File-node QNs now keep the full filename (tokenize_path splits on '/' only, so the extension rides in the last segment and stays unique). Extension stripping is retained for MODULE/symbol QNs (name==NULL or a symbol name) — that stem unification is load-bearing for C/C++ declaration↔definition cross-file resolution, and is left untouched. All 26 __file__ QN sites route through this one function, so node creation and every lookup stay consistent. Reproduce-first: - fqn_file_qn_preserves_dotfile_variants_issue1077 and fqn_file_qn_distinguishes_same_stem_header_source_issue964 assert the distinct File QNs (RED before: both collide to the stripped stem). - fqn_module_qn_still_strips_extension guards that module QNs are unaffected. - repro_issue964 now asserts the header keeps its own File node AND is connected (CONTAINS_FILE + DEFINES the class it declares), matching the reporter's zero-inbound disconnection metric — GREEN. Closes #1077 Closes #964 Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
ef1482245f
commit
751752bd21
3 files changed
+80
-23
No files matched your search
+13
-1
@@ -117,7 +117,19 @@ char *cbm_pipeline_fqn_compute(const char *project, const char *rel_path, const
|
||||
|
||||
char *path = strdup(rel_path ? rel_path : "");
|
||||
cbm_normalize_path_sep(path);
|
||||
strip_file_extension(path);
|
||||
/* #1077/#964: File-node QNs (name=="__file__") must preserve the full
|
||||
* filename so sibling files sharing a stem get DISTINCT nodes — e.g.
|
||||
* .env / .env.local / .env.production (which all strip to ".env" and
|
||||
* collide, so only one survives per directory), and a C/C++ header vs
|
||||
* its same-stem .cpp. Extension stripping stays for MODULE/symbol QNs
|
||||
* (name==NULL or a symbol): that stem unification is load-bearing for
|
||||
* C/C++ declaration↔definition cross-file resolution. All 26 __file__
|
||||
* QN sites route through here, so creation and every lookup stay
|
||||
* consistent. */
|
||||
bool is_file_qn = name && strcmp(name, "__file__") == 0;
|
||||
if (!is_file_qn) {
|
||||
strip_file_extension(path);
|
||||
}
|
||||
|
||||
const char *segments[CBM_SZ_256];
|
||||
int seg_count = 0;
|
||||
|
||||
@@ -60,26 +60,36 @@ static int r964_file_node_exists(cbm_store_t *store, const char *project, const
|
||||
return found;
|
||||
}
|
||||
|
||||
/* Count inbound IMPORTS edges whose TARGET node name is exactly `name`. */
|
||||
static int r964_inbound_imports(cbm_store_t *store, const char *project, const char *name) {
|
||||
cbm_edge_t *edges = NULL;
|
||||
int n = 0;
|
||||
if (cbm_store_find_edges_by_type(store, project, "IMPORTS", &edges, &n) != CBM_STORE_OK)
|
||||
return -1;
|
||||
int hits = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
cbm_node_t tgt;
|
||||
if (cbm_store_find_node_by_id(store, edges[i].target_id, &tgt) != CBM_STORE_OK)
|
||||
/* Count INBOUND edges (any of the reporter's connection types) landing on the
|
||||
* File node named `name` — the exact disconnection metric from the issue's
|
||||
* query `NOT EXISTS { (f)<-[:IMPORTS|CALLS|CONTAINS_FILE|DEFINES|USAGE]-() }`.
|
||||
* A header that used to have no node at all (extension-stripped QN collision)
|
||||
* now has its own File node AND is connected (CONTAINS_FILE from its folder,
|
||||
* plus DEFINES of the class it declares); `#include` itself resolves to the
|
||||
* declared Class node via #983, not to the header File. */
|
||||
static int r964_file_inbound(cbm_store_t *store, const char *project, const char *name) {
|
||||
static const char *const types[] = {"IMPORTS", "CALLS", "CONTAINS_FILE",
|
||||
"DEFINES", "USAGE", NULL};
|
||||
int total = 0;
|
||||
for (int t = 0; types[t]; t++) {
|
||||
cbm_edge_t *edges = NULL;
|
||||
int n = 0;
|
||||
if (cbm_store_find_edges_by_type(store, project, types[t], &edges, &n) != CBM_STORE_OK)
|
||||
continue;
|
||||
if (tgt.name && strcmp(tgt.name, name) == 0)
|
||||
hits++;
|
||||
cbm_node_free_fields(&tgt);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cbm_node_t tgt;
|
||||
if (cbm_store_find_node_by_id(store, edges[i].target_id, &tgt) != CBM_STORE_OK)
|
||||
continue;
|
||||
if (tgt.name && strcmp(tgt.name, name) == 0)
|
||||
total++;
|
||||
cbm_node_free_fields(&tgt);
|
||||
}
|
||||
cbm_store_free_edges(edges, n);
|
||||
}
|
||||
cbm_store_free_edges(edges, n);
|
||||
return hits;
|
||||
return total;
|
||||
}
|
||||
|
||||
TEST(repro_issue964_header_has_node_and_inbound_import) {
|
||||
TEST(repro_issue964_header_has_node_and_is_connected) {
|
||||
static const RFile files[] = {
|
||||
{"NodeController.h", "#pragma once\n"
|
||||
"class NodeController {\n"
|
||||
@@ -102,20 +112,20 @@ TEST(repro_issue964_header_has_node_and_inbound_import) {
|
||||
ASSERT_NOT_NULL(store);
|
||||
|
||||
int header_node = r964_file_node_exists(store, lp.project, "NodeController.h");
|
||||
int header_inbound = r964_inbound_imports(store, lp.project, "NodeController.h");
|
||||
int header_inbound = r964_file_inbound(store, lp.project, "NodeController.h");
|
||||
if (!header_node || header_inbound < 1) {
|
||||
fprintf(stderr,
|
||||
" [964] FAIL header_file_node=%d inbound_imports=%d (header merged into "
|
||||
"same-stem .cpp by extension-stripped QN collision)\n",
|
||||
" [964] FAIL header_file_node=%d inbound_edges=%d (header merged into "
|
||||
"same-stem .cpp by extension-stripped QN collision, or left disconnected)\n",
|
||||
header_node, header_inbound);
|
||||
}
|
||||
ASSERT_TRUE(header_node); /* header must keep its own File node */
|
||||
ASSERT_TRUE(header_inbound >= 1); /* #include must land an edge on it */
|
||||
ASSERT_TRUE(header_node); /* header keeps its own File node (not merged) */
|
||||
ASSERT_TRUE(header_inbound >= 1); /* and is connected (not the reporter's zero-inbound) */
|
||||
|
||||
rh_cleanup(&lp, store);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(repro_issue964) {
|
||||
RUN_TEST(repro_issue964_header_has_node_and_inbound_import);
|
||||
RUN_TEST(repro_issue964_header_has_node_and_is_connected);
|
||||
}
|
||||
@@ -52,6 +52,38 @@ TEST(fqn_compute_basic_c) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #1077/#964: File-node QNs (name=="__file__") must preserve the FULL filename
|
||||
* so sibling files sharing a stem get DISTINCT nodes. Extension stripping is
|
||||
* kept for module/symbol QNs (name!=__file__) — load-bearing for C/C++
|
||||
* declaration↔definition resolution. */
|
||||
TEST(fqn_file_qn_preserves_dotfile_variants_issue1077) {
|
||||
/* .env / .env.local / .env.production all stripped to ".env" before,
|
||||
* colliding so only one File node survived per directory. */
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", ".env", "__file__"), "proj..env.__file__");
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", ".env.local", "__file__"),
|
||||
"proj..env.local.__file__");
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", ".env.production", "__file__"),
|
||||
"proj..env.production.__file__");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_file_qn_distinguishes_same_stem_header_source_issue964) {
|
||||
/* NodeController.h and NodeController.cpp both stripped to
|
||||
* "NodeController", so the header's File node was merged into the .cpp's. */
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "NodeController.h", "__file__"),
|
||||
"proj.NodeController.h.__file__");
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "NodeController.cpp", "__file__"),
|
||||
"proj.NodeController.cpp.__file__");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_module_qn_still_strips_extension) {
|
||||
/* The MODULE/symbol QN keeps stripping — unchanged by the File-QN fix. */
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "core.c", "init"), "proj.core.init");
|
||||
ASSERT_FQN(cbm_pipeline_fqn_module("proj", "NodeController.cpp"), "proj.NodeController");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_basic_rs) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "lib.rs", "new"), "proj.lib.new");
|
||||
PASS();
|
||||
@@ -559,6 +591,9 @@ SUITE(fqn) {
|
||||
RUN_TEST(fqn_compute_basic_ts);
|
||||
RUN_TEST(fqn_compute_basic_js);
|
||||
RUN_TEST(fqn_compute_basic_c);
|
||||
RUN_TEST(fqn_file_qn_preserves_dotfile_variants_issue1077);
|
||||
RUN_TEST(fqn_file_qn_distinguishes_same_stem_header_source_issue964);
|
||||
RUN_TEST(fqn_module_qn_still_strips_extension);
|
||||
RUN_TEST(fqn_compute_basic_rs);
|
||||
|
||||
/* fqn_compute: nested paths */
|
||||
|
||||
Reference in new issue
Block a user