mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
1232 lines
54 KiB
C
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;
|
|
}
|