Skip to content

Commit 90d7315

Browse files
authored
Merge pull request DeusData#1108 from DeusData/fix/964-1077-fqn-collision
fix(fqn): preserve full filename in File-node QNs so sibling files don't collide
2 parents 8483759 + 751752b commit 90d7315

3 files changed

Lines changed: 80 additions & 23 deletions

File tree

‎src/pipeline/fqn.c‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,19 @@ char *cbm_pipeline_fqn_compute(const char *project, const char *rel_path, const
117117

118118
char *path = strdup(rel_path ? rel_path : "");
119119
cbm_normalize_path_sep(path);
120-
strip_file_extension(path);
120+
/* #1077/#964: File-node QNs (name=="__file__") must preserve the full
121+
* filename so sibling files sharing a stem get DISTINCT nodes — e.g.
122+
* .env / .env.local / .env.production (which all strip to ".env" and
123+
* collide, so only one survives per directory), and a C/C++ header vs
124+
* its same-stem .cpp. Extension stripping stays for MODULE/symbol QNs
125+
* (name==NULL or a symbol): that stem unification is load-bearing for
126+
* C/C++ declaration↔definition cross-file resolution. All 26 __file__
127+
* QN sites route through here, so creation and every lookup stay
128+
* consistent. */
129+
bool is_file_qn = name && strcmp(name, "__file__") == 0;
130+
if (!is_file_qn) {
131+
strip_file_extension(path);
132+
}
121133

122134
const char *segments[CBM_SZ_256];
123135
int seg_count = 0;

‎tests/repro/repro_issue964.c‎

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -60,26 +60,36 @@ static int r964_file_node_exists(cbm_store_t *store, const char *project, const
6060
return found;
6161
}
6262

63-
/* Count inbound IMPORTS edges whose TARGET node name is exactly `name`. */
64-
static int r964_inbound_imports(cbm_store_t *store, const char *project, const char *name) {
65-
cbm_edge_t *edges = NULL;
66-
int n = 0;
67-
if (cbm_store_find_edges_by_type(store, project, "IMPORTS", &edges, &n) != CBM_STORE_OK)
68-
return -1;
69-
int hits = 0;
70-
for (int i = 0; i < n; i++) {
71-
cbm_node_t tgt;
72-
if (cbm_store_find_node_by_id(store, edges[i].target_id, &tgt) != CBM_STORE_OK)
63+
/* Count INBOUND edges (any of the reporter's connection types) landing on the
64+
* File node named `name` — the exact disconnection metric from the issue's
65+
* query `NOT EXISTS { (f)<-[:IMPORTS|CALLS|CONTAINS_FILE|DEFINES|USAGE]-() }`.
66+
* A header that used to have no node at all (extension-stripped QN collision)
67+
* now has its own File node AND is connected (CONTAINS_FILE from its folder,
68+
* plus DEFINES of the class it declares); `#include` itself resolves to the
69+
* declared Class node via #983, not to the header File. */
70+
static int r964_file_inbound(cbm_store_t *store, const char *project, const char *name) {
71+
static const char *const types[] = {"IMPORTS", "CALLS", "CONTAINS_FILE",
72+
"DEFINES", "USAGE", NULL};
73+
int total = 0;
74+
for (int t = 0; types[t]; t++) {
75+
cbm_edge_t *edges = NULL;
76+
int n = 0;
77+
if (cbm_store_find_edges_by_type(store, project, types[t], &edges, &n) != CBM_STORE_OK)
7378
continue;
74-
if (tgt.name && strcmp(tgt.name, name) == 0)
75-
hits++;
76-
cbm_node_free_fields(&tgt);
79+
for (int i = 0; i < n; i++) {
80+
cbm_node_t tgt;
81+
if (cbm_store_find_node_by_id(store, edges[i].target_id, &tgt) != CBM_STORE_OK)
82+
continue;
83+
if (tgt.name && strcmp(tgt.name, name) == 0)
84+
total++;
85+
cbm_node_free_fields(&tgt);
86+
}
87+
cbm_store_free_edges(edges, n);
7788
}
78-
cbm_store_free_edges(edges, n);
79-
return hits;
89+
return total;
8090
}
8191

82-
TEST(repro_issue964_header_has_node_and_inbound_import) {
92+
TEST(repro_issue964_header_has_node_and_is_connected) {
8393
static const RFile files[] = {
8494
{"NodeController.h", "#pragma once\n"
8595
"class NodeController {\n"
@@ -102,20 +112,20 @@ TEST(repro_issue964_header_has_node_and_inbound_import) {
102112
ASSERT_NOT_NULL(store);
103113

104114
int header_node = r964_file_node_exists(store, lp.project, "NodeController.h");
105-
int header_inbound = r964_inbound_imports(store, lp.project, "NodeController.h");
115+
int header_inbound = r964_file_inbound(store, lp.project, "NodeController.h");
106116
if (!header_node || header_inbound < 1) {
107117
fprintf(stderr,
108-
" [964] FAIL header_file_node=%d inbound_imports=%d (header merged into "
109-
"same-stem .cpp by extension-stripped QN collision)\n",
118+
" [964] FAIL header_file_node=%d inbound_edges=%d (header merged into "
119+
"same-stem .cpp by extension-stripped QN collision, or left disconnected)\n",
110120
header_node, header_inbound);
111121
}
112-
ASSERT_TRUE(header_node); /* header must keep its own File node */
113-
ASSERT_TRUE(header_inbound >= 1); /* #include must land an edge on it */
122+
ASSERT_TRUE(header_node); /* header keeps its own File node (not merged) */
123+
ASSERT_TRUE(header_inbound >= 1); /* and is connected (not the reporter's zero-inbound) */
114124

115125
rh_cleanup(&lp, store);
116126
PASS();
117127
}
118128

119129
SUITE(repro_issue964) {
120-
RUN_TEST(repro_issue964_header_has_node_and_inbound_import);
130+
RUN_TEST(repro_issue964_header_has_node_and_is_connected);
121131
}

‎tests/test_fqn.c‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,38 @@ TEST(fqn_compute_basic_c) {
5252
PASS();
5353
}
5454

55+
/* #1077/#964: File-node QNs (name=="__file__") must preserve the FULL filename
56+
* so sibling files sharing a stem get DISTINCT nodes. Extension stripping is
57+
* kept for module/symbol QNs (name!=__file__) — load-bearing for C/C++
58+
* declaration↔definition resolution. */
59+
TEST(fqn_file_qn_preserves_dotfile_variants_issue1077) {
60+
/* .env / .env.local / .env.production all stripped to ".env" before,
61+
* colliding so only one File node survived per directory. */
62+
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", ".env", "__file__"), "proj..env.__file__");
63+
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", ".env.local", "__file__"),
64+
"proj..env.local.__file__");
65+
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", ".env.production", "__file__"),
66+
"proj..env.production.__file__");
67+
PASS();
68+
}
69+
70+
TEST(fqn_file_qn_distinguishes_same_stem_header_source_issue964) {
71+
/* NodeController.h and NodeController.cpp both stripped to
72+
* "NodeController", so the header's File node was merged into the .cpp's. */
73+
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "NodeController.h", "__file__"),
74+
"proj.NodeController.h.__file__");
75+
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "NodeController.cpp", "__file__"),
76+
"proj.NodeController.cpp.__file__");
77+
PASS();
78+
}
79+
80+
TEST(fqn_module_qn_still_strips_extension) {
81+
/* The MODULE/symbol QN keeps stripping — unchanged by the File-QN fix. */
82+
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "core.c", "init"), "proj.core.init");
83+
ASSERT_FQN(cbm_pipeline_fqn_module("proj", "NodeController.cpp"), "proj.NodeController");
84+
PASS();
85+
}
86+
5587
TEST(fqn_compute_basic_rs) {
5688
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "lib.rs", "new"), "proj.lib.new");
5789
PASS();
@@ -559,6 +591,9 @@ SUITE(fqn) {
559591
RUN_TEST(fqn_compute_basic_ts);
560592
RUN_TEST(fqn_compute_basic_js);
561593
RUN_TEST(fqn_compute_basic_c);
594+
RUN_TEST(fqn_file_qn_preserves_dotfile_variants_issue1077);
595+
RUN_TEST(fqn_file_qn_distinguishes_same_stem_header_source_issue964);
596+
RUN_TEST(fqn_module_qn_still_strips_extension);
562597
RUN_TEST(fqn_compute_basic_rs);
563598

564599
/* fqn_compute: nested paths */

0 commit comments

Comments
 (0)