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). */
| 1180 | /* Handle QN collision: update dst node fields (src wins), record remap if IDs differ. |
| 1181 | * label/file_path are re-interned into dst's pool (sn's pointers belong to src). */ |
| 1182 | static void merge_update_existing(cbm_gbuf_t *dst, cbm_gbuf_node_t *existing, |
| 1183 | const cbm_gbuf_node_t *sn, CBMHashTable **remap) { |
| 1184 | /* Same guard as cbm_gbuf_upsert_node: a per-file "Module" def coming from a |
| 1185 | * worker-local gbuf must not touch the structural directory node ("Project" |
| 1186 | * root or "Folder") that shares its QN in a directory-based-module language |
| 1187 | * (Java/Go). pass_structure seeds Folder/Project nodes on the MAIN gbuf |
| 1188 | * before the parallel extract, so every worker's always-emitted Module def |
| 1189 | * for that package collides here; unconditional "src wins" relabelled the |
| 1190 | * directory node to Module and set its file_path to whichever worker merged |
| 1191 | * LAST — the nondeterministic USAGE-source misattribution of #787. Keep the |
| 1192 | * structural node intact; the ID remap below still redirects the worker's |
| 1193 | * edges onto the canonical node. */ |
| 1194 | bool module_on_container = |
| 1195 | existing->label && sn->label && strcmp(sn->label, "Module") == 0 && |
| 1196 | (strcmp(existing->label, "Project") == 0 || strcmp(existing->label, "Folder") == 0); |
| 1197 | if (!module_on_container) { |
| 1198 | /* Canonical collision winner (determinism) — mirrors |
| 1199 | * cbm_gbuf_upsert_node exactly. Distinct source entities can share a |
| 1200 | * QN (C: struct/function/macro with one name); unconditional "src |
| 1201 | * wins" made the survivor depend on worker merge order, flickering |
| 1202 | * the node set (and every downstream consumer) run to run. Winner = |
| 1203 | * smallest file_path, then LARGEST start_line, then largest |
| 1204 | * name/label — one total order, commutative, scheduling-free; a full |
| 1205 | * tie is the same entity → refresh from src. */ |
| 1206 | int c = strcmp(sn->file_path ? sn->file_path : "", |
| 1207 | existing->file_path ? existing->file_path : ""); |
| 1208 | if (c == 0) { |
| 1209 | c = existing->start_line - sn->start_line; |
| 1210 | } |
| 1211 | if (c == 0) { |
| 1212 | c = strcmp(existing->name ? existing->name : "", sn->name ? sn->name : ""); |
| 1213 | } |
| 1214 | if (c == 0) { |
| 1215 | c = strcmp(existing->label ? existing->label : "", sn->label ? sn->label : ""); |
| 1216 | } |
| 1217 | bool sn_wins = c <= 0; |
| 1218 | if (sn_wins) { |
| 1219 | /* Keep the secondary indexes consistent when the surviving |
| 1220 | * label/name changes (the old code left the node listed under its |
| 1221 | * original label/name, so find_by_label/name mis-listed it). */ |
| 1222 | const char *new_label = gb_intern(dst, sn->label); |
| 1223 | bool label_changed = |
| 1224 | !existing->label || !new_label || strcmp(existing->label, new_label) != 0; |
| 1225 | bool name_changed = |
| 1226 | !existing->name || !sn->name || strcmp(existing->name, sn->name) != 0; |
| 1227 | if (label_changed) { |
| 1228 | remove_node_from_ptr_array( |
| 1229 | cbm_ht_get(dst->nodes_by_label, existing->label ? existing->label : ""), |
| 1230 | existing->id); |
| 1231 | } |
| 1232 | if (name_changed) { |
| 1233 | remove_node_from_ptr_array( |
| 1234 | cbm_ht_get(dst->nodes_by_name, existing->name ? existing->name : ""), |
| 1235 | existing->id); |
| 1236 | } |
| 1237 | existing->label = (char *)new_label; |
| 1238 | free(existing->name); |
| 1239 | existing->name = heap_strdup(sn->name); |
no test coverage detected