| 2246 | static void node_fields_free(cbm_node_t *n); /* defined below; used by the stub re-fetch */ |
| 2247 | |
| 2248 | static const char *node_prop(const cbm_node_t *n, const char *prop, cbm_store_t *store) { |
| 2249 | if (!n || !prop) { |
| 2250 | return ""; |
| 2251 | } |
| 2252 | const char *str = node_string_field(n, prop); |
| 2253 | if (str && str[0]) { |
| 2254 | return str; |
| 2255 | } |
| 2256 | /* Note: a string field that exists but is empty ("") falls through here so a |
| 2257 | * WITH-aggregation node stub (below) can re-fetch it. */ |
| 2258 | /* Computed and JSON-derived values live in rotating thread-local buffers: |
| 2259 | * a single row (or an ORDER-BY comparison) reads several of these before any |
| 2260 | * of them is copied out, so returning one shared static buffer would alias |
| 2261 | * every column to the last value read. Mirrors edge_prop's rotation. */ |
| 2262 | static _Thread_local char bufs[CYP_BUF_8][CBM_SZ_512]; |
| 2263 | static _Thread_local int buf_idx = 0; |
| 2264 | char *out = bufs[buf_idx]; |
| 2265 | buf_idx = (buf_idx + SKIP_ONE) % CYP_BUF_8; |
| 2266 | |
| 2267 | if (strcmp(prop, "start_line") == 0) { |
| 2268 | snprintf(out, CBM_SZ_512, "%d", n->start_line); |
| 2269 | return out; |
| 2270 | } |
| 2271 | if (strcmp(prop, "end_line") == 0) { |
| 2272 | snprintf(out, CBM_SZ_512, "%d", n->end_line); |
| 2273 | return out; |
| 2274 | } |
| 2275 | /* Virtual computed properties: in_degree/out_degree via CALLS edges. |
| 2276 | * Enables Cypher dead-code detection: WHERE n.in_degree = '0'. */ |
| 2277 | if (store && (strcmp(prop, "in_degree") == 0 || strcmp(prop, "out_degree") == 0)) { |
| 2278 | int in_deg = 0; |
| 2279 | int out_deg = 0; |
| 2280 | cbm_store_node_degree(store, n->id, &in_deg, &out_deg); |
| 2281 | int val = (strcmp(prop, "in_degree") == 0) ? in_deg : out_deg; |
| 2282 | snprintf(out, CBM_SZ_512, "%d", val); |
| 2283 | return out; |
| 2284 | } |
| 2285 | /* Fall back to any value stored in the node's properties JSON — exposes the |
| 2286 | * extraction metrics (complexity, cognitive, loop_count, loop_depth, |
| 2287 | * transitive_loop_depth, recursive) and any other persisted property to |
| 2288 | * WHERE/RETURN, e.g. WHERE n.loop_depth >= 2. */ |
| 2289 | if (n->properties_json && n->properties_json[0] == '{') { |
| 2290 | const char *v = json_extract_prop(n->properties_json, prop, out, CBM_SZ_512); |
| 2291 | if (v && v[0]) { |
| 2292 | return v; |
| 2293 | } |
| 2294 | } |
| 2295 | /* WITH aggregation carries a node group var by id + name only (the group key |
| 2296 | * is the node name), so every other property is absent on the stub. Detect |
| 2297 | * the stub (id set, but the full string fields were never populated) and |
| 2298 | * re-fetch the node so RETURN g.file_path / g.label / g.<metric> project |
| 2299 | * correctly instead of returning blank. The gate is heuristic, not an exact |
| 2300 | * stub discriminator: a real bound node with NULL label AND file_path would |
| 2301 | * also match, but in that case the worst case is one redundant indexed fetch |
| 2302 | * that returns the same value — never a wrong result. */ |
| 2303 | if (store && n->id > 0 && !n->file_path && !n->label) { |
| 2304 | cbm_node_t full = {0}; |
| 2305 | if (cbm_store_find_node_by_id(store, n->id, &full) == CBM_STORE_OK) { |
no test coverage detected