|
| 1 | +/* |
| 2 | + * repro_issue964.c — Reproduce-first case for OPEN bug #964. |
| 3 | + * |
| 4 | + * Issue: #964 — "C/C++ #include not mapped as IMPORTS edges — expected or a |
| 5 | + * bug?" (triaged as a bug: header files look disconnected; the reporter's |
| 6 | + * zero-inbound query returns 194 of 214 files on a PlatformIO project). |
| 7 | + * |
| 8 | + * Root cause (deeper than the report guesses): |
| 9 | + * cbm_pipeline_fqn_compute (src/pipeline/fqn.c) strips the file extension, |
| 10 | + * so a header and its same-stem source collide on BOTH derived QNs: |
| 11 | + * NodeController.h -> module "proj.NodeController", |
| 12 | + * file "proj.NodeController.__file__" |
| 13 | + * NodeController.cpp -> module "proj.NodeController", |
| 14 | + * file "proj.NodeController.__file__" |
| 15 | + * Node upserts match by QN, so the header's File node is merged into the |
| 16 | + * source's — the header has NO node of its own in the graph at all. The |
| 17 | + * `#include "NodeController.h"` import then resolves to the shared module |
| 18 | + * (named after the .cpp), and the header can never receive an inbound |
| 19 | + * IMPORTS edge. Headers without a same-stem sibling keep a node but the |
| 20 | + * include-edge targeting still lands on the module, not the header file. |
| 21 | + * |
| 22 | + * NOTE: the module-QN unification itself is (at least partly) load-bearing |
| 23 | + * for C/C++ — header declarations and source definitions sharing a module |
| 24 | + * QN is what lets cross-file call resolution join them. The fix must give |
| 25 | + * headers their own FILE identity + point include edges at it WITHOUT |
| 26 | + * splitting the shared module QN. Both File-node creation sites (pipeline.c |
| 27 | + * full build; pipeline_incremental.c changed-file re-creation) must agree, |
| 28 | + * and the incremental site is concurrently being fixed by PR #995 (#994) — |
| 29 | + * implement this fix on top of that landing, never in parallel with it. |
| 30 | + * |
| 31 | + * Expected (correct) behaviour: |
| 32 | + * 1. NodeController.h has its own File node (distinct from the .cpp's). |
| 33 | + * 2. main.cpp's `#include "NodeController.h"` produces an IMPORTS edge |
| 34 | + * whose target is the HEADER's node, so the header is not disconnected. |
| 35 | + * |
| 36 | + * Why RED on current code: the header's File node does not exist (merged by |
| 37 | + * QN collision), so both assertions fail. |
| 38 | + */ |
| 39 | + |
| 40 | +#include <foundation/compat.h> |
| 41 | +#include "test_framework.h" |
| 42 | +#include "repro_harness.h" |
| 43 | + |
| 44 | +#include <string.h> |
| 45 | +#include <stdlib.h> |
| 46 | +#include <stdio.h> |
| 47 | + |
| 48 | +/* True if a File-labelled node with exactly `name` exists. */ |
| 49 | +static int r964_file_node_exists(cbm_store_t *store, const char *project, const char *name) { |
| 50 | + cbm_node_t *nodes = NULL; |
| 51 | + int count = 0; |
| 52 | + if (cbm_store_find_nodes_by_label(store, project, "File", &nodes, &count) != CBM_STORE_OK) |
| 53 | + return 0; |
| 54 | + int found = 0; |
| 55 | + for (int i = 0; i < count; i++) { |
| 56 | + if (nodes[i].name && strcmp(nodes[i].name, name) == 0) |
| 57 | + found = 1; |
| 58 | + } |
| 59 | + cbm_store_free_nodes(nodes, count); |
| 60 | + return found; |
| 61 | +} |
| 62 | + |
| 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) |
| 73 | + continue; |
| 74 | + if (tgt.name && strcmp(tgt.name, name) == 0) |
| 75 | + hits++; |
| 76 | + cbm_node_free_fields(&tgt); |
| 77 | + } |
| 78 | + cbm_store_free_edges(edges, n); |
| 79 | + return hits; |
| 80 | +} |
| 81 | + |
| 82 | +TEST(repro_issue964_header_has_node_and_inbound_import) { |
| 83 | + static const RFile files[] = { |
| 84 | + {"NodeController.h", "#pragma once\n" |
| 85 | + "class NodeController {\n" |
| 86 | + "public:\n" |
| 87 | + " void run();\n" |
| 88 | + "};\n"}, |
| 89 | + {"NodeController.cpp", "#include \"NodeController.h\"\n" |
| 90 | + "void NodeController::run() {}\n"}, |
| 91 | + {"main.cpp", "#include \"NodeController.h\"\n" |
| 92 | + "#include <vector>\n" |
| 93 | + "\n" |
| 94 | + "int main() {\n" |
| 95 | + " NodeController c;\n" |
| 96 | + " c.run();\n" |
| 97 | + " return 0;\n" |
| 98 | + "}\n"}}; |
| 99 | + |
| 100 | + RProj lp; |
| 101 | + cbm_store_t *store = rh_index_files(&lp, files, 3); |
| 102 | + ASSERT_NOT_NULL(store); |
| 103 | + |
| 104 | + int header_node = r964_file_node_exists(store, lp.project, "NodeController.h"); |
| 105 | + int header_inbound = r964_inbound_imports(store, lp.project, "NodeController.h"); |
| 106 | + if (!header_node || header_inbound < 1) { |
| 107 | + 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", |
| 110 | + header_node, header_inbound); |
| 111 | + } |
| 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 */ |
| 114 | + |
| 115 | + rh_cleanup(&lp, store); |
| 116 | + PASS(); |
| 117 | +} |
| 118 | + |
| 119 | +SUITE(repro_issue964) { |
| 120 | + RUN_TEST(repro_issue964_header_has_node_and_inbound_import); |
| 121 | +} |
0 commit comments