Handle QN collision: update dst node fields (src wins), record remap if IDs differ. * label/file_path are re-interned into dst's pool (sn's pointers belong to src). */
| 1230 | /* Handle QN collision: update dst node fields (src wins), record remap if IDs differ. |
| 1231 | * label/file_path are re-interned into dst's pool (sn's pointers belong to src). */ |
| 1232 | static void merge_update_existing(cbm_gbuf_t *dst, cbm_gbuf_node_t *existing, |
| 1233 | const cbm_gbuf_node_t *sn, CBMHashTable **remap) { |
| 1234 | /* Same guard as cbm_gbuf_upsert_node: a per-file "Module" def coming from a |
| 1235 | * worker-local gbuf must not touch the structural directory node ("Project" |
| 1236 | * root or "Folder") that shares its QN in a directory-based-module language |
| 1237 | * (Java/Go). pass_structure seeds Folder/Project nodes on the MAIN gbuf |
| 1238 | * before the parallel extract, so every worker's always-emitted Module def |
| 1239 | * for that package collides here; unconditional "src wins" relabelled the |
| 1240 | * directory node to Module and set its file_path to whichever worker merged |
| 1241 | * LAST — the nondeterministic USAGE-source misattribution of #787. Keep the |
| 1242 | * structural node intact; the ID remap below still redirects the worker's |
| 1243 | * edges onto the canonical node. */ |
| 1244 | bool module_on_container = |
| 1245 | existing->label && sn->label && strcmp(sn->label, "Module") == 0 && |
| 1246 | (strcmp(existing->label, "Project") == 0 || strcmp(existing->label, "Folder") == 0); |
| 1247 | if (!module_on_container) { |
| 1248 | /* Canonical collision winner (determinism) — mirrors |
| 1249 | * cbm_gbuf_upsert_node exactly. Distinct source entities can share a |
| 1250 | * QN (C: struct/function/macro with one name); unconditional "src |
| 1251 | * wins" made the survivor depend on worker merge order, flickering |
| 1252 | * the node set (and every downstream consumer) run to run. Winner = |
| 1253 | * smallest file_path, then LARGEST start_line, then largest |
| 1254 | * name/label — one total order, commutative, scheduling-free; a full |
| 1255 | * tie is the same entity → refresh from src. */ |
| 1256 | int c = strcmp(sn->file_path ? sn->file_path : "", |
| 1257 | existing->file_path ? existing->file_path : ""); |
| 1258 | if (c == 0) { |
| 1259 | c = existing->start_line - sn->start_line; |
| 1260 | } |
| 1261 | if (c == 0) { |
| 1262 | c = strcmp(existing->name ? existing->name : "", sn->name ? sn->name : ""); |
| 1263 | } |
| 1264 | if (c == 0) { |
| 1265 | c = strcmp(existing->label ? existing->label : "", sn->label ? sn->label : ""); |
| 1266 | } |
| 1267 | bool sn_wins = c <= 0; |
| 1268 | if (sn_wins) { |
| 1269 | /* Keep the secondary indexes consistent when the surviving |
| 1270 | * label/name changes (the old code left the node listed under its |
| 1271 | * original label/name, so find_by_label/name mis-listed it). */ |
| 1272 | const char *new_label = gb_intern(dst, sn->label); |
| 1273 | bool label_changed = |
| 1274 | !existing->label || !new_label || strcmp(existing->label, new_label) != 0; |
| 1275 | bool name_changed = |
| 1276 | !existing->name || !sn->name || strcmp(existing->name, sn->name) != 0; |
| 1277 | if (label_changed) { |
| 1278 | remove_node_from_ptr_array( |
| 1279 | cbm_ht_get(dst->nodes_by_label, existing->label ? existing->label : ""), |
| 1280 | existing->id); |
| 1281 | } |
| 1282 | if (name_changed) { |
| 1283 | remove_node_from_ptr_array( |
| 1284 | cbm_ht_get(dst->nodes_by_name, existing->name ? existing->name : ""), |
| 1285 | existing->id); |
| 1286 | } |
| 1287 | existing->label = (char *)new_label; |
| 1288 | free(existing->name); |
| 1289 | existing->name = heap_strdup(sn->name); |
no test coverage detected