Files
codebase-memory-mcp/internal/cbm/cbm.c
T
2026-07-04 11:20:26 +02:00

1095 lines
42 KiB
C

#include "cbm.h"
#include "arena.h" // CBMArena, cbm_arena_init/alloc/strdup/destroy
#include "helpers.h"
#include "lang_specs.h"
#include "extract_unified.h"
#include "lsp/go_lsp.h"
#include "lsp/c_lsp.h"
#include "lsp/php_lsp.h"
#include "lsp/py_lsp.h"
#include "lsp/ts_lsp.h"
#include "lsp/cs_lsp.h"
#include "lsp/java_lsp.h"
#include "lsp/kotlin_lsp.h"
#include "lsp/rust_lsp.h"
#include "preprocessor.h"
#include "foundation/compat.h"
#include "foundation/compat_fs.h" // cbm_fopen — crash-supervisor per-file marker write
#include "foundation/hash_table.h" // CBMHashTable — crash-supervisor quarantine set
#include "tree_sitter/api.h" // TSParser, TSNode, TSTree, TSInput, TSLanguage, TSPoint, TSParseOptions, TSParseState
#include "foundation/constants.h"
#include "mimalloc.h" // mi_malloc/mi_calloc/mi_realloc/mi_free/mi_usable_size — bind 3rd-party allocators (#424)
#if defined(CBM_BIND_TS_ALLOCATOR) && CBM_BIND_TS_ALLOCATOR
#include "sqlite3.h" // sqlite3_mem_methods, sqlite3_config, SQLITE_CONFIG_MALLOC — bind sqlite to mimalloc
#if defined(HAVE_LIBGIT2)
#include <git2/version.h>
#if defined(LIBGIT2_VERSION_CHECK)
#if !LIBGIT2_VERSION_CHECK(1, 7, 0)
#error "HAVE_LIBGIT2 requires libgit2 >= 1.7.0 for git_allocator"
#endif
#elif defined(LIBGIT2_VER_MAJOR) && defined(LIBGIT2_VER_MINOR) && defined(LIBGIT2_VER_REVISION)
#if ((LIBGIT2_VER_MAJOR * 1000000) + (LIBGIT2_VER_MINOR * 10000) + (LIBGIT2_VER_REVISION * 100)) < \
1070000
#error "HAVE_LIBGIT2 requires libgit2 >= 1.7.0 for git_allocator"
#endif
#else
#error "HAVE_LIBGIT2 requires known libgit2 version macros for the >= 1.7.0 git_allocator guard"
#endif
#include <git2.h> // git_libgit2_opts, GIT_OPT_SET_ALLOCATOR — bind libgit2 to mimalloc
#include <git2/sys/alloc.h> // git_allocator — not pulled in by <git2.h> (it's a sys/ header)
#endif
#endif
#include <stdint.h> // uint32_t, uint64_t, int64_t
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h> // struct timespec, CLOCK_MONOTONIC
// Atomic counters for profiling parse vs extraction time (nanoseconds).
// Accessed from multiple threads; using _Atomic for safe accumulation.
#include <stdatomic.h>
static _Atomic uint64_t total_parse_ns = 0;
static _Atomic uint64_t total_extract_ns = 0;
static _Atomic uint64_t total_lsp_ns = 0;
static _Atomic uint64_t total_preprocess_ns = 0;
static _Atomic uint64_t total_files_preprocessed = 0;
static _Atomic uint64_t total_files = 0;
// C/C++ preprocessor #define macros are extracted as Macro nodes (#375). On a
// macro-dense codebase (e.g. the Linux kernel: ~2.4M macros, 49% of all nodes)
// this is the dominant extraction cost, so it is gated to the full/advanced
// index modes. Default ON to preserve behavior for direct callers/tests; the
// pipeline sets it from the index mode before extraction. Set once pre-extract,
// read-only during, so a relaxed atomic is sufficient.
static _Atomic int g_extract_macros = 1;
void cbm_set_macro_extraction(int enabled) {
atomic_store_explicit(&g_extract_macros, enabled ? 1 : 0, memory_order_relaxed);
}
int cbm_macro_extraction_enabled(void) {
return atomic_load_explicit(&g_extract_macros, memory_order_relaxed);
}
#define NSEC_PER_SEC 1000000000ULL
#define USEC_TO_NSEC 1000ULL
/* Use compat.h's cbm_clock_gettime which accepts CLOCK_MONOTONIC (value
* varies by platform: 1 on Linux/Windows, 6 on macOS). We pass the
* platform value via the compat.h fallback. */
#if defined(CLOCK_MONOTONIC)
#define CBM_CLOCK_MONO CLOCK_MONOTONIC
#elif defined(__APPLE__)
#define CBM_CLOCK_MONO 6
#else
#define CBM_CLOCK_MONO 1
#endif
static uint64_t now_ns(void) {
struct timespec ts;
cbm_clock_gettime(CBM_CLOCK_MONO, &ts);
return ((uint64_t)ts.tv_sec * NSEC_PER_SEC) + (uint64_t)ts.tv_nsec;
}
// cbm_get_profile returns accumulated parse/extract times and file count.
void cbm_get_profile(cbm_profile_out_t out) {
*out.parse_ns = atomic_load(&total_parse_ns);
*out.extract_ns = atomic_load(&total_extract_ns);
*out.files = atomic_load(&total_files);
}
uint64_t cbm_get_lsp_ns(void) {
return atomic_load(&total_lsp_ns);
}
uint64_t cbm_get_preprocess_ns(void) {
return atomic_load(&total_preprocess_ns);
}
uint64_t cbm_get_files_preprocessed(void) {
return atomic_load(&total_files_preprocessed);
}
// cbm_reset_profile zeros the profiling counters.
void cbm_reset_profile(void) {
atomic_store(&total_parse_ns, 0);
atomic_store(&total_extract_ns, 0);
atomic_store(&total_lsp_ns, 0);
atomic_store(&total_preprocess_ns, 0);
atomic_store(&total_files_preprocessed, 0);
atomic_store(&total_files, 0);
}
// --- Growable array push functions ---
#define GROW_ARRAY(arr, arena) \
do { \
if ((arr)->count >= (arr)->cap) { \
int new_cap = (arr)->cap == 0 ? CBM_SZ_32 : (arr)->cap * PAIR_LEN; \
void *new_items = cbm_arena_alloc((arena), (size_t)new_cap * sizeof(*(arr)->items)); \
if (!new_items) \
return; \
if ((arr)->items && (arr)->count > 0) { \
memcpy(new_items, (arr)->items, (size_t)(arr)->count * sizeof(*(arr)->items)); \
} \
(arr)->items = new_items; \
(arr)->cap = new_cap; \
} \
} while (0)
void cbm_defs_push(CBMDefArray *arr, CBMArena *a, CBMDefinition def) {
GROW_ARRAY(arr, a);
arr->items[arr->count++] = def;
}
void cbm_calls_push(CBMCallArray *arr, CBMArena *a, CBMCall call) {
GROW_ARRAY(arr, a);
arr->items[arr->count++] = call;
}
void cbm_imports_push(CBMImportArray *arr, CBMArena *a, CBMImport imp) {
GROW_ARRAY(arr, a);
arr->items[arr->count++] = imp;
}
void cbm_usages_push(CBMUsageArray *arr, CBMArena *a, CBMUsage usage) {
GROW_ARRAY(arr, a);
arr->items[arr->count++] = usage;
}
void cbm_throws_push(CBMThrowArray *arr, CBMArena *a, CBMThrow thr) {
GROW_ARRAY(arr, a);
arr->items[arr->count++] = thr;
}
void cbm_rw_push(CBMRWArray *arr, CBMArena *a, CBMReadWrite rw) {
GROW_ARRAY(arr, a);
arr->items[arr->count++] = rw;
}
void cbm_typerefs_push(CBMTypeRefArray *arr, CBMArena *a, CBMTypeRef tr) {
GROW_ARRAY(arr, a);
arr->items[arr->count++] = tr;
}
void cbm_envaccess_push(CBMEnvAccessArray *arr, CBMArena *a, CBMEnvAccess ea) {
GROW_ARRAY(arr, a);
arr->items[arr->count++] = ea;
}
void cbm_typeassign_push(CBMTypeAssignArray *arr, CBMArena *a, CBMTypeAssign ta) {
GROW_ARRAY(arr, a);
arr->items[arr->count++] = ta;
}
void cbm_stringref_push(CBMStringRefArray *arr, CBMArena *a, CBMStringRef sr) {
GROW_ARRAY(arr, a);
arr->items[arr->count++] = sr;
}
void cbm_infrabinding_push(CBMInfraBindingArray *arr, CBMArena *a, CBMInfraBinding ib) {
GROW_ARRAY(arr, a);
arr->items[arr->count++] = ib;
}
void cbm_impltrait_push(CBMImplTraitArray *arr, CBMArena *a, CBMImplTrait it) {
GROW_ARRAY(arr, a);
arr->items[arr->count++] = it;
}
void cbm_resolvedcall_push(CBMResolvedCallArray *arr, CBMArena *a, CBMResolvedCall rc) {
GROW_ARRAY(arr, a);
arr->items[arr->count++] = rc;
}
void cbm_channels_push(CBMChannelArray *arr, CBMArena *a, CBMChannel ch) {
GROW_ARRAY(arr, a);
arr->items[arr->count++] = ch;
}
// --- String input reader (for parse_with_options) ---
typedef struct {
const char *string;
uint32_t length;
} CBMStringInput;
static const char *cbm_string_read(void *payload, uint32_t byte, TSPoint point,
uint32_t *bytes_read) {
(void)point;
CBMStringInput *self = (CBMStringInput *)payload;
if (byte >= self->length) {
*bytes_read = 0;
return "";
}
*bytes_read = self->length - byte;
return self->string + byte;
}
// --- Parse timeout callback ---
static bool cbm_timeout_cb(TSParseState *state) {
uint64_t deadline = *(uint64_t *)state->payload;
return now_ns() > deadline;
}
// --- Thread-local parser pool ---
// TSParser is not thread-safe, but can be reused across files on the same thread.
// We keep one parser per thread, and just switch language as needed.
// This avoids ~70K ts_parser_new()/ts_parser_delete() cycles on large repos.
static CBM_TLS TSParser *tl_parser = NULL;
static CBM_TLS CBMLanguage tl_parser_lang = CBM_LANG_COUNT; // invalid sentinel
// Get or create a thread-local parser configured for the given language.
static TSParser *get_thread_parser(const TSLanguage *ts_lang, CBMLanguage lang) {
if (!tl_parser) {
tl_parser = ts_parser_new();
if (!tl_parser) {
return NULL;
}
tl_parser_lang = CBM_LANG_COUNT;
}
if (tl_parser_lang != lang) {
ts_parser_set_language(tl_parser, ts_lang);
tl_parser_lang = lang;
}
return tl_parser;
}
// --- Allocator binding (defense-in-depth, #424) ---
/* Bind tree-sitter, sqlite3, and libgit2 to mimalloc explicitly so a correct
* binary does NOT depend on the fragile MI_OVERRIDE symbol override. Under
* MI_OVERRIDE=1 — particularly the Windows static-MinGW link with
* --allow-multiple-definition — `malloc`/`free` can resolve to DIFFERENT
* allocators (mimalloc vs the CRT) inside third-party libs, so a block
* allocated by mimalloc gets freed by the CRT (or vice-versa), corrupting the
* heap freelist (#424). Binding each library through one explicit allocator
* eliminates that mismatch class generically, on every platform.
*
* Guarded to the production build (CBM_BIND_TS_ALLOCATOR=1, which CFLAGS_PROD
* defines alongside MI_OVERRIDE=1). The test build is CRT + ASan, where binding
* to mimalloc would mismatch ASan/CRT frees — there these binds compile to
* no-ops and the build stays unchanged. */
#if defined(CBM_BIND_TS_ALLOCATOR) && CBM_BIND_TS_ALLOCATOR
#include <assert.h>
/* sqlite3 mem methods backed by mimalloc. sqlite's xMalloc/xRealloc/xSize use
* `int` sizes; wrap with size_t casts. xRoundup rounds to an 8-byte boundary
* (sqlite requires 8-byte-aligned roundup, and mimalloc honors that alignment).
* Field order matches struct sqlite3_mem_methods exactly:
* xMalloc, xFree, xRealloc, xSize, xRoundup, xInit, xShutdown, pAppData. */
static void *cbm_sqlite_malloc(int n) {
return mi_malloc((size_t)n);
}
static void cbm_sqlite_free(void *p) {
mi_free(p);
}
static void *cbm_sqlite_realloc(void *p, int n) {
return mi_realloc(p, (size_t)n);
}
static int cbm_sqlite_size(void *p) {
return (int)mi_usable_size(p);
}
static int cbm_sqlite_roundup(int n) {
return (n + 7) & ~7; /* round up to 8-byte boundary */
}
static int cbm_sqlite_meminit(void *appdata) {
(void)appdata;
return SQLITE_OK;
}
static void cbm_sqlite_memshutdown(void *appdata) {
(void)appdata;
}
#if defined(HAVE_LIBGIT2)
/* libgit2 >= 1.7 git_allocator backed by mimalloc. The struct has exactly
* three members: gmalloc(size_t,file,line), grealloc(ptr,size,file,line),
* gfree(ptr). The file/line args are ignored. */
static void *cbm_git_malloc(size_t n, const char *file, int line) {
(void)file;
(void)line;
return mi_malloc(n);
}
static void *cbm_git_realloc(void *ptr, size_t size, const char *file, int line) {
(void)file;
(void)line;
return mi_realloc(ptr, size);
}
static void cbm_git_free(void *ptr) {
mi_free(ptr);
}
#endif /* HAVE_LIBGIT2 */
#endif /* CBM_BIND_TS_ALLOCATOR */
void cbm_alloc_init(void) {
#if defined(CBM_BIND_TS_ALLOCATOR) && CBM_BIND_TS_ALLOCATOR
static int alloc_bound = 0; /* single-threaded startup; plain int is fine */
if (alloc_bound) {
return;
}
alloc_bound = 1;
/* tree-sitter runtime (was previously bound in cbm_init; consolidated here). */
ts_set_allocator(mi_malloc, mi_calloc, mi_realloc, mi_free);
/* sqlite3. SQLITE_CONFIG_MALLOC MUST run before sqlite3_initialize / the
* first sqlite3_open* — otherwise sqlite3_config returns SQLITE_MISUSE
* silently and the binding is ignored. cbm_alloc_init() runs as the very
* first statement of main(), before cbm_mcp_server_new → cbm_store_open*. */
static sqlite3_mem_methods cbm_sqlite_mem = {
cbm_sqlite_malloc, /* xMalloc */
cbm_sqlite_free, /* xFree */
cbm_sqlite_realloc, /* xRealloc */
cbm_sqlite_size, /* xSize */
cbm_sqlite_roundup, /* xRoundup */
cbm_sqlite_meminit, /* xInit */
cbm_sqlite_memshutdown, /* xShutdown */
NULL, /* pAppData */
};
int sqlite_rc = sqlite3_config(SQLITE_CONFIG_MALLOC, &cbm_sqlite_mem);
assert(sqlite_rc == SQLITE_OK && "SQLITE_CONFIG_MALLOC must run before sqlite3_initialize");
(void)sqlite_rc;
#if defined(HAVE_LIBGIT2)
/* libgit2. GIT_OPT_SET_ALLOCATOR MUST be set BEFORE git_libgit2_init():
* libgit2's git_allocator_global_init (run during init) installs the default
* stdalloc only if no custom allocator is set yet
* (`if (git__allocator.gmalloc != git_failalloc_malloc) return 0;`), and the
* pre-init global is the fail-allocator. There is no allocator-reset on
* git_libgit2_shutdown, so binding once here — before pass_githistory's
* per-call git_libgit2_init/shutdown pairs ever run — persists for the whole
* process. git_libgit2_opts(GIT_OPT_SET_ALLOCATOR,...) itself does not
* allocate, so calling it before init is safe. */
static git_allocator cbm_git_alloc = {
cbm_git_malloc, /* gmalloc */
cbm_git_realloc, /* grealloc */
cbm_git_free, /* gfree */
};
git_libgit2_opts(GIT_OPT_SET_ALLOCATOR, &cbm_git_alloc);
#endif /* HAVE_LIBGIT2 */
#endif /* CBM_BIND_TS_ALLOCATOR */
}
// --- Init/Shutdown ---
static int cbm_initialized = 0;
int cbm_init(void) {
if (cbm_initialized) {
return 0;
}
enum { CBM_INIT_DONE = 1 };
cbm_initialized = CBM_INIT_DONE;
/* Defense-in-depth allocator binds (idempotent). main() calls cbm_alloc_init
* first; this covers non-main entry points (pipeline passes call cbm_init).
* For sqlite the SQLITE_CONFIG_MALLOC bind only takes effect if it runs
* before sqlite initializes — main() guarantees that ordering; here it is a
* best-effort idempotent re-assert for paths that never hit main(). */
cbm_alloc_init();
return 0;
}
void cbm_reset_thread_parser(void) {
// Release parser's internal slab-allocated subtrees (stack, cached token).
// Must be called BEFORE cbm_slab_reset_thread() to avoid corrupting
// live slab chunks that the parser still references.
if (tl_parser) {
ts_parser_reset(tl_parser);
}
}
void cbm_destroy_thread_parser(void) {
// Full cleanup: delete the parser. Call on worker thread exit.
if (tl_parser) {
ts_parser_delete(tl_parser);
tl_parser = NULL;
tl_parser_lang = CBM_LANG_COUNT;
}
}
void cbm_shutdown(void) {
// Clean up thread-local parser for the calling thread.
// Note: other threads' TLS parsers are freed when those threads exit.
cbm_destroy_thread_parser();
cbm_initialized = 0;
}
// --- Bottleneck call-name classification (language-agnostic heuristics) ---
// Case-insensitive equality for short callee names.
static bool name_ieq(const char *a, const char *b) {
for (; *a && *b; a++, b++) {
if (tolower((unsigned char)*a) != tolower((unsigned char)*b)) {
return false;
}
}
return *a == '\0' && *b == '\0';
}
static bool name_in_set(const char *name, const char *const *set) {
for (const char *const *s = set; *s; s++) {
if (name_ieq(name, *s)) {
return true;
}
}
return false;
}
// Linear-scan / membership calls: a hit inside a loop is the textbook hidden
// O(n^2) (cf. Olivo et al., PLDI'15) that syntactic loop-depth alone misses.
static bool is_linear_scan_name(const char *n) {
static const char *const set[] = {"find", "indexof", "contains", "includes", "search",
"lookup", "strstr", "strchr", "strrchr", "memchr",
"find_if", "findindex", "count", "index", NULL};
return name_in_set(n, set);
}
// Allocation / growable-append calls: repeated inside a loop is the classic
// accidental reallocation / string-concat O(n^2). Names are deliberately
// conservative; meaningless in some languages → simply never matches there.
static bool is_alloc_name(const char *n) {
static const char *const set[] = {"malloc", "calloc", "realloc", "strdup", "strndup",
"append", "push_back", "emplace_back", "concat", "strcat",
"strncat", "push", "pushback", NULL};
return name_in_set(n, set);
}
// Extract the receiver identifier from a def's receiver text — Go's
// "(s *Store)" / "(s Store)" → "s". Stores the identifier start in *out and
// returns its length; returns 0 for unnamed receivers ("(*Store)", "(Store)"),
// where no second token follows the identifier (a lone token is the TYPE, not
// a name — such methods have no receiver variable to call through anyway).
static size_t receiver_ident(const char *recv_text, const char **out) {
const char *p = recv_text;
if (*p == '(') {
p++;
}
while (*p == ' ' || *p == '\t') {
p++;
}
const char *start = p;
while ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') ||
*p == '_') {
p++;
}
size_t len = (size_t)(p - start);
if (len == 0) {
return 0; // "(*Store)": leading '*', no identifier
}
while (*p == ' ' || *p == '\t') {
p++;
}
if (*p == ')' || *p == '\0') {
return 0; // "(Store)": single token is the type, receiver unnamed
}
*out = start;
return len;
}
// Whether a callee expression targets the same instance/class as the enclosing
// def, i.e. counts as genuine self-recursion rather than a same-named call on a
// different receiver. callee_name may be bare ("recur") or qualified
// ("self.recur", "this.recur", "super().save", "axios.get", "self.obj.recur").
//
// Bare names have no receiver → assume self-call (free function calling itself
// by bare name; preserves prior behavior). Qualified names: the receiver chain
// is everything before the LAST '.', and the WHOLE chain must name the same
// object — self/this/cls/@self, or the enclosing def's own receiver identifier
// (Go: `s` in `func (s *Store) save()`, from CBMDefinition.receiver). Matching
// the whole chain (not its first segment) keeps self.obj.recur() out: it
// targets self's FIELD obj, a different object. super() is the parent class and
// any other receiver (axios, console, ...) a different target. See #599.
static bool is_self_receiver(const char *callee_name, const char *def_receiver) {
if (!callee_name || !callee_name[0]) {
return false;
}
const char *dot = strrchr(callee_name, '.');
if (!dot) {
return true; // bare name → self-recursion candidate
}
size_t rlen = (size_t)(dot - callee_name);
static const char *const self_receivers[] = {"self", "this", "cls", "@self", NULL};
for (int i = 0; self_receivers[i]; i++) {
size_t sl = strlen(self_receivers[i]);
if (rlen == sl && strncmp(callee_name, self_receivers[i], sl) == 0) {
return true;
}
}
if (def_receiver) {
const char *rid = NULL;
size_t ril = receiver_ident(def_receiver, &rid);
if (ril > 0 && ril == rlen && strncmp(callee_name, rid, ril) == 0) {
return true; // call through the enclosing method's own receiver
}
}
return false; // super() / axios / console / self.obj / any other receiver
}
// Count parameters from a signature string like "(int a, Foo* b, cb (*)(int,int))".
// Fallback for languages where param_names isn't populated (e.g. C keeps only the
// signature text). Counts commas at the top paren level; treats "()"/"(void)" as 0.
// Approximate by design (a structural smell, not an exact arity).
static int count_params_from_signature(const char *sig) {
if (!sig) {
return 0;
}
const char *p = sig;
while (*p && *p != '(') {
p++;
}
if (*p != '(') {
return 0;
}
p++;
const char *list = p;
int depth = 0;
int commas = 0;
bool any = false;
for (; *p; p++) {
char ch = *p;
if (ch == '(' || ch == '[' || ch == '{' || ch == '<') {
depth++;
} else if (ch == ')') {
if (depth == 0) {
break;
}
depth--;
} else if (ch == ']' || ch == '}' || ch == '>') {
if (depth > 0) {
depth--;
}
} else if (ch == ',' && depth == 0) {
commas++;
} else if (!isspace((unsigned char)ch)) {
any = true;
}
}
if (!any) {
return 0; /* "()" */
}
if (commas == 0) {
while (*list == ' ' || *list == '\t') {
list++;
}
if (strncmp(list, "void", 4) == 0 &&
(list[4] == ')' || list[4] == ' ' || list[4] == '\0')) {
return 0; /* C "(void)" */
}
}
return commas + 1;
}
// --- Main extraction function ---
/* Test-only deterministic fault injection for the crash/hang supervisor tests.
* Gated entirely behind env vars that are never set in production; a matching
* rel_path either aborts (a fault signal the supervisor classifies as a crash)
* or spins forever (an external-scanner infinite loop the quiet-timeout kills).
* This gives an honest guard — green iff the supervisor actually contains a real
* fault — instead of a fixture that may stop faulting once a root cause is fixed. */
/* Crash-supervisor per-file marker (Stage 3c skip-and-continue). In the
* supervisor's single-threaded recovery re-run, cbm_extract_file records the
* file it is ABOUT to process here (CBM_INDEX_MARKER_FILE) before touching it,
* so that if this file hard-crashes the worker the parent can read the marker
* back and learn the EXACT crasher to quarantine. Only meaningful single-
* threaded (parallel would race the marker); the env var is set solely by the
* supervisor during recovery, so it is a no-op on every normal run. */
static void cbm_index_write_marker(const char *rel_path) {
const char *mf = getenv("CBM_INDEX_MARKER_FILE");
if (!mf || !mf[0] || !rel_path || !rel_path[0]) {
return;
}
FILE *f = cbm_fopen(mf, "wb");
if (f) {
(void)fputs(rel_path, f);
(void)fclose(f);
}
}
/* ── Crash-quarantine set (Stage 3c skip-and-continue) ──────────────────────
* After a crash the supervisor re-runs the worker single-threaded, passing
* CBM_INDEX_QUARANTINE_FILE — a newline-delimited list of repo-relative paths
* that already crashed the indexer and MUST NOT be extracted again. Owned here,
* next to the other env-driven extract hooks (marker + fault injector), so the
* single hard guard lives at the one choke point every pass funnels through
* (cbm_extract_file): whether a pass re-extracts from disk on a cache miss
* (sequential pass_calls/usages/semantic) or extracts fresh, a quarantined file
* short-circuits to an empty result and never reaches the parser/crash. The
* pipeline extract loops separately REPORT the skip as phase="crash" via
* cbm_index_is_quarantined() so the crasher surfaces in the response skipped[].
* Loaded once, lazily; read-only after load (safe for the parallel workers,
* though recovery runs single-threaded). Unset env ⇒ empty set ⇒ cheap no-op. */
static CBMHashTable *g_quarantine_set = NULL;
enum { CBM_QSET_UNINIT = 0, CBM_QSET_INITING = 1, CBM_QSET_INITED = 2 };
static atomic_int g_quarantine_state = CBM_QSET_UNINIT;
static void cbm_quarantine_load(void) {
const char *qf = getenv("CBM_INDEX_QUARANTINE_FILE");
if (!qf || !qf[0]) {
return; /* normal path: empty set */
}
FILE *f = cbm_fopen(qf, "rb");
if (!f) {
return;
}
CBMHashTable *set = cbm_ht_create(16);
if (!set) {
(void)fclose(f);
return;
}
char line[2048];
while (fgets(line, sizeof(line), f)) {
size_t len = strlen(line);
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
line[--len] = '\0';
}
if (len == 0) {
continue;
}
/* Line format: "path\tphase" where phase is "crash" or "hang". A bare
* "path" line (no tab) is tolerated and defaults to phase "crash" for
* backward compatibility with older quarantine files. */
char *tab = strchr(line, '\t');
const char *phase = "crash";
if (tab) {
*tab = '\0';
if (tab[1]) {
phase = tab + 1;
}
}
if (line[0] == '\0') {
continue; /* empty path (line began with a tab) — skip */
}
/* The table borrows the key + value pointers, so dup both. Intentionally
* never freed: the set lives for the whole (short-lived worker) process.
* The value stores the phase so cbm_index_quarantine_phase() can report
* "crash" vs "hang"; membership (cbm_index_is_quarantined) is value != NULL. */
char *key = cbm_strdup(line);
char *pval = cbm_strdup(phase);
if (key && pval) {
cbm_ht_set(set, key, (void *)pval);
}
}
(void)fclose(f);
g_quarantine_set = set;
}
bool cbm_index_is_quarantined(const char *rel_path) {
if (!rel_path || !rel_path[0]) {
return false;
}
int state = atomic_load(&g_quarantine_state);
if (state != CBM_QSET_INITED) {
/* First caller wins the CAS and loads; racers spin until INITED.
* Same once-init pattern as cbm_ui_log_init (http_server.c). */
state = CBM_QSET_UNINIT;
if (atomic_compare_exchange_strong(&g_quarantine_state, &state, CBM_QSET_INITING)) {
cbm_quarantine_load();
atomic_store(&g_quarantine_state, CBM_QSET_INITED);
} else {
while (atomic_load(&g_quarantine_state) != CBM_QSET_INITED) {
cbm_usleep(1000); /* 1ms */
}
}
}
return g_quarantine_set && cbm_ht_has(g_quarantine_set, rel_path);
}
const char *cbm_index_quarantine_phase(const char *rel_path) {
/* cbm_index_is_quarantined drives the lazy once-load and returns true only
* when the set is loaded and holds rel_path — so on true, g_quarantine_set is
* non-NULL and the stored value is the phase string ("crash"/"hang"). */
if (!cbm_index_is_quarantined(rel_path)) {
return NULL;
}
return (const char *)cbm_ht_get(g_quarantine_set, rel_path);
}
static void cbm_test_fault_inject(const char *rel_path) {
if (!rel_path || !rel_path[0]) {
return;
}
const char *crash_on = getenv("CBM_TEST_CRASH_ON");
if (crash_on && crash_on[0] && strstr(rel_path, crash_on)) {
abort(); /* SIGABRT → WIFSIGNALED → classified as a crash */
}
const char *hang_on = getenv("CBM_TEST_HANG_ON");
if (hang_on && hang_on[0] && strstr(rel_path, hang_on)) {
for (;;) {
/* Busy-spin: the supervisor's quiet-timeout kills + reports us. */
}
}
}
CBMFileResult *cbm_extract_file(const char *source, int source_len, CBMLanguage language,
const char *project, const char *rel_path, int64_t timeout_micros,
const char **extra_defines, const char **include_paths) {
// Allocate result on heap (arena inside for all string data)
enum { SINGLE = 1 };
CBMFileResult *result = (CBMFileResult *)calloc(SINGLE, sizeof(CBMFileResult));
if (!result) {
return NULL;
}
cbm_arena_init(&result->arena);
CBMArena *a = &result->arena;
/* Crash-quarantine hard guard (Stage 3c): a file the supervisor pinned as a
* crasher must NEVER be parsed again. Return a clean empty result BEFORE the
* marker write and fault injector so no pass (including sequential re-extract
* passes that miss the result cache) can crash on it. The pipeline extract
* loops separately record it as a phase="crash" skip. Checked before the
* marker so quarantined files never overwrite it — the marker keeps pointing
* at the real (non-quarantined) file being processed when a crash hits. */
if (cbm_index_is_quarantined(rel_path)) {
return result;
}
cbm_index_write_marker(rel_path);
cbm_test_fault_inject(rel_path);
// Get language spec
const CBMLangSpec *spec = cbm_lang_spec(language);
if (!spec) {
result->has_error = true;
result->error_msg = cbm_arena_strdup(a, "unsupported language");
return result;
}
// Get tree-sitter language
const TSLanguage *ts_lang = cbm_ts_language(language);
if (!ts_lang) {
result->has_error = true;
result->error_msg = cbm_arena_strdup(a, "no tree-sitter grammar");
return result;
}
// Get thread-local parser (reused across files on same thread)
TSParser *parser = get_thread_parser(ts_lang, language);
if (!parser) {
result->has_error = true;
result->error_msg = cbm_arena_strdup(a, "parser alloc failed");
return result;
}
// Reset parser state from any previous parse (cancellation flags etc.)
ts_parser_reset(parser);
uint64_t t0 = now_ns();
// Build string input + timeout options for parse_with_options
CBMStringInput str_input = {source, (uint32_t)source_len};
TSInput ts_input = {
&str_input,
cbm_string_read,
TSInputEncodingUTF8,
NULL,
};
TSParseOptions opts = {0};
uint64_t deadline_ns = 0; // cppcheck-suppress unreadVariable
if (timeout_micros > 0) {
deadline_ns = t0 + ((uint64_t)timeout_micros * USEC_TO_NSEC);
opts.payload = &deadline_ns;
opts.progress_callback = cbm_timeout_cb;
}
TSTree *tree = ts_parser_parse_with_options(parser, NULL, ts_input, opts);
uint64_t t1 = now_ns();
if (!tree) {
result->has_error = true;
result->error_msg =
cbm_arena_strdup(a, timeout_micros > 0 ? "parse timeout" : "parse failed");
return result;
}
TSNode root = ts_tree_root_node(tree);
// Compute module QN. Java/Go derive the module from the CONTAINING
// DIRECTORY (package semantics) rather than baking the filename stem in,
// so def QNs, the LSP caller_qn, and the textual calls-enclosing QN all
// agree (e.g. Outer.java -> module "proj", not "proj.Outer"). Other
// languages are unchanged.
result->module_qn = cbm_fqn_module_source_lang(a, project, rel_path, language);
result->is_test_file = cbm_is_test_file(rel_path, language);
// Build extraction context
CBMExtractCtx ctx = {
.arena = a,
.result = result,
.source = source,
.source_len = source_len,
.language = language,
.project = project,
.rel_path = rel_path,
.module_qn = result->module_qn,
.root = root,
};
// Run extractors: defs + imports use separate walks (unique recursion patterns),
// then a single unified cursor walk handles the remaining 7 extractors.
cbm_extract_definitions(&ctx);
cbm_extract_imports(&ctx);
cbm_extract_unified(&ctx);
// Channel detection (Socket.IO / EventEmitter) — JS/TS only.
cbm_extract_channels(&ctx);
// K8s / Kustomize semantic pass (additional structured extraction for YAML-based infra files).
if (ctx.language == CBM_LANG_KUSTOMIZE || ctx.language == CBM_LANG_K8S) {
cbm_extract_k8s(&ctx);
}
// LSP type-aware call/usage resolution (per-file). Runs in every mode;
// refines the tree-sitter + textual-resolution graph with type info.
uint64_t lsp_start = now_ns();
{
if (language == CBM_LANG_GO) {
cbm_run_go_lsp(a, result, source, source_len, root);
}
if (language == CBM_LANG_C || language == CBM_LANG_CPP || language == CBM_LANG_CUDA) {
cbm_run_c_lsp(a, result, source, source_len, root, language != CBM_LANG_C);
}
if (language == CBM_LANG_PHP) {
cbm_run_php_lsp(a, result, source, source_len, root);
}
if (language == CBM_LANG_PYTHON) {
cbm_run_py_lsp(a, result, source, source_len, root);
}
if (language == CBM_LANG_JAVASCRIPT || language == CBM_LANG_TYPESCRIPT ||
language == CBM_LANG_TSX) {
bool js_mode = (language == CBM_LANG_JAVASCRIPT);
// jsx_mode: TSX always; .jsx in the JS bucket also enables it.
bool jsx_mode = (language == CBM_LANG_TSX);
if (language == CBM_LANG_JAVASCRIPT && rel_path) {
size_t rl = strlen(rel_path);
if (rl >= 4 && strcmp(rel_path + rl - 4, ".jsx") == 0)
jsx_mode = true;
}
// dts_mode: ".d.ts" suffix (TypeScript only).
bool dts_mode = false;
if (language == CBM_LANG_TYPESCRIPT && rel_path) {
size_t rl = strlen(rel_path);
if (rl >= 5 && strcmp(rel_path + rl - 5, ".d.ts") == 0)
dts_mode = true;
}
cbm_run_ts_lsp(a, result, source, source_len, root, js_mode, jsx_mode, dts_mode);
}
if (language == CBM_LANG_CSHARP) {
cbm_run_cs_lsp(a, result, source, source_len, root);
}
}
if (language == CBM_LANG_JAVA) {
cbm_run_java_lsp(a, result, source, source_len, root);
}
if (language == CBM_LANG_KOTLIN) {
cbm_run_kotlin_lsp(a, result, source, source_len, root);
}
if (language == CBM_LANG_RUST) {
cbm_run_rust_lsp(a, result, source, source_len, root);
}
atomic_fetch_add(&total_lsp_ns, now_ns() - lsp_start);
// Calls extracted so far all carry ORIGINAL-source line numbers; the C/C++
// preprocessor second pass below appends calls with EXPANDED-source lines,
// which must not be used for the def line-range attribution of the bottleneck
// metrics. Remember the boundary.
int orig_calls_count = result->calls.count;
// Second pass: preprocess C/C++/CUDA and extract additional macro-hidden calls.
// Defs keep original-source line numbers; only CALLS are extracted from expanded source.
if (language == CBM_LANG_C || language == CBM_LANG_CPP || language == CBM_LANG_CUDA) {
uint64_t pp_start = now_ns();
char *expanded = cbm_preprocess(source, source_len, rel_path, extra_defines, include_paths,
language != CBM_LANG_C);
if (expanded) {
int expanded_len = (int)strlen(expanded);
// Record calls count before second pass
int calls_before = result->calls.count;
// Parse expanded source with fresh tree
TSParser *pp_parser = get_thread_parser(ts_lang, language);
if (pp_parser) {
ts_parser_reset(pp_parser);
CBMStringInput pp_input = {expanded, (uint32_t)expanded_len};
TSInput pp_ts_input = {
&pp_input,
cbm_string_read,
TSInputEncodingUTF8,
NULL,
};
TSParseOptions pp_opts = {0};
TSTree *pp_tree =
ts_parser_parse_with_options(pp_parser, NULL, pp_ts_input, pp_opts);
if (pp_tree) {
TSNode pp_root = ts_tree_root_node(pp_tree);
// Build context for expanded source — extract only calls via unified extractor
CBMExtractCtx pp_ctx = {
.arena = a,
.result = result,
.source = expanded,
.source_len = expanded_len,
.language = language,
.project = project,
.rel_path = rel_path,
.module_qn = result->module_qn,
.root = pp_root,
};
// Re-run unified extraction on expanded source.
// This adds macro-expanded calls; duplicates with original calls are
// harmless (pipeline deduplicates by caller+callee).
cbm_extract_unified(&pp_ctx);
// Also run LSP on expanded source for additional type-resolved
// calls (language is already C/C++/CUDA — checked in enclosing
// block). Runs in every mode.
cbm_run_c_lsp(a, result, expanded, expanded_len, pp_root,
language != CBM_LANG_C);
ts_tree_delete(pp_tree);
}
}
cbm_preprocess_free(expanded);
atomic_fetch_add(&total_files_preprocessed, 1);
(void)calls_before; // used for future logging
}
atomic_fetch_add(&total_preprocess_ns, now_ns() - pp_start);
}
// Bottleneck call-context metrics. Each call is attributed to the INNERMOST
// enclosing Function/Method def by source-line range (defs and calls in one
// CBMFileResult share the same file). Range matching is used instead of
// enclosing_func_qn string matching because some grammars (notably C, whose
// function_definition has no "name" field) attribute the call's scope to the
// module rather than the function — line ranges are unambiguous and
// language-agnostic. Bounded per file (defs x calls), not a repo-scale scan.
int def_count = result->defs.count;
bool *has_self = def_count > 0 ? calloc((size_t)def_count, sizeof(bool)) : NULL;
bool *has_guarded = def_count > 0 ? calloc((size_t)def_count, sizeof(bool)) : NULL;
// param_count is a standalone structural smell (independent of calls). Prefer
// the parsed param_names array; fall back to counting from the signature text
// for languages (e.g. C) that populate only the signature.
for (int di = 0; di < def_count; di++) {
CBMDefinition *d = &result->defs.items[di];
int pc = 0;
if (d->param_names) {
while (d->param_names[pc]) {
pc++;
}
}
if (pc == 0 && d->signature) {
pc = count_params_from_signature(d->signature);
}
d->param_count = pc;
}
for (int ci = 0; ci < orig_calls_count; ci++) {
const CBMCall *c = &result->calls.items[ci];
if (!c->callee_name || c->start_line <= 0) {
continue;
}
// Innermost enclosing Function/Method def by line range (smallest span).
int best = -1;
int best_span = -1;
for (int di = 0; di < def_count; di++) {
const CBMDefinition *d = &result->defs.items[di];
if (!d->name || !d->label ||
(strcmp(d->label, "Function") != 0 && strcmp(d->label, "Method") != 0)) {
continue;
}
if ((int)d->start_line <= c->start_line && c->start_line <= (int)d->end_line) {
int span = (int)d->end_line - (int)d->start_line;
if (best < 0 || span < best_span) {
best_span = span;
best = di;
}
}
}
if (best < 0) {
continue;
}
CBMDefinition *d = &result->defs.items[best];
// callee_name may be bare ("recur") or qualified ("self.recur",
// "super().save", "axios.get"). A short-name match alone is not
// self-recursion: the callee must also target the same object
// (is_self_receiver), or super().save() inside save and axios.get
// inside get are false positives (#599).
const char *dot = strrchr(c->callee_name, '.');
const char *callee_short = dot ? dot + 1 : c->callee_name;
bool in_loop = c->loop_depth > 0;
if (strcmp(callee_short, d->name) == 0 && is_self_receiver(c->callee_name, d->receiver)) {
// Direct self-recursion. The call graph omits self-edges (pass_calls
// skips source==target), so detect it here; seeds "recursive".
d->is_recursive = true;
if (has_self) {
has_self[best] = true;
}
if (in_loop) {
d->recursion_in_loop = true; // recursion compounded by a loop
}
if (c->branch_depth > 0 && has_guarded) {
has_guarded[best] = true; // a self-call guarded by some conditional
}
}
if (in_loop && is_linear_scan_name(callee_short)) {
d->linear_scan_in_loop++; // hidden O(n^2): linear scan inside a loop
}
if (in_loop && is_alloc_name(callee_short)) {
d->alloc_in_loop++; // repeated allocation/append inside a loop
}
}
// Recursive with no self-call guarded by any conditional → no obvious base
// case on the recursive path: a stronger "potentially unbounded" signal.
for (int di = 0; di < def_count; di++) {
if (has_self && has_self[di] && !(has_guarded && has_guarded[di])) {
result->defs.items[di].unguarded_recursion = true;
}
}
free(has_self);
free(has_guarded);
uint64_t t2 = now_ns();
result->imports_count = result->imports.count;
// Accumulate profiling counters
atomic_fetch_add(&total_parse_ns, t1 - t0);
atomic_fetch_add(&total_extract_ns, t2 - t1);
atomic_fetch_add(&total_files, 1);
// Retain tree for cross-file LSP reuse (caller frees via cbm_free_tree)
result->cached_tree = tree;
result->cached_lang = language;
return result;
}
void cbm_free_result(CBMFileResult *result) {
if (!result) {
return;
}
if (result->cached_tree) {
ts_tree_delete(result->cached_tree);
result->cached_tree = NULL;
}
cbm_arena_destroy(&result->arena);
free(result);
}
void cbm_free_tree(CBMFileResult *result) {
if (result && result->cached_tree) {
ts_tree_delete(result->cached_tree);
result->cached_tree = NULL;
}
}
void cbm_free_tree_ptr(TSTree *tree) {
if (tree) {
ts_tree_delete(tree);
}
}