Files
codebase-memory-mcp/src/pipeline/pass_definitions.c
T

1232 lines
54 KiB
C

/*
* pass_definitions.c — Extract definitions from source files.
*
* For each discovered file:
* 1. Read source content from disk
* 2. Call cbm_extract_file() to get defs, calls, imports
* 3. Create Function/Class/Method/Variable/Module nodes in graph buffer
* 4. Register callables in the function registry
* 5. Store import maps and call sites for later passes
*
* Depends on: extraction layer (cbm.h), graph_buffer, pipeline internals
*/
#include "foundation/constants.h"
enum { PD_RING = 4, PD_RING_MASK = 3, PD_JSON_MARGIN = 10, PD_ESC_MARGIN = 3, PD_ESC_SPACE = 2 };
/* Fixed bytes around a serialized JSON field: ,"key":"value" / ,"key":[...]
* -> comma + 2 key quotes + colon + 2 value quotes (resp. brackets). */
enum { PD_JSON_FIELD_OVERHEAD = 6 };
#include "pipeline/pipeline.h"
#include <stdint.h>
#include <ctype.h>
#include "pipeline/pipeline_internal.h"
#include "graph_buffer/graph_buffer.h"
#include "foundation/log.h"
#include "foundation/mem_core.h" /* cbm_alloc/cbm_free: the grown properties buffer */
#include "foundation/compat.h"
#include "foundation/compat_fs.h"
#include "foundation/limits.h"
#include "foundation/mem_core.h"
#include "foundation/str_util.h"
#include "cbm.h"
#include "arena.h"
#include "iris_export_xml.h"
#include "simhash/minhash.h"
#include "semantic/ast_profile.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Read entire file into heap-allocated buffer. Returns NULL on error.
* Caller must free(). Sets *out_len to byte count. *out_size receives the
* on-disk size and *out_status the failure reason, so the caller can attribute
* a skip to the right phase/reason (read vs oversized) instead of a silent
* drop. Both out params may be NULL. */
static char *read_file(const char *path, int *out_len, long *out_size,
cbm_read_status_t *out_status) {
if (out_size) {
*out_size = 0;
}
if (out_status) {
*out_status = CBM_READ_OK;
}
FILE *f = cbm_fopen(path, "rb");
if (!f) {
if (out_status) {
*out_status = CBM_READ_OPEN_FAIL;
}
return NULL;
}
(void)fseek(f, 0, SEEK_END);
long size = ftell(f);
(void)fseek(f, 0, SEEK_SET);
if (out_size) {
*out_size = size;
}
if (size <= 0) {
(void)fclose(f);
if (out_status) {
*out_status = CBM_READ_EMPTY;
}
return NULL;
}
if (size > cbm_max_file_bytes()) { /* generous, env-configurable cap (B4) */
(void)fclose(f);
if (out_status) {
*out_status = CBM_READ_OVERSIZED;
}
return NULL;
}
/* +16 padding: tree-sitter's lexer peeks a few bytes past the final UTF-8
* character when computing lookahead, reading beyond the logical end.
* Over-allocate and zero the tail so that read stays in-bounds (ASan
* flags it as a heap-buffer-overflow otherwise; harmless but real UB). */
enum { CBM_TS_LOOKAHEAD_PAD = 16 };
char *buf = malloc((size_t)size + CBM_TS_LOOKAHEAD_PAD);
if (!buf) {
(void)fclose(f);
if (out_status) {
*out_status = CBM_READ_OOM;
}
return NULL;
}
size_t nread = fread(buf, SKIP_ONE, size, f);
(void)fclose(f);
if (nread > (size_t)size) {
nread = (size_t)size;
}
memset(buf + nread, 0, CBM_TS_LOOKAHEAD_PAD);
*out_len = (int)nread;
return buf;
}
/* Format int to string for logging. Thread-safe via TLS. */
static const char *itoa_log(int val) {
static CBM_TLS char bufs[PD_RING][CBM_SZ_32];
static CBM_TLS int idx = 0;
int i = idx;
idx = (idx + SKIP_ONE) & PD_RING_MASK;
snprintf(bufs[i], sizeof(bufs[i]), "%d", val);
return bufs[i];
}
/* Append a JSON-escaped string value to buf at position *pos.
* Writes: ,"key":"escaped_value"
* Handles: \, ", \n, \r, \t */
static int def_json_escape_char(char *buf, size_t avail, char ch) {
char esc = 0;
switch (ch) {
case '"':
esc = '"';
break;
case '\\':
esc = '\\';
break;
case '\n':
esc = 'n';
break;
case '\r':
esc = 'r';
break;
case '\t':
esc = 't';
break;
default:
if (avail >= SKIP_ONE) {
/* Any other raw control byte (e.g. form feed) is invalid inside a
* JSON string — degrade to a space. */
buf[0] = ((unsigned char)ch < 0x20) ? ' ' : ch;
}
return SKIP_ONE;
}
if (avail >= PD_ESC_SPACE) {
buf[0] = '\\';
buf[SKIP_ONE] = esc;
}
return PD_ESC_SPACE;
}
/* Escaped length of a string under def_json_escape_char's rules: escaped
* characters expand to 2 bytes, everything else stays 1. */
static size_t def_json_escaped_len(const char *s) {
size_t n = 0;
for (; *s; s++) {
switch (*s) {
case '"':
case '\\':
case '\n':
case '\r':
case '\t':
n += PD_ESC_SPACE;
break;
default:
n += SKIP_ONE;
}
}
return n;
}
/* Appends are ATOMIC: a field is emitted only if the WHOLE serialized form
* fits (with PD_ESC_SPACE bytes reserved for the closing '}' + NUL). Cutting a
* field mid-value produced unterminated strings/arrays — malformed properties
* JSON that aborts every json_extract()-based consumer downstream (seen on the
* Linux kernel: 50-param functions truncated at the 2 KB cap). Dropping an
* oversized optional field whole keeps the JSON valid. */
static void append_json_string(char *buf, size_t bufsize, size_t *pos, const char *key,
const char *val) {
if (!val || val[0] == '\0') {
return;
}
/* ,"key":"<escaped>" — comma + 2 key quotes + colon + 2 value quotes */
size_t required = strlen(key) + def_json_escaped_len(val) + PD_JSON_FIELD_OVERHEAD;
if (*pos + required + PD_ESC_SPACE > bufsize) {
return; /* whole field would not fit — skip it atomically */
}
size_t p = *pos;
int w = snprintf(buf + p, bufsize - p, ",\"%s\":\"", key);
if (w <= 0 || (size_t)w >= bufsize - p) {
return;
}
p += (size_t)w;
for (const char *s = val; *s && p < bufsize - PD_ESC_MARGIN; s++) {
p += (size_t)def_json_escape_char(buf + p, bufsize - p - PD_ESC_SPACE, *s);
}
if (p < bufsize - SKIP_ONE) {
buf[p++] = '"';
}
buf[p] = '\0';
*pos = p;
}
/* Append an already-serialized JSON value verbatim: ,"key":<json>. Atomic like
* append_json_string. For `variants`, which extraction builds as a JSON array
* of objects (cbm.h). Twin of pass_parallel.c -- keep both in sync. */
static void append_json_raw(char *buf, size_t bufsize, size_t *pos, const char *key,
const char *json) {
if (!json || json[0] == '\0') {
return;
}
size_t required = strlen(key) + strlen(json) + PD_JSON_FIELD_OVERHEAD;
if (*pos + required + PD_ESC_SPACE > bufsize) {
return; /* whole field would not fit — skip it atomically */
}
int w = snprintf(buf + *pos, bufsize - *pos, ",\"%s\":%s", key, json);
if (w > 0 && (size_t)w < bufsize - *pos) {
*pos += (size_t)w;
}
}
/* Append a JSON array of strings: ,"key":["a","b","c"]. Atomic like
* append_json_string: emitted only if the whole array fits. */
static void append_json_str_array(char *buf, size_t bufsize, size_t *pos, const char *key,
const char **arr) {
if (!arr || !arr[0] || *pos >= bufsize - PD_JSON_MARGIN) {
return;
}
/* ,"key":[ + per item "<escaped>" + separating commas + ] */
size_t required = strlen(key) + PD_JSON_FIELD_OVERHEAD;
for (int i = 0; arr[i]; i++) {
required += def_json_escaped_len(arr[i]) + PD_ESC_SPACE + (i > 0 ? SKIP_ONE : 0);
}
if (*pos + required + PD_ESC_SPACE > bufsize) {
return; /* whole array would not fit — skip it atomically */
}
size_t p = *pos;
int n = snprintf(buf + p, bufsize - p, ",\"%s\":[", key);
if (n <= 0 || p + (size_t)n >= bufsize - PD_ESC_SPACE) {
return;
}
p += (size_t)n;
for (int i = 0; arr[i]; i++) {
if (i > 0 && p < bufsize - SKIP_ONE) {
buf[p++] = ',';
}
if (p < bufsize - SKIP_ONE) {
buf[p++] = '"';
}
/* Full escaping (not just quote/backslash): items like C param types
* sliced from multi-line declarations carry raw \n/\t bytes, which are
* invalid inside JSON strings. */
for (const char *s = arr[i]; *s && p < bufsize - PD_ESC_SPACE; s++) {
p += (size_t)def_json_escape_char(buf + p, bufsize - p - PD_ESC_SPACE, *s);
}
if (p < bufsize - SKIP_ONE) {
buf[p++] = '"';
}
}
if (p < bufsize - SKIP_ONE) {
buf[p++] = ']';
}
buf[p] = '\0';
*pos = p;
}
/* Build properties JSON for a definition node. */
static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def) {
/* The complexity/loop/recursion metrics are only meaningful for executable
* units (Function/Method). Emitting them on the millions of Macro/Field/
* Variable/Class/Enum nodes — where they are always zero — bloats every
* node's properties (~150 B), inflating RAM, the gbuf merge copy and the
* dump. Gate the block to functions; other labels keep the lean base. */
const bool is_fn =
def->label && (strcmp(def->label, "Function") == 0 || strcmp(def->label, "Method") == 0);
const char *test_role = def->test_role == CBM_TEST_ROLE_CASE ? ",\"test_role\":\"case\""
: def->test_role == CBM_TEST_ROLE_SUITE ? ",\"test_role\":\"suite\""
: "";
int n;
if (is_fn) {
n = snprintf(buf, bufsize,
"{\"complexity\":%d,\"cognitive\":%d,\"loop_count\":%d,\"loop_depth\":%d,"
"\"self_recursive\":%s,\"param_count\":%d,\"max_access_depth\":%d,"
"\"linear_scan_in_loop\":%d,\"alloc_in_loop\":%d,\"recursion_in_loop\":%s,"
"\"unguarded_recursion\":%s,"
"\"lines\":%d,\"is_exported\":%s,\"is_test\":%s,\"is_entry_point\":%s%s",
def->complexity, def->cognitive, def->loop_count, def->loop_depth,
def->is_recursive ? "true" : "false", def->param_count, def->max_access_depth,
def->linear_scan_in_loop, def->alloc_in_loop,
def->recursion_in_loop ? "true" : "false",
def->unguarded_recursion ? "true" : "false", def->lines,
def->is_exported ? "true" : "false", def->is_test ? "true" : "false",
def->is_entry_point ? "true" : "false", test_role);
} else {
n = snprintf(buf, bufsize,
"{\"complexity\":%d,\"lines\":%d,\"is_exported\":%s,\"is_test\":%s,"
"\"is_entry_point\":%s%s",
def->complexity, def->lines, def->is_exported ? "true" : "false",
def->is_test ? "true" : "false", def->is_entry_point ? "true" : "false",
test_role);
}
if (n <= 0 || (size_t)n >= bufsize) {
buf[0] = '\0';
return;
}
size_t pos = (size_t)n;
append_json_string(buf, bufsize, &pos, "docstring", def->docstring);
/* The buffer is sized for every field below (pd_props_buf), so none can be
* squeezed out. */
append_json_raw(buf, bufsize, &pos, "variants", def->variants);
append_json_string(buf, bufsize, &pos, "variant_key", def->variant_key);
append_json_string(buf, bufsize, &pos, "signature", def->signature);
append_json_string(buf, bufsize, &pos, "return_type", def->return_type);
append_json_string(buf, bufsize, &pos, "parent_class", def->parent_class);
append_json_str_array(buf, bufsize, &pos, "decorators", def->decorators);
append_json_str_array(buf, bufsize, &pos, "base_classes", def->base_classes);
append_json_str_array(buf, bufsize, &pos, "param_names", def->param_names);
append_json_str_array(buf, bufsize, &pos, "param_types", def->param_types);
append_json_string(buf, bufsize, &pos, "route_path", def->route_path);
append_json_string(buf, bufsize, &pos, "route_method", def->route_method);
append_json_string(buf, bufsize, &pos, "http_client", def->http_client);
append_json_string(buf, bufsize, &pos, "http_base_url", def->http_base_url);
/* A document definition's facts (an ADR's id, status, date, ...): key, value pairs. */
for (const char **kv = def->extra_props; kv && kv[0] && kv[SKIP_ONE]; kv += PAIR_LEN) {
append_json_string(buf, bufsize, &pos, kv[0], kv[SKIP_ONE]);
}
/* MinHash fingerprint — append if present and buffer has room. */
if (def->fingerprint && def->fingerprint_k > 0 &&
pos + CBM_MINHASH_HEX_LEN + CBM_MINHASH_JSON_OVERHEAD < bufsize) {
char fp_hex[CBM_MINHASH_HEX_BUF];
cbm_minhash_to_hex((const cbm_minhash_t *)def->fingerprint, fp_hex, sizeof(fp_hex));
append_json_string(buf, bufsize, &pos, "fp", fp_hex);
}
/* AST structural profile */
if (def->structural_profile && pos + CBM_AST_PROFILE_BUF < bufsize) {
append_json_string(buf, bufsize, &pos, "sp", def->structural_profile);
}
/* Body tokens */
if (def->body_tokens && pos + CBM_SZ_512 < bufsize) {
append_json_string(buf, bufsize, &pos, "bt", def->body_tokens);
}
if (pos < bufsize - SKIP_ONE) {
buf[pos] = '}';
buf[pos + SKIP_ONE] = '\0';
}
}
/* Upper bound of build_def_props' fixed prefix: the Function/Method metrics
* literal (about 330 B) with nine ints and seven true/false/test_role strings at
* their widest stays under 600 B. */
enum { PD_PROPS_PREFIX_MAX = CBM_SZ_1K };
/* Serialized size of one optional field as append_json_string emits it
* (0 when the field is absent). */
static size_t pd_str_field_len(const char *key, const char *val) {
return val && val[0] ? strlen(key) + def_json_escaped_len(val) + PD_JSON_FIELD_OVERHEAD : 0;
}
/* Serialized size of a string array as append_json_str_array emits it. */
static size_t pd_arr_field_len(const char *key, const char **arr) {
if (!arr || !arr[0]) {
return 0;
}
size_t n = strlen(key) + PD_JSON_FIELD_OVERHEAD;
for (int i = 0; arr[i]; i++) {
n += def_json_escaped_len(arr[i]) + PD_ESC_SPACE + (i > 0 ? SKIP_ONE : 0);
}
return n;
}
/* A def's properties buffer, sized so EVERY field build_def_props appends fits:
* the fixed metrics prefix (PD_PROPS_PREFIX_MAX) plus each optional field's
* whole serialized form. A field that does not fit is dropped whole, so a buffer
* sized for only some of them let a long signature, decorator list or parameter
* list squeeze out the MinHash fingerprint ("fp"), structural profile ("sp") and
* body tokens ("bt") behind them: no SIMILAR_TO edge and fewer
* SEMANTICALLY_RELATED edges for exactly the widest definitions (jellyfin's
* ImageController actions, 2026-10-18). fp/sp/bt are appended behind their own
* room checks, so both the check's reserve and the field are counted. Returns
* `stack` when everything fits it, or when the larger buffer cannot be
* allocated. Twin of pass_parallel.c -- keep both in sync. */
static char *pd_props_buf(const CBMDefinition *def, char *stack, size_t *size) {
size_t need = PD_PROPS_PREFIX_MAX + pd_str_field_len("docstring", def->docstring);
if (def->variants && def->variants[0]) {
need += strlen("variants") + strlen(def->variants) + PD_JSON_FIELD_OVERHEAD;
}
need += pd_str_field_len("variant_key", def->variant_key) +
pd_str_field_len("signature", def->signature) +
pd_str_field_len("return_type", def->return_type) +
pd_str_field_len("parent_class", def->parent_class) +
pd_arr_field_len("decorators", def->decorators) +
pd_arr_field_len("base_classes", def->base_classes) +
pd_arr_field_len("param_names", def->param_names) +
pd_arr_field_len("param_types", def->param_types) +
pd_str_field_len("route_path", def->route_path) +
pd_str_field_len("route_method", def->route_method) +
pd_str_field_len("http_client", def->http_client) +
pd_str_field_len("http_base_url", def->http_base_url);
for (const char **kv = def->extra_props; kv && kv[0] && kv[SKIP_ONE]; kv += PAIR_LEN) {
need += pd_str_field_len(kv[0], kv[SKIP_ONE]);
}
if (def->fingerprint && def->fingerprint_k > 0) {
need += CBM_MINHASH_HEX_LEN + CBM_MINHASH_JSON_OVERHEAD + SKIP_ONE;
}
if (def->structural_profile) {
need += CBM_AST_PROFILE_BUF + pd_str_field_len("sp", def->structural_profile) + SKIP_ONE;
}
if (def->body_tokens) {
need += CBM_SZ_512 + pd_str_field_len("bt", def->body_tokens) + SKIP_ONE;
}
need += PD_ESC_SPACE; /* the closing '}' + NUL */
if (need <= *size) {
return stack;
}
char *buf = cbm_alloc(CBM_MEM_CLASS_GBUF_STRING, need);
if (!buf) {
return stack;
}
*size = need;
return buf;
}
/* Add a file's own doc (Go package comment, Rust inner docs) to its File
* node as "docstring". Twin of pass_parallel.c -- keep both in sync. */
static void pd_add_file_doc(const cbm_gbuf_node_t *file_node, const char *doc) {
if (!file_node || !doc || !doc[0]) {
return;
}
const char *old = file_node->properties_json ? file_node->properties_json : "{}";
size_t olen = strlen(old);
if (olen < PAIR_LEN || old[olen - SKIP_ONE] != '}') {
return; /* not a JSON object -- leave it untouched */
}
size_t cap = olen + strlen("docstring") + def_json_escaped_len(doc) + PD_JSON_FIELD_OVERHEAD +
PD_ESC_SPACE + SKIP_ONE;
char *neu = cbm_alloc(CBM_MEM_CLASS_GBUF_STRING, cap);
if (!neu) {
return;
}
size_t pos = olen - SKIP_ONE; /* without the closing brace */
memcpy(neu, old, pos);
neu[pos] = '\0';
append_json_string(neu, cap, &pos, "docstring", doc);
if (olen == PAIR_LEN && pos > PAIR_LEN) { /* "{}": drop the leading comma */
memmove(neu + SKIP_ONE, neu + PAIR_LEN, pos - SKIP_ONE);
pos--;
}
neu[pos++] = '}';
neu[pos] = '\0';
(void)cbm_gbuf_node_set_properties_json((cbm_gbuf_node_t *)file_node, neu);
cbm_free(CBM_MEM_CLASS_GBUF_STRING, neu);
}
/* DEFINES_METHOD from a method's type, and the parent edge of a diagram
* element. Both look the parent up by QN, so the parent's node must exist. */
static void link_def_parents(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, int64_t node_id) {
if (node_id <= 0) {
return;
}
if (def->parent_class && def->label && strcmp(def->label, "Method") == 0) {
const cbm_gbuf_node_t *parent = cbm_gbuf_find_by_qn(ctx->gbuf, def->parent_class);
if (parent) {
cbm_gbuf_insert_edge(ctx->gbuf, parent->id, node_id, "DEFINES_METHOD", "{}");
}
}
cbm_pipeline_diagram_parent_edge(ctx, def, node_id);
}
/* Process one definition: create node, register, DEFINES edge. `file_doc`,
* the file's own doc (passed with its first def only), goes on the File node.
* `link_parents` links its DEFINES_METHOD / diagram parent right away; with
* the result cache the pass links them once every file is registered. */
static void process_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const char *rel,
const char *file_doc, CBMLanguage lang, size_t module_prefix_len,
bool link_parents) {
if (!def->qualified_name || !def->name) {
return;
}
char stack[CBM_SZ_2K];
size_t props_size = sizeof(stack);
char *props = pd_props_buf(def, stack, &props_size);
build_def_props(props, props_size, def);
const char *def_rel = def->file_path ? def->file_path : rel;
char marked[CBM_SZ_4K];
const char *qn = def->label && strcmp(def->label, "Module") == 0
? cbm_stem_marks_module_qn(ctx->stem_marks, def->qualified_name, def_rel,
lang, marked, sizeof(marked))
: def->qualified_name;
int64_t node_id =
cbm_gbuf_upsert_node(ctx->gbuf, def->label ? def->label : "Function", def->name, qn,
def_rel, (int)def->start_line, (int)def->end_line, props);
if (props != stack) {
cbm_free(CBM_MEM_CLASS_GBUF_STRING, props);
}
/* Per definition, like pass_parallel.c: overloads sharing this QN each keep
* their own route (the merged node properties hold only one of them). */
if (node_id > 0) {
cbm_pipeline_emit_def_route(ctx->gbuf, node_id, def, def_rel);
}
/* Registry membership is defined ONCE by cbm_label_is_registry_symbol
* (helpers.c): callables + type-like containers (INHERITS/IMPLEMENTS/method/
* field resolution), Variable/Field (READS/WRITES resolution), and Table/View
* (SQL FROM/JOIN lineage). pass_parallel.c and pipeline_incremental.c seed
* through the same predicate, so the three registries cannot diverge.
* A generated member is registered by qualified name only, at all three. */
if (node_id > 0 && cbm_label_is_registry_symbol(def->label)) {
if (cbm_def_props_generated(def->extra_props)) {
cbm_registry_add_qn_only(ctx->registry, def->qualified_name, def->label);
} else {
cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name,
def->label, lang, module_prefix_len);
}
}
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__");
const cbm_gbuf_node_t *file_node = cbm_gbuf_find_by_qn(ctx->gbuf, file_qn);
pd_add_file_doc(file_node, file_doc);
if (file_node && node_id > 0) {
cbm_gbuf_insert_edge(ctx->gbuf, file_node->id, node_id, "DEFINES", "{}");
}
free(file_qn);
if (link_parents) {
link_def_parents(ctx, def, node_id);
}
}
/* Parent links of the definitions cached in Phase 1, made once EVERY file's
* nodes exist. A method is often defined in a file the directory walk returns
* before its type's file (a Go method in another file of its package, an
* out-of-line C++ method in foo.cpp before foo.h); linking it while files were
* still being registered found no parent, so the sequential pipeline lost the
* edge -- and the IMPLEMENTS / OVERRIDE edges derived from it -- that the
* parallel registry build (pass_parallel.c register_and_link_def) keeps. */
static void link_cached_def_parents(cbm_pipeline_ctx_t *ctx, CBMFileResult **cache,
int file_count) {
for (int i = 0; i < file_count; i++) {
if (cbm_pipeline_check_cancel(ctx)) {
return;
}
const CBMFileResult *result = cache[i];
if (!result) {
continue;
}
for (int d = 0; d < result->defs.count; d++) {
const CBMDefinition *def = &result->defs.items[d];
if (!def->parent_class || !def->qualified_name) {
continue;
}
const cbm_gbuf_node_t *node = cbm_gbuf_find_by_qn(ctx->gbuf, def->qualified_name);
if (node) {
link_def_parents(ctx, def, node->id);
}
}
}
}
/* Create Channel nodes + EMITS / LISTENS_ON edges for one file's channels.
* Mirrors the parallel path in cbm_build_registry_from_cache — keep in sync. */
/* Find the source node for a channel edge: enclosing function or file node. */
static const cbm_gbuf_node_t *find_channel_source(cbm_pipeline_ctx_t *ctx, const CBMChannel *ch,
const char *rel) {
const cbm_gbuf_node_t *node = NULL;
if (ch->enclosing_func_qn && ch->enclosing_func_qn[0]) {
node = cbm_gbuf_find_by_qn(ctx->gbuf, ch->enclosing_func_qn);
}
if (!node) {
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__");
node = cbm_gbuf_find_by_qn(ctx->gbuf, file_qn);
free(file_qn);
}
return node;
}
static void create_channel_edges_for_file(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result,
const char *rel) {
for (int j = 0; j < result->channels.count; j++) {
const CBMChannel *ch = &result->channels.items[j];
if (!ch->channel_name || !ch->channel_name[0]) {
continue;
}
char channel_qn[CBM_SZ_512];
snprintf(channel_qn, sizeof(channel_qn), "__channel__%s__%s",
ch->transport ? ch->transport : "unknown", ch->channel_name);
char channel_props[CBM_SZ_512];
snprintf(channel_props, sizeof(channel_props), "{\"transport\":\"%s\",\"name\":\"%s\"}",
ch->transport ? ch->transport : "unknown", ch->channel_name);
int64_t channel_id = cbm_gbuf_upsert_node(ctx->gbuf, "Channel", ch->channel_name,
channel_qn, "", 0, 0, channel_props);
const cbm_gbuf_node_t *src_node = find_channel_source(ctx, ch, rel);
if (src_node && channel_id > 0) {
const char *edge_type = ch->direction == CBM_CHANNEL_EMIT ? "EMITS" : "LISTENS_ON";
char edge_props[CBM_SZ_128];
snprintf(edge_props, sizeof(edge_props), "{\"transport\":\"%s\"}",
ch->transport ? ch->transport : "unknown");
cbm_gbuf_insert_edge(ctx->gbuf, src_node->id, channel_id, edge_type, edge_props);
}
}
}
/* Create CONFIGURES edges for one file's env accesses. extract_env_accesses.c
* records every os.Getenv / process.env / Environment.GetEnvironmentVariable
* style access into result->env_accesses. We materialize one EnvVar node per
* env key and link the enclosing function (or the file node) CONFIGURES-> it,
* so environment-driven configuration is visible even when the accessor is a
* stdlib symbol that never resolves to an in-graph callee. */
int cbm_pipeline_create_env_configures_for_file(cbm_pipeline_ctx_t *ctx,
const CBMFileResult *result, const char *rel) {
int count = 0;
char *file_qn = NULL;
const cbm_gbuf_node_t *file_node = NULL;
for (int j = 0; j < result->env_accesses.count; j++) {
const CBMEnvAccess *ea = &result->env_accesses.items[j];
if (!ea->env_key || !ea->env_key[0]) {
continue;
}
char env_qn[CBM_SZ_512];
snprintf(env_qn, sizeof(env_qn), "__env__%s", ea->env_key);
char env_props[CBM_SZ_512];
snprintf(env_props, sizeof(env_props), "{\"env_key\":\"%s\"}", ea->env_key);
int64_t env_id =
cbm_gbuf_upsert_node(ctx->gbuf, "EnvVar", ea->env_key, env_qn, "", 0, 0, env_props);
if (env_id <= 0) {
continue;
}
const cbm_gbuf_node_t *src = NULL;
if (ea->enclosing_func_qn && ea->enclosing_func_qn[0]) {
src = cbm_gbuf_find_by_qn(ctx->gbuf, ea->enclosing_func_qn);
/* A class-level env access in a directory-module language carries
* the DIRECTORY module QN, which hits the shared Folder/Project
* node — attribute to this file's File node instead (#787, #842). */
if (cbm_pipeline_node_is_dir_container(src)) {
src = NULL;
}
}
if (!src) {
if (!file_qn) {
file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__");
file_node = cbm_gbuf_find_by_qn(ctx->gbuf, file_qn);
}
src = file_node;
}
if (src && src->id != env_id) {
cbm_gbuf_insert_edge(ctx->gbuf, src->id, env_id, "CONFIGURES",
"{\"strategy\":\"env_access\"}");
count++;
}
}
free(file_qn);
return count;
}
/* Create IMPORTS edges for one file's imports. Mirrors the resolution
* logic in pass_parallel.c register_and_link_def — keep the two in sync. */
static int create_import_edges_for_file(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result,
const char *rel, CBMHashTable *namespace_map) {
int count = 0;
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__");
const cbm_gbuf_node_t *source_node = cbm_gbuf_find_by_qn(ctx->gbuf, file_qn);
if (!source_node) {
free(file_qn);
return 0;
}
if (cbm_language_for_filename(rel) == CBM_LANG_PHP) {
count = cbm_pipeline_php_create_import_edges(ctx, result, rel, file_qn, source_node,
namespace_map);
} else {
for (int j = 0; j < result->imports.count; j++) {
const CBMImport *imp = &result->imports.items[j];
if (!imp->module_path) {
continue;
}
const cbm_gbuf_node_t *target =
cbm_pipeline_resolve_import_node(ctx, rel, file_qn, imp, namespace_map);
if (target && target->id != source_node->id) {
char esc_ln[CBM_SZ_128];
cbm_json_escape(esc_ln, sizeof(esc_ln), imp->local_name ? imp->local_name : "");
char imp_props[CBM_SZ_256];
snprintf(imp_props, sizeof(imp_props), "{\"local_name\":\"%s\"}", esc_ln);
cbm_gbuf_insert_edge(ctx->gbuf, source_node->id, target->id, "IMPORTS", imp_props);
count++;
}
}
}
free(file_qn);
return count;
}
static bool objectscript_export_append_strings(CBMArena *arena, const char ***dst,
const char *const *src) {
if (!src) {
return true;
}
int old_count = 0;
int add_count = 0;
while (*dst && (*dst)[old_count]) {
old_count++;
}
while (src[add_count]) {
add_count++;
}
const char **items = (const char **)cbm_arena_alloc(arena, (size_t)(old_count + add_count + 1) *
sizeof(const char *));
if (!items) {
return false;
}
for (int i = 0; i < old_count; i++) {
items[i] = (*dst)[i];
}
for (int i = 0; i < add_count; i++) {
items[old_count + i] = src[i];
}
items[old_count + add_count] = NULL;
*dst = items;
return true;
}
#define OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, field, push_fn) \
do { \
int expected_count = (aggregate)->field.count + (part)->field.count; \
for (int item_i = 0; item_i < (part)->field.count; item_i++) { \
push_fn(&(aggregate)->field, &(aggregate)->arena, (part)->field.items[item_i]); \
} \
if ((aggregate)->field.count != expected_count) { \
return false; \
} \
} while (0)
static bool objectscript_export_append_primary_arrays(CBMFileResult *aggregate,
const CBMFileResult *part) {
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, defs, cbm_defs_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, calls, cbm_calls_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, imports, cbm_imports_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, usages, cbm_usages_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, throws, cbm_throws_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, rw, cbm_rw_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, type_refs, cbm_typerefs_push);
return true;
}
static bool objectscript_export_append_secondary_arrays(CBMFileResult *aggregate,
const CBMFileResult *part) {
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, env_accesses, cbm_envaccess_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, type_assigns, cbm_typeassign_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, impl_traits, cbm_impltrait_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, resolved_calls, cbm_resolvedcall_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, string_refs, cbm_stringref_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, infra_bindings, cbm_infrabinding_push);
OBJECTSCRIPT_EXPORT_APPEND_ARRAY(aggregate, part, channels, cbm_channels_push);
return true;
}
/* Preserve every generated class's parse diagnostics. The generated UDL
* snippets all map back to one physical Studio Export file, so their compact
* range lists can be concatenated using the ordinary comma separator. */
/* Read the trailing ",+<N>" truncation marker off a range string. Returns the
* number of dropped ranges the marker reports, or 0 when there is no marker,
* and writes the length of the part before the marker to `body_len`. */
static int objectscript_export_split_range_marker(const char *ranges, size_t *body_len) {
size_t len = ranges ? strlen(ranges) : 0;
*body_len = len;
if (len == 0) {
return 0;
}
size_t i = len;
while (i > 0 && isdigit((unsigned char)ranges[i - 1])) {
i--;
}
if (i == len || i == 0 || ranges[i - 1] != '+') {
return 0;
}
size_t marker = i - 1; /* index of '+' */
if (marker > 0 && ranges[marker - 1] == ',') {
marker--; /* drop the separator too */
}
*body_len = marker;
return atoi(ranges + i);
}
/* Join one Studio Export part's ranges onto the aggregate.
*
* One export file can hold several <Class> elements, each parsed separately,
* so their range strings get concatenated. A ",+<N>" truncation marker must
* end up ONCE, at the very end: every reader stops at the first token that is
* not a range, so a marker left in the middle would silently hide every range
* after it. Strip the marker off both sides, join the plain ranges, then add
* one marker back carrying the summed count. */
static bool objectscript_export_append_error_ranges(CBMFileResult *aggregate,
const CBMFileResult *part) {
aggregate->parse_incomplete = aggregate->parse_incomplete || part->parse_incomplete;
aggregate->parse_unusable = aggregate->parse_unusable || part->parse_unusable;
aggregate->error_region_count += part->error_region_count;
if (!part->error_ranges || !part->error_ranges[0]) {
return true;
}
size_t agg_len = 0;
size_t part_len = 0;
int dropped = 0;
const char *agg_body = aggregate->error_ranges;
if (agg_body && agg_body[0]) {
dropped += objectscript_export_split_range_marker(agg_body, &agg_len);
} else {
agg_body = NULL;
}
dropped += objectscript_export_split_range_marker(part->error_ranges, &part_len);
const char *combined = NULL;
if (agg_body && agg_len > 0 && part_len > 0) {
combined = cbm_arena_sprintf(&aggregate->arena, "%.*s,%.*s", (int)agg_len, agg_body,
(int)part_len, part->error_ranges);
} else if (agg_body && agg_len > 0) {
combined = cbm_arena_sprintf(&aggregate->arena, "%.*s", (int)agg_len, agg_body);
} else if (part_len > 0) {
combined = cbm_arena_sprintf(&aggregate->arena, "%.*s", (int)part_len, part->error_ranges);
} else {
combined = cbm_arena_strdup(&aggregate->arena, "");
}
if (!combined) {
return false;
}
if (dropped > 0) {
combined = cbm_arena_sprintf(&aggregate->arena, "%s%s+%d", combined, combined[0] ? "," : "",
dropped);
if (!combined) {
return false;
}
}
aggregate->error_ranges = combined;
return true;
}
#if defined(CBM_COVERAGE_MARKER_TEST_API) && CBM_COVERAGE_MARKER_TEST_API
/* Test seam. This join only fires for a Studio Export file holding several
* <Class> elements where a class overruns the 256-region cap — hard to reach
* through the pipeline, easy to get wrong, and a wrong result hides ranges
* without saying so. Expose the join so the marker rules can be pinned. */
bool cbm_pipeline_coverage_marker_test_join(CBMFileResult *aggregate, const CBMFileResult *part) {
return objectscript_export_append_error_ranges(aggregate, part);
}
#endif
/* Studio Export files may contain multiple <Class> elements, while the
* pipeline cache has one slot per physical file. Extract each generated UDL
* class independently (preserving the upstream parser behavior), then compose
* every extracted carrier into one result for the normal registry/call/usage/
* semantic passes. */
static CBM_TLS cbm_objectscript_export_extractor_t objectscript_export_test_extractor;
void cbm_pipeline_set_objectscript_export_extractor_for_tests(
cbm_objectscript_export_extractor_t extractor) {
objectscript_export_test_extractor = extractor;
}
CBMFileResult *cbm_pipeline_extract_objectscript_export_with_timeout(
const char *source, int source_len, const char *project_name, const char *rel_path,
const CBMMacroTable *macro_table, const CBMReturnTypeTable *return_type_table,
int64_t timeout_micros) {
CBMArena export_arena;
cbm_arena_init(&export_arena);
int class_count = 0;
char **udl_strings = cbm_iris_export_to_udl(&export_arena, source, source_len, &class_count);
if (!udl_strings || class_count <= 0) {
cbm_arena_destroy(&export_arena);
return NULL;
}
CBMFileResult *aggregate = (CBMFileResult *)calloc(1, sizeof(CBMFileResult));
if (!aggregate) {
cbm_arena_destroy(&export_arena);
return NULL;
}
cbm_arena_init(&aggregate->arena);
if (aggregate->arena.nblocks == 0) {
cbm_free_result(aggregate);
cbm_arena_destroy(&export_arena);
return NULL;
}
aggregate->owned_results =
(CBMFileResult **)calloc((size_t)class_count, sizeof(CBMFileResult *));
if (!aggregate->owned_results) {
cbm_free_result(aggregate);
cbm_arena_destroy(&export_arena);
return NULL;
}
aggregate->cached_lang = CBM_LANG_OBJECTSCRIPT_UDL;
cbm_objectscript_export_extractor_t extract = objectscript_export_test_extractor
? objectscript_export_test_extractor
: cbm_extract_file_ex;
for (int ci = 0; ci < class_count; ci++) {
CBMFileResult *part = extract(udl_strings[ci], (int)strlen(udl_strings[ci]),
CBM_LANG_OBJECTSCRIPT_UDL, project_name, rel_path,
timeout_micros, NULL, NULL, macro_table, return_type_table);
if (!part) {
continue;
}
/* The aggregate has no single parse tree. Later ObjectScript Export
* passes consume extracted carriers, not a raw-XML tree. */
cbm_free_tree(part);
if (!objectscript_export_append_primary_arrays(aggregate, part) ||
!objectscript_export_append_secondary_arrays(aggregate, part) ||
!objectscript_export_append_strings(&aggregate->arena, &aggregate->exports,
part->exports) ||
!objectscript_export_append_strings(&aggregate->arena, &aggregate->constants,
part->constants) ||
!objectscript_export_append_strings(&aggregate->arena, &aggregate->global_vars,
part->global_vars) ||
!objectscript_export_append_strings(&aggregate->arena, &aggregate->macros,
part->macros) ||
!objectscript_export_append_error_ranges(aggregate, part)) {
goto merge_failed;
}
if (!aggregate->module_qn) {
aggregate->module_qn = part->module_qn;
}
if (!aggregate->namespace_name) {
aggregate->namespace_name = part->namespace_name;
}
if (part->has_error) {
aggregate->has_error = true;
if (!aggregate->error_msg) {
aggregate->error_msg = part->error_msg;
}
}
aggregate->is_test_file = aggregate->is_test_file || part->is_test_file;
aggregate->owned_results[aggregate->owned_result_count++] = part;
continue;
merge_failed:
cbm_free_result(part);
cbm_free_result(aggregate);
cbm_arena_destroy(&export_arena);
return NULL;
}
aggregate->imports_count = aggregate->imports.count;
cbm_arena_destroy(&export_arena);
return aggregate;
}
CBMFileResult *cbm_pipeline_extract_objectscript_export(
const char *source, int source_len, const char *project_name, const char *rel_path,
const CBMMacroTable *macro_table, const CBMReturnTypeTable *return_type_table) {
return cbm_pipeline_extract_objectscript_export_with_timeout(
source, source_len, project_name, rel_path, macro_table, return_type_table,
CBM_EXTRACT_BUDGET);
}
#undef OBJECTSCRIPT_EXPORT_APPEND_ARRAY
int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
int file_count) {
cbm_log_info("pass.start", "pass", "definitions", "files", itoa_log(file_count));
/* Ensure extraction library is initialized */
cbm_init();
/* Defensive: a prior pipeline run may have left a thread-local parser whose
* lexer holds pointers into a slab that has since been reclaimed. Drop it
* here so the first cbm_extract_file below recreates a fresh parser. */
cbm_destroy_thread_parser();
int total_defs = 0;
int total_calls = 0;
int total_imports = 0;
int errors = 0;
/* Sequential pass must extract all defs (which create Module/Function/...
* nodes) BEFORE resolving imports — otherwise a workspace import in the
* first file processed can't find the target Module node, because the
* target file's defs haven't been extracted yet. Result cache is
* required for this two-phase ordering. */
CBMFileResult **local_cache = ctx->result_cache;
bool owns_local_cache = false;
if (!local_cache) {
local_cache = (CBMFileResult **)cbm_calloc(CBM_MEM_CLASS_EXTRACT,
(size_t)file_count * sizeof(CBMFileResult *));
owns_local_cache = (local_cache != NULL);
}
/* Phase 1: extract every file and create def-derived nodes (Modules,
* Functions, ...) so any file's IMPORTS can resolve against the
* complete in-memory graph in Phase 2. */
for (int i = 0; i < file_count; i++) {
if (cbm_pipeline_check_cancel(ctx)) {
/* Cancellation mid-extraction: release the cache this pass owns,
* including results already extracted into it (the normal cleanup
* at the end of the pass does the same) -- clang-analyzer caught
* this return leaking the whole cache. */
if (owns_local_cache) {
for (int j = 0; j < file_count; j++) {
if (local_cache[j]) {
cbm_free_result(local_cache[j]);
}
}
cbm_free(CBM_MEM_CLASS_EXTRACT, local_cache);
}
cbm_work_arena_release();
return CBM_NOT_FOUND;
}
const char *path = files[i].path;
const char *rel = files[i].rel_path;
CBMLanguage lang = files[i].language;
/* Crash-quarantine skip (Stage 3c): the supervisor's single-threaded
* recovery re-run always lands on THIS sequential path (worker_count
* forced to 1). This first sequential pass REPORTS a crasher as a
* phase="crash" skip (surfacing it in skipped[]) and continues; later
* sequential passes (calls/usages/semantic) re-extract on a cache miss
* but hit the hard guard inside cbm_extract_file, so they no-op without
* re-crashing and without duplicating the skip. No-op unless
* CBM_INDEX_QUARANTINE_FILE is set. */
if (cbm_index_is_quarantined(rel)) {
const char *phase = cbm_index_quarantine_phase(rel);
if (!phase) {
phase = "crash";
}
const char *reason = (strcmp(phase, "hang") == 0) ? "quarantined after hang"
: (strcmp(phase, "error") == 0) ? "quarantined after error"
: "quarantined after crash";
cbm_pipeline_add_file_error(ctx->pipeline, rel, reason, phase);
errors++;
if (!cbm_pipeline_test_extraction_ok(ctx, lang, NULL)) {
goto configured_failure;
}
continue;
}
/* Read source file */
int source_len = 0;
long file_size = 0;
cbm_read_status_t rst = CBM_READ_OK;
char *source = read_file(path, &source_len, &file_size, &rst);
if (!source) {
errors++;
if (rst == CBM_READ_OVERSIZED) {
/* Never a silent drop: record the oversized skip + WARN so the
* file surfaces in the response/logfile with its sizes. */
long cap = cbm_max_file_bytes();
char reason[96];
snprintf(reason, sizeof(reason), "oversized (%lld MB > %lld MB)",
(long long)(file_size / (CBM_SZ_1K * CBM_SZ_1K)),
(long long)(cap / (CBM_SZ_1K * CBM_SZ_1K)));
cbm_pipeline_add_file_error(ctx->pipeline, rel, reason, "oversized");
cbm_log_warn("index.file_oversized", "path", rel, "size_mb",
itoa_log((int)(file_size / (CBM_SZ_1K * CBM_SZ_1K))), "cap_mb",
itoa_log((int)(cap / (CBM_SZ_1K * CBM_SZ_1K))));
} else if (rst == CBM_READ_OPEN_FAIL || rst == CBM_READ_OOM) {
cbm_pipeline_add_file_error(ctx->pipeline, rel, "read failed", "read");
}
/* Proven empty files contain no invocation; other missing source
* cannot certify an active configured-definition mapping. */
if (rst != CBM_READ_EMPTY && !cbm_pipeline_test_extraction_ok(ctx, lang, NULL)) {
goto configured_failure;
}
continue;
}
/* Studio Export XML is transformed to one cacheable aggregate so later
* passes see the same calls/usages/semantic carriers as native UDL. */
CBMFileResult *result =
cbm_pipeline_test_force_extract_null(ctx, lang) ? NULL
: lang == CBM_LANG_OBJECTSCRIPT_EXPORT
? cbm_pipeline_extract_objectscript_export(source, source_len, ctx->project_name,
rel, ctx->macro_table, NULL)
: cbm_extract_file_ex_with_tests(source, source_len, lang, ctx->project_name, rel,
CBM_EXTRACT_BUDGET, NULL,
NULL /* no extra defines or include paths */,
ctx->macro_table, NULL, ctx->test_declarations);
free(source);
if (!cbm_pipeline_test_extraction_ok(ctx, lang, result)) {
cbm_free_result(result);
goto configured_failure;
}
if (!result) {
errors++;
cbm_pipeline_add_file_error(ctx->pipeline, rel, "extract failed", "extract");
continue;
}
if (result->has_test_definition_owners && result->lsp_skipped) {
cbm_pipeline_test_owner_error(result);
}
if (!cbm_pipeline_test_result_ok(ctx, result)) {
cbm_free_result(result);
goto configured_failure;
}
/* Consume the previously-ignored has_error flag: a parse timeout /
* parse failure / unsupported-grammar result carries no defs but must
* still be reported (phase "extract", reason = the extractor's message).
* The empty result flows through unchanged (the defs loop is a no-op). */
if (result->has_error) {
cbm_pipeline_add_file_error(ctx->pipeline, rel,
result->error_msg ? result->error_msg : "extract failed",
"extract");
errors++;
} else if (result->parse_incomplete) {
/* Best-effort parse-coverage signal (#963): indexed, but with
* ERROR/MISSING regions — see pass_parallel.c (keep in sync). */
cbm_pipeline_add_file_error(
ctx->pipeline, rel, result->error_ranges ? result->error_ranges : "unknown",
result->parse_unusable ? "parse_unusable" : "parse_partial");
}
if (result->test_declarations_degraded) {
/* Its configured test forms could not be mapped: indexed without
* configured test roles (degraded per file, not the index). */
cbm_pipeline_add_file_error(
ctx->pipeline, rel,
cbm_test_extract_status_message(result->test_declarations_degraded_status),
"test_declarations");
}
/* Create nodes for each definition. A Python file's module QN bounds
* the registry's function-local detection to this file. */
const char *file_doc = result->module_doc; /* goes with the first def */
size_t py_prefix_len = lang == CBM_LANG_PYTHON
? cbm_pipeline_fqn_symbol_prefix_len(ctx->project_name, rel)
: 0;
for (int d = 0; d < result->defs.count; d++) {
process_def(ctx, &result->defs.items[d], rel, file_doc, lang, py_prefix_len,
!local_cache);
file_doc = NULL;
total_defs++;
}
/* Store calls for pass_calls (we save them in the extraction results
* for now — a future optimization would batch these) */
total_calls += result->calls.count;
if (local_cache) {
/* The cache lives through every later pass: hold the result as the
* parallel extract does (pass_parallel.c), without its tree (the
* cross-file LSP re-parses) and compacted. A PHP result keeps its
* tree: the vendor-import check reads each call and usage site's
* syntax from it (php_import_kind_at_site, pass_pkgmap.c), and this
* route keeps no source to fall back on. */
if (lang != CBM_LANG_PHP) {
cbm_free_tree(result);
}
cbm_result_compact(result);
local_cache[i] = result;
} else {
/* Cache unavailable: imports for this file can still only
* resolve to defs already in the graph, but the file's
* own defs are now persisted before the lookup. No namespace
* map is available without the cache (single-file scope). */
total_imports += create_import_edges_for_file(ctx, result, rel, NULL);
create_channel_edges_for_file(ctx, result, rel);
cbm_pipeline_diagram_arrows(ctx, result);
cbm_pipeline_create_env_configures_for_file(ctx, result, rel);
cbm_free_result(result);
}
}
cbm_work_arena_release(); /* the working arena compaction kept between files */
/* Every file's nodes exist now: link methods and diagram elements to
* their parents (without the cache each file linked its own above). */
if (local_cache) {
link_cached_def_parents(ctx, local_cache, file_count);
}
/* Phase 2: now that all extraction results are cached and Module
* nodes for every file are in the graph, walk the cache again to
* create IMPORTS / channel edges. Imports resolve against the full
* project graph. */
if (local_cache) {
/* Build a namespace/package → File-QN map so that namespace imports
* (C# `using`, Java/Kotlin `import`, PHP `use`) resolve to the file
* that declares the namespace. */
const char **rels = (const char **)calloc((size_t)file_count, sizeof(char *));
if (rels) {
for (int i = 0; i < file_count; i++) {
rels[i] = files[i].rel_path;
}
}
CBMHashTable *namespace_map =
cbm_pipeline_namespace_map_build(ctx->project_name, local_cache, rels, file_count);
free(rels);
for (int i = 0; i < file_count; i++) {
if (cbm_pipeline_check_cancel(ctx)) {
break;
}
CBMFileResult *result = local_cache[i];
if (!result) {
continue;
}
total_imports +=
create_import_edges_for_file(ctx, result, files[i].rel_path, namespace_map);
create_channel_edges_for_file(ctx, result, files[i].rel_path);
cbm_pipeline_diagram_arrows(ctx, result);
cbm_pipeline_create_env_configures_for_file(ctx, result, files[i].rel_path);
}
/* Publish instead of free — see the twin in pass_parallel.c (#1355). */
cbm_pipeline_namespace_map_free(cbm_pipeline_get_nsmap());
cbm_pipeline_set_nsmap(namespace_map);
if (owns_local_cache) {
for (int i = 0; i < file_count; i++) {
if (local_cache[i]) {
cbm_free_result(local_cache[i]);
}
}
cbm_free(CBM_MEM_CLASS_EXTRACT, local_cache);
}
}
cbm_log_info("pass.done", "pass", "definitions", "defs", itoa_log(total_defs), "calls",
itoa_log(total_calls), "imports", itoa_log(total_imports), "errors",
itoa_log(errors));
return 0;
configured_failure:
if (owns_local_cache) {
for (int j = 0; j < file_count; j++) {
cbm_free_result(local_cache[j]);
}
cbm_free(CBM_MEM_CLASS_EXTRACT, local_cache);
}
cbm_work_arena_release(); /* compaction keeps it between files: release on every exit */
return CBM_PIPELINE_ABORT_PRESERVE_DB;
}