diff --git a/internal/cbm/ac.c b/internal/cbm/ac.c index af20f1a37..69b29729e 100644 --- a/internal/cbm/ac.c +++ b/internal/cbm/ac.c @@ -61,6 +61,99 @@ static void queue_free(Queue *q) { free(q->data); } +// Phase 1: Build trie (goto function) from patterns. Returns state count. +static int ac_build_trie(CBMAutomaton *ac, const char **patterns, const int *lengths, int count) { + int alpha_size = ac->alpha_size; + int num_states = 1; // state 0 = root + + for (int p = 0; p < count; p++) { + int state = 0; + for (int j = 0; j < lengths[p]; j++) { + int c = ac->alpha_map[(unsigned char)patterns[p][j]]; + int idx = (state * alpha_size) + c; + if (ac->go_table[idx] == -1) { + ac->go_table[idx] = num_states++; + } + state = ac->go_table[idx]; + } + if (p < CBM_AC_MAX_BITMASK) { + ac->output[state] |= (1ULL << p); + } + ac->output_list[state] = p; + } + + // Root self-loops for unmatched bytes. + for (int c = 0; c < alpha_size; c++) { + if (ac->go_table[c] == -1) { + ac->go_table[c] = 0; + } + } + return num_states; +} + +// Phase 2: Build failure function via BFS + compute full goto table. +static void ac_build_failure(CBMAutomaton *ac, int num_states) { + int alpha_size = ac->alpha_size; + int *fail = (int *)calloc(num_states, sizeof(int)); + + Queue q; + queue_init(&q, num_states); + + for (int c = 0; c < alpha_size; c++) { + int s = ac->go_table[c]; + if (s != 0) { + fail[s] = 0; + queue_push(&q, s); + } + } + + while (!queue_empty(&q)) { + int r = queue_pop(&q); + for (int c = 0; c < alpha_size; c++) { + int idx = (r * alpha_size) + c; + int s = ac->go_table[idx]; + if (s != -1) { + fail[s] = ac->go_table[(fail[r] * alpha_size) + c]; + ac->output[s] |= ac->output[fail[s]]; + if (ac->output_next[s] == -1 && ac->output_list[fail[s]] != -1) { + ac->output_next[s] = fail[s]; + } + queue_push(&q, s); + } else { + ac->go_table[idx] = ac->go_table[(fail[r] * alpha_size) + c]; + } + } + } + + free(fail); + queue_free(&q); +} + +// Shrink allocations to exact state count. +static void ac_shrink_tables(CBMAutomaton *ac, int num_states, int max_states) { + if (num_states >= max_states) { + return; + } + int alpha_size = ac->alpha_size; + void *tmp; + tmp = realloc(ac->go_table, (size_t)num_states * alpha_size * sizeof(int)); + if (tmp) { + ac->go_table = (int *)tmp; + } + tmp = realloc(ac->output, (size_t)num_states * sizeof(uint64_t)); + if (tmp) { + ac->output = (uint64_t *)tmp; + } + tmp = realloc(ac->output_list, (size_t)num_states * sizeof(int)); + if (tmp) { + ac->output_list = (int *)tmp; + } + tmp = realloc(ac->output_next, (size_t)num_states * sizeof(int)); + if (tmp) { + ac->output_next = (int *)tmp; + } +} + // cbm_ac_build constructs an Aho-Corasick automaton from a set of patterns. // // Parameters: @@ -72,7 +165,6 @@ static void queue_free(Queue *q) { // alpha_size — alphabet size (256 if alpha_map is NULL) // // Returns a heap-allocated automaton. Caller must call cbm_ac_free(). -// NOLINTNEXTLINE(readability-function-cognitive-complexity) CBMAutomaton *cbm_ac_build(const char **patterns, const int *lengths, int count, const uint8_t *alpha_map, int alpha_size) { if (count <= 0) { @@ -82,7 +174,6 @@ CBMAutomaton *cbm_ac_build(const char **patterns, const int *lengths, int count, alpha_size = 256; } - // Estimate max states: sum of pattern lengths + 1 (root). int max_states = 1; for (int i = 0; i < count; i++) { max_states += lengths[i]; @@ -92,7 +183,6 @@ CBMAutomaton *cbm_ac_build(const char **patterns, const int *lengths, int count, ac->alpha_size = alpha_size; ac->num_patterns = count; - // Set up alphabet mapping. if (alpha_map) { memcpy(ac->alpha_map, alpha_map, 256); } else { @@ -101,7 +191,6 @@ CBMAutomaton *cbm_ac_build(const char **patterns, const int *lengths, int count, } } - // Allocate goto table and output arrays. ac->go_table = (int *)malloc((size_t)max_states * alpha_size * sizeof(int)); memset(ac->go_table, -1, (size_t)max_states * alpha_size * sizeof(int)); ac->output = (uint64_t *)calloc(max_states, sizeof(uint64_t)); @@ -112,98 +201,10 @@ CBMAutomaton *cbm_ac_build(const char **patterns, const int *lengths, int count, ac->output_next[i] = -1; } - int num_states = 1; // state 0 = root - - // Phase 1: Build trie (goto function) from patterns. - for (int p = 0; p < count; p++) { - int state = 0; - for (int j = 0; j < lengths[p]; j++) { - int c = ac->alpha_map[(unsigned char)patterns[p][j]]; - int idx = (state * alpha_size) + c; - if (ac->go_table[idx] == -1) { - ac->go_table[idx] = num_states++; - } - state = ac->go_table[idx]; - } - // Mark this state as accepting pattern p. - if (p < CBM_AC_MAX_BITMASK) { - ac->output[state] |= (1ULL << p); - } - // Append to output list. - ac->output_list[state] = p; - } - - // Root self-loops for unmatched bytes. - for (int c = 0; c < alpha_size; c++) { - if (ac->go_table[c] == -1) { - ac->go_table[c] = 0; - } - } - - // Phase 2: Build failure function via BFS + compute full goto table. - // We store failure links temporarily in a separate array. - int *fail = (int *)calloc(num_states, sizeof(int)); - - Queue q; - queue_init(&q, num_states); - - // Depth-1 states: failure → root. - for (int c = 0; c < alpha_size; c++) { - int s = ac->go_table[c]; // root's goto for c - if (s != 0) { - fail[s] = 0; - queue_push(&q, s); - } - } - - // BFS: compute failure links and fill in missing goto entries. - while (!queue_empty(&q)) { - int r = queue_pop(&q); - for (int c = 0; c < alpha_size; c++) { - int idx = (r * alpha_size) + c; - int s = ac->go_table[idx]; - if (s != -1) { - // s exists in trie - fail[s] = ac->go_table[(fail[r] * alpha_size) + c]; - // Merge output: dictionary suffix links. - ac->output[s] |= ac->output[fail[s]]; - // Chain output list (for >64 pattern mode). - if (ac->output_next[s] == -1 && ac->output_list[fail[s]] != -1) { - ac->output_next[s] = fail[s]; - } - queue_push(&q, s); - } else { - // Fill missing transition: follow failure link. - ac->go_table[idx] = ac->go_table[(fail[r] * alpha_size) + c]; - } - } - } - - free(fail); - queue_free(&q); - + int num_states = ac_build_trie(ac, patterns, lengths, count); + ac_build_failure(ac, num_states); ac->num_states = num_states; - - // Reallocate to exact size (optional, saves memory for large automatons). - if (num_states < max_states) { - void *tmp; - tmp = realloc(ac->go_table, (size_t)num_states * alpha_size * sizeof(int)); - if (tmp) { - ac->go_table = (int *)tmp; - } - tmp = realloc(ac->output, (size_t)num_states * sizeof(uint64_t)); - if (tmp) { - ac->output = (uint64_t *)tmp; - } - tmp = realloc(ac->output_list, (size_t)num_states * sizeof(int)); - if (tmp) { - ac->output_list = (int *)tmp; - } - tmp = realloc(ac->output_next, (size_t)num_states * sizeof(int)); - if (tmp) { - ac->output_next = (int *)tmp; - } - } + ac_shrink_tables(ac, num_states, max_states); return ac; } diff --git a/internal/cbm/extract_calls.c b/internal/cbm/extract_calls.c index 49420d498..3195b1334 100644 --- a/internal/cbm/extract_calls.c +++ b/internal/cbm/extract_calls.c @@ -87,19 +87,9 @@ static bool lean_is_in_type_position(TSNode node) { return false; } -// Extract callee name from a call node -// NOLINTNEXTLINE(readability-function-cognitive-complexity) -static char *extract_callee_name(CBMArena *a, TSNode node, const char *source, CBMLanguage lang) { - // Lean 4: apply — name field is callee. Skip if in a type annotation position. - // Must be checked before the generic "name" field handler below. - if (lang == CBM_LANG_LEAN && strcmp(ts_node_type(node), "apply") == 0) { - if (lean_is_in_type_position(node)) { - return NULL; - } - // Fall through to generic handler - } - - // Try "function" field (most languages: call_expression, etc.) +// Try common field-based callee resolution (function, name, method fields). +static char *extract_callee_from_fields(CBMArena *a, TSNode node, const char *source) { + // Try "function" field TSNode func_node = ts_node_child_by_field_name(node, "function", 8); if (!ts_node_is_null(func_node)) { const char *fk = ts_node_type(func_node); @@ -117,7 +107,6 @@ static char *extract_callee_name(CBMArena *a, TSNode node, const char *source, C TSNode name_node = ts_node_child_by_field_name(node, "name", 4); if (!ts_node_is_null(name_node)) { char *name = cbm_node_text(a, name_node, source); - // For Java: prepend object if present TSNode obj = ts_node_child_by_field_name(node, "object", 6); if (!ts_node_is_null(obj) && name) { char *obj_text = cbm_node_text(a, obj, source); @@ -142,131 +131,163 @@ static char *extract_callee_name(CBMArena *a, TSNode node, const char *source, C return method; } - // ObjC message_expression: [receiver message] - if (lang == CBM_LANG_OBJC && strcmp(ts_node_type(node), "message_expression") == 0) { - TSNode selector = ts_node_child_by_field_name(node, "selector", 8); - if (!ts_node_is_null(selector)) { - return cbm_node_text(a, selector, source); - } - } + return NULL; +} - // Erlang: call -> first child is module:function or just function - if (lang == CBM_LANG_ERLANG && strcmp(ts_node_type(node), "call") == 0) { +// Haskell/OCaml: extract callee from apply/infix nodes. +static char *extract_fp_callee(CBMArena *a, TSNode node, const char *source, const char *nk) { + if (strcmp(nk, "apply") == 0 || strcmp(nk, "application_expression") == 0) { if (ts_node_child_count(node) > 0) { - return cbm_node_text(a, ts_node_child(node, 0), source); - } - } - - // Haskell/OCaml: application_expression, infix, apply - if (lang == CBM_LANG_HASKELL || lang == CBM_LANG_OCAML) { - const char *nk = ts_node_type(node); - if (strcmp(nk, "apply") == 0 || strcmp(nk, "application_expression") == 0) { - if (ts_node_child_count(node) > 0) { - TSNode callee = ts_node_child(node, 0); - if (strcmp(ts_node_type(callee), "identifier") == 0 || - strcmp(ts_node_type(callee), "variable") == 0 || - strcmp(ts_node_type(callee), "constructor") == 0 || - strcmp(ts_node_type(callee), "value_path") == 0) { - return cbm_node_text(a, callee, source); - } - } - } - if (strcmp(nk, "infix") == 0 || strcmp(nk, "infix_expression") == 0) { - TSNode op = ts_node_child_by_field_name(node, "operator", 8); - if (!ts_node_is_null(op)) { - return cbm_node_text(a, op, source); - } - // Fallback: second child is usually the operator - if (ts_node_child_count(node) >= 3) { - return cbm_node_text(a, ts_node_child(node, 1), source); + TSNode callee = ts_node_child(node, 0); + const char *ck = ts_node_type(callee); + if (strcmp(ck, "identifier") == 0 || strcmp(ck, "variable") == 0 || + strcmp(ck, "constructor") == 0 || strcmp(ck, "value_path") == 0) { + return cbm_node_text(a, callee, source); } } } - - // Elixir: first child of call is the function name - if (lang == CBM_LANG_ELIXIR && strcmp(ts_node_type(node), "call") == 0) { - if (ts_node_child_count(node) > 0) { - TSNode first = ts_node_child(node, 0); - const char *fk = ts_node_type(first); - if (strcmp(fk, "identifier") == 0 || strcmp(fk, "dot") == 0) { - return cbm_node_text(a, first, source); - } + if (strcmp(nk, "infix") == 0 || strcmp(nk, "infix_expression") == 0) { + TSNode op = ts_node_child_by_field_name(node, "operator", 8); + if (!ts_node_is_null(op)) { + return cbm_node_text(a, op, source); + } + if (ts_node_child_count(node) >= 3) { + return cbm_node_text(a, ts_node_child(node, 1), source); } } + return NULL; +} - // Perl: various call expression types - if (lang == CBM_LANG_PERL) { - if (ts_node_child_count(node) > 0) { - return cbm_node_text(a, ts_node_child(node, 0), source); +// Wolfram: extract callee from apply, skipping LHS of set definitions. +static char *extract_wolfram_callee(CBMArena *a, TSNode node, const char *source) { + TSNode parent = ts_node_parent(node); + if (!ts_node_is_null(parent)) { + const char *pk = ts_node_type(parent); + if ((strcmp(pk, "set_delayed_top") == 0 || strcmp(pk, "set_top") == 0 || + strcmp(pk, "set_delayed") == 0 || strcmp(pk, "set") == 0) && + ts_node_named_child_count(parent) > 0 && + ts_node_eq(ts_node_named_child(parent, 0), node)) { + return NULL; } } + if (ts_node_named_child_count(node) > 0) { + TSNode head = ts_node_named_child(node, 0); + const char *hk = ts_node_type(head); + if (strcmp(hk, "user_symbol") == 0 || strcmp(hk, "builtin_symbol") == 0) { + return cbm_node_text(a, head, source); + } + } + return NULL; +} - // PHP: function_call_expression, member_call_expression, etc. +// Language-specific callee extraction for FP and niche languages. +// Swift callee extraction from call/constructor expressions. +static char *extract_swift_callee(CBMArena *a, TSNode node, const char *source, const char *nk) { + if (strcmp(nk, "call_expression") != 0 && strcmp(nk, "constructor_expression") != 0) { + return NULL; + } + if (ts_node_named_child_count(node) > 0) { + TSNode callee = ts_node_named_child(node, 0); + const char *ck = ts_node_type(callee); + if (strcmp(ck, "simple_identifier") == 0 || strcmp(ck, "navigation_expression") == 0) { + return cbm_node_text(a, callee, source); + } + } + return NULL; +} + +// Callee extraction for scripting languages (Elixir, Perl, PHP, Kotlin, MATLAB). +static char *extract_scripting_callee(CBMArena *a, TSNode node, const char *source, + CBMLanguage lang, const char *nk) { + if (lang == CBM_LANG_ELIXIR && strcmp(nk, "call") == 0 && ts_node_child_count(node) > 0) { + TSNode first = ts_node_child(node, 0); + const char *fk = ts_node_type(first); + if (strcmp(fk, "identifier") == 0 || strcmp(fk, "dot") == 0) { + return cbm_node_text(a, first, source); + } + return NULL; + } + if (lang == CBM_LANG_PERL && ts_node_child_count(node) > 0) { + return cbm_node_text(a, ts_node_child(node, 0), source); + } if (lang == CBM_LANG_PHP) { - func_node = ts_node_child_by_field_name(node, "function", 8); + TSNode func_node = ts_node_child_by_field_name(node, "function", 8); if (ts_node_is_null(func_node)) { func_node = ts_node_child_by_field_name(node, "name", 4); } - if (!ts_node_is_null(func_node)) { - return cbm_node_text(a, func_node, source); - } + return ts_node_is_null(func_node) ? NULL : cbm_node_text(a, func_node, source); } - - // Kotlin: navigation_expression, call_expression - if (lang == CBM_LANG_KOTLIN) { - if (ts_node_child_count(node) > 0) { - return cbm_node_text(a, ts_node_child(node, 0), source); - } + if (lang == CBM_LANG_KOTLIN && ts_node_child_count(node) > 0) { + return cbm_node_text(a, ts_node_child(node, 0), source); } - - // MATLAB: command node — first child is command_name (not identifier) - if (lang == CBM_LANG_MATLAB && strcmp(ts_node_type(node), "command") == 0) { - if (ts_node_child_count(node) > 0) { - return cbm_node_text(a, ts_node_child(node, 0), source); - } + if (lang == CBM_LANG_MATLAB && strcmp(nk, "command") == 0 && ts_node_child_count(node) > 0) { + return cbm_node_text(a, ts_node_child(node, 0), source); } + return NULL; +} - // Wolfram: apply — first named child is callee (user_symbol or builtin_symbol) - // Skip if this apply is the LHS of a set/set_delayed definition (top or nested) - if (lang == CBM_LANG_WOLFRAM && strcmp(ts_node_type(node), "apply") == 0) { - TSNode parent = ts_node_parent(node); - if (!ts_node_is_null(parent)) { - const char *pk = ts_node_type(parent); - if ((strcmp(pk, "set_delayed_top") == 0 || strcmp(pk, "set_top") == 0 || - strcmp(pk, "set_delayed") == 0 || strcmp(pk, "set") == 0) && - ts_node_named_child_count(parent) > 0 && - ts_node_eq(ts_node_named_child(parent, 0), node)) { - return NULL; - } - } - if (ts_node_named_child_count(node) > 0) { - TSNode head = ts_node_named_child(node, 0); - const char *hk = ts_node_type(head); - if (strcmp(hk, "user_symbol") == 0 || strcmp(hk, "builtin_symbol") == 0) { - return cbm_node_text(a, head, source); - } - } +// ObjC: extract callee from message_expression selector. +static char *extract_objc_callee(CBMArena *a, TSNode node, const char *source, const char *nk) { + if (strcmp(nk, "message_expression") != 0) { return NULL; } + TSNode selector = ts_node_child_by_field_name(node, "selector", 8); + return ts_node_is_null(selector) ? NULL : cbm_node_text(a, selector, source); +} - // Swift: call_expression has no "function" field — callee is first named child. - // Also handle constructor_expression for MyClass() syntax. +// Erlang: extract callee from call node's first child. +static char *extract_erlang_callee(CBMArena *a, TSNode node, const char *source, const char *nk) { + if (strcmp(nk, "call") != 0 || ts_node_child_count(node) == 0) { + return NULL; + } + return cbm_node_text(a, ts_node_child(node, 0), source); +} + +static char *extract_callee_lang_specific(CBMArena *a, TSNode node, const char *source, + CBMLanguage lang) { + const char *nk = ts_node_type(node); + + if (lang == CBM_LANG_OBJC) { + return extract_objc_callee(a, node, source, nk); + } + if (lang == CBM_LANG_ERLANG) { + return extract_erlang_callee(a, node, source, nk); + } + if (lang == CBM_LANG_HASKELL || lang == CBM_LANG_OCAML) { + return extract_fp_callee(a, node, source, nk); + } + if (lang == CBM_LANG_WOLFRAM && strcmp(nk, "apply") == 0) { + return extract_wolfram_callee(a, node, source); + } if (lang == CBM_LANG_SWIFT) { - const char *nk = ts_node_type(node); - if (strcmp(nk, "call_expression") == 0 || strcmp(nk, "constructor_expression") == 0) { - if (ts_node_named_child_count(node) > 0) { - TSNode callee = ts_node_named_child(node, 0); - const char *ck = ts_node_type(callee); - if (strcmp(ck, "simple_identifier") == 0 || - strcmp(ck, "navigation_expression") == 0) { - return cbm_node_text(a, callee, source); - } - } - } - return NULL; + return extract_swift_callee(a, node, source, nk); } - // Generic fallback: first child + return extract_scripting_callee(a, node, source, lang, nk); +} + +// Extract callee name from a call node +static char *extract_callee_name(CBMArena *a, TSNode node, const char *source, CBMLanguage lang) { + // Lean 4: skip type-position applies + if (lang == CBM_LANG_LEAN && strcmp(ts_node_type(node), "apply") == 0) { + if (lean_is_in_type_position(node)) { + return NULL; + } + } + + // Try common field-based resolution first + char *name = extract_callee_from_fields(a, node, source); + if (name) { + return name; + } + + // Language-specific patterns + name = extract_callee_lang_specific(a, node, source, lang); + if (name) { + return name; + } + + // Generic fallback: first identifier child if (ts_node_child_count(node) > 0) { TSNode first = ts_node_child(node, 0); if (strcmp(ts_node_type(first), "identifier") == 0) { @@ -277,9 +298,43 @@ static char *extract_callee_name(CBMArena *a, TSNode node, const char *source, C return NULL; } +// Strip quotes and validate a string arg. Returns validated text or NULL. +static const char *strip_and_validate_string_arg(CBMArena *a, char *text) { + if (!text || !text[0]) { + return NULL; + } + int len = (int)strlen(text); + if (len >= 2 && (text[0] == '"' || text[0] == '\'')) { + text = cbm_arena_strndup(a, text + 1, (size_t)(len - 2)); + len -= 2; + } + if (!text || len <= 0 || len >= 512) { + return NULL; + } + for (int vi = 0; vi < len; vi++) { + if ((unsigned char)text[vi] < 0x20 && text[vi] != '\t') { + return NULL; + } + } + return text; +} + +// Extract first string argument from a call's arguments node. +static const char *extract_first_string_arg(CBMExtractCtx *ctx, TSNode args) { + uint32_t nc = ts_node_named_child_count(args); + for (uint32_t ai = 0; ai < nc && ai < 3; ai++) { + TSNode arg = ts_node_named_child(args, ai); + const char *ak = ts_node_type(arg); + if (is_string_like(ak)) { + char *text = cbm_node_text(ctx->arena, arg, ctx->source); + return strip_and_validate_string_arg(ctx->arena, text); + } + } + return NULL; +} + // Walk AST for call nodes (iterative) #define CALLS_STACK_CAP 512 -// NOLINTNEXTLINE(readability-function-cognitive-complexity) static void walk_calls(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec) { TSNode stack[CALLS_STACK_CAP]; int top = 0; @@ -291,48 +346,16 @@ static void walk_calls(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec) if (cbm_kind_in_set(node, spec->call_node_types)) { char *callee = extract_callee_name(ctx->arena, node, ctx->source, ctx->language); - if (callee && callee[0]) { - if (!cbm_is_keyword(callee, ctx->language)) { - CBMCall call = {0}; - call.callee_name = callee; - call.enclosing_func_qn = cbm_enclosing_func_qn_cached(ctx, node); - call.first_string_arg = NULL; + if (callee && callee[0] && !cbm_is_keyword(callee, ctx->language)) { + CBMCall call = {0}; + call.callee_name = callee; + call.enclosing_func_qn = cbm_enclosing_func_qn_cached(ctx, node); - TSNode args = ts_node_child_by_field_name(node, "arguments", 9); - if (!ts_node_is_null(args)) { - uint32_t nc = ts_node_named_child_count(args); - for (uint32_t ai = 0; ai < nc && ai < 3; ai++) { - TSNode arg = ts_node_named_child(args, ai); - const char *ak = ts_node_type(arg); - if (strcmp(ak, "string") == 0 || strcmp(ak, "string_literal") == 0 || - strcmp(ak, "interpreted_string_literal") == 0 || - strcmp(ak, "raw_string_literal") == 0 || - strcmp(ak, "string_content") == 0) { - char *text = cbm_node_text(ctx->arena, arg, ctx->source); - if (text && text[0]) { - int len = (int)strlen(text); - if (len >= 2 && (text[0] == '"' || text[0] == '\'')) { - text = cbm_arena_strndup(ctx->arena, text + 1, - (size_t)(len - 2)); - len -= 2; - } - bool valid = (text != NULL && len > 0 && len < 512); - for (int vi = 0; vi < len && valid; vi++) { - unsigned char ch = (unsigned char)text[vi]; - if (ch < 0x20 && ch != '\t') { - valid = false; - } - } - if (valid) { - call.first_string_arg = text; - } - } - break; - } - } - } - cbm_calls_push(&ctx->result->calls, ctx->arena, call); + TSNode args = ts_node_child_by_field_name(node, "arguments", 9); + if (!ts_node_is_null(args)) { + call.first_string_arg = extract_first_string_arg(ctx, args); } + cbm_calls_push(&ctx->result->calls, ctx->arena, call); } } @@ -384,10 +407,27 @@ void cbm_extract_calls(CBMExtractCtx *ctx) { // --- Unified handler: called once per node by the cursor walk --- -/* Extract all arguments from a call expression into call->args[]. - * Captures expression text, resolved constants, keyword names, and - * dotted field chains (member_expression → "payload.info.url"). */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Process a keyword argument (keyword_argument or pair node). +static void process_keyword_arg(CBMExtractCtx *ctx, TSNode arg_node, CBMCallArg *ca) { + TSNode key_n = ts_node_child_by_field_name(arg_node, "name", 4); + TSNode val_n = ts_node_child_by_field_name(arg_node, "value", 5); + if (ts_node_is_null(key_n)) { + key_n = ts_node_child_by_field_name(arg_node, "key", 3); + } + if (!ts_node_is_null(key_n)) { + ca->keyword = cbm_node_text(ctx->arena, key_n, ctx->source); + } + if (!ts_node_is_null(val_n)) { + ca->expr = cbm_node_text(ctx->arena, val_n, ctx->source); + if (strcmp(ts_node_type(val_n), "identifier") == 0 && ca->expr) { + ca->value = lookup_string_constant(ctx, ca->expr); + } else if (is_string_like(ts_node_type(val_n)) && ca->expr) { + ca->value = strip_quotes(ctx->arena, ca->expr); + } + } +} + +/* Extract all arguments from a call expression into call->args[]. */ static void extract_call_args(CBMExtractCtx *ctx, TSNode args, CBMCall *call) { uint32_t argc = ts_node_named_child_count(args); int positional_idx = 0; @@ -398,22 +438,7 @@ static void extract_call_args(CBMExtractCtx *ctx, TSNode args, CBMCall *call) { memset(ca, 0, sizeof(*ca)); if (strcmp(ak, "keyword_argument") == 0 || strcmp(ak, "pair") == 0) { - TSNode key_n = ts_node_child_by_field_name(arg_node, "name", 4); - TSNode val_n = ts_node_child_by_field_name(arg_node, "value", 5); - if (ts_node_is_null(key_n)) { - key_n = ts_node_child_by_field_name(arg_node, "key", 3); - } - if (!ts_node_is_null(key_n)) { - ca->keyword = cbm_node_text(ctx->arena, key_n, ctx->source); - } - if (!ts_node_is_null(val_n)) { - ca->expr = cbm_node_text(ctx->arena, val_n, ctx->source); - if (strcmp(ts_node_type(val_n), "identifier") == 0 && ca->expr) { - ca->value = lookup_string_constant(ctx, ca->expr); - } else if (is_string_like(ts_node_type(val_n)) && ca->expr) { - ca->value = strip_quotes(ctx->arena, ca->expr); - } - } + process_keyword_arg(ctx, arg_node, ca); ca->index = positional_idx++; call->arg_count++; } else if (strcmp(ak, "list_splat") == 0 || strcmp(ak, "dictionary_splat") == 0 || @@ -432,14 +457,142 @@ static void extract_call_args(CBMExtractCtx *ctx, TSNode args, CBMCall *call) { } } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Check if a keyword name matches URL or topic patterns. +static bool is_url_or_topic_keyword(const char *key) { + static const char *url_keywords[] = {"url", "endpoint", "path", "uri", + "target_url", "base_url", NULL}; + static const char *topic_keywords[] = {"topic", "topic_id", "topic_name", + "queue", "queue_name", "queue_id", + "subject", "channel", NULL}; + for (int i = 0; url_keywords[i]; i++) { + if (strcmp(key, url_keywords[i]) == 0) { + return true; + } + } + for (int i = 0; topic_keywords[i]; i++) { + if (strcmp(key, topic_keywords[i]) == 0) { + return true; + } + } + return false; +} + +// Extract string value from a node (literal or constant reference). +static const char *extract_string_value(CBMExtractCtx *ctx, TSNode val_node) { + const char *vk = ts_node_type(val_node); + if (is_string_like(vk)) { + char *text = cbm_node_text(ctx->arena, val_node, ctx->source); + if (text && text[0]) { + return strip_quotes(ctx->arena, text); + } + } else if (strcmp(vk, "identifier") == 0) { + char *const_name = cbm_node_text(ctx->arena, val_node, ctx->source); + if (const_name) { + return lookup_string_constant(ctx, const_name); + } + } + return NULL; +} + +// Try to extract URL/topic from a keyword_argument or pair node. +static const char *extract_keyword_url(CBMExtractCtx *ctx, TSNode arg) { + TSNode key_node = ts_node_child_by_field_name(arg, "name", 4); + TSNode val_node = ts_node_child_by_field_name(arg, "value", 5); + if (ts_node_is_null(key_node)) { + key_node = ts_node_child_by_field_name(arg, "key", 3); + } + if (ts_node_is_null(key_node) || ts_node_is_null(val_node)) { + return NULL; + } + char *key = cbm_node_text(ctx->arena, key_node, ctx->source); + if (!key || !is_url_or_topic_keyword(key)) { + return NULL; + } + return extract_string_value(ctx, val_node); +} + +// Try to extract URL/topic from a positional argument (string or constant). +static const char *extract_positional_url(CBMExtractCtx *ctx, TSNode arg, const char *ak) { + if (is_string_like(ak)) { + char *text = cbm_node_text(ctx->arena, arg, ctx->source); + const char *validated = strip_and_validate_string_arg(ctx->arena, text); + if (validated) { + return validated; + } + } + if (strcmp(ak, "identifier") == 0) { + char *const_name = cbm_node_text(ctx->arena, arg, ctx->source); + if (const_name) { + return lookup_string_constant(ctx, const_name); + } + } + return NULL; +} + +// Extract URL/topic from keyword or positional args. +static const char *extract_url_or_topic_arg(CBMExtractCtx *ctx, TSNode args) { + uint32_t nc = ts_node_named_child_count(args); + for (uint32_t ai = 0; ai < nc; ai++) { + TSNode arg = ts_node_named_child(args, ai); + const char *ak = ts_node_type(arg); + + if (strcmp(ak, "keyword_argument") == 0 || strcmp(ak, "pair") == 0) { + const char *val = extract_keyword_url(ctx, arg); + if (val) { + return val; + } + continue; + } + + if (ai < 3) { + const char *val = extract_positional_url(ctx, arg, ak); + if (val) { + return val; + } + } + } + return NULL; +} + +// Extract second argument name (handler ref for route registrations). +static const char *extract_handler_arg(CBMExtractCtx *ctx, TSNode args) { + uint32_t nc = ts_node_named_child_count(args); + for (uint32_t ai = 1; ai < nc && ai < 4; ai++) { + TSNode arg2 = ts_node_named_child(args, ai); + const char *ak2 = ts_node_type(arg2); + if (strcmp(ak2, "identifier") == 0 || strcmp(ak2, "member_expression") == 0 || + strcmp(ak2, "selector_expression") == 0 || strcmp(ak2, "attribute") == 0 || + strcmp(ak2, "field_expression") == 0) { + return cbm_node_text(ctx->arena, arg2, ctx->source); + } + } + return NULL; +} + +// Extract JSX component refs (uppercase tags) as CALLS edges. +static void extract_jsx_component_ref(CBMExtractCtx *ctx, TSNode node, const char *kind, + const char *enclosing_func_qn) { + if (strcmp(kind, "jsx_self_closing_element") != 0 && strcmp(kind, "jsx_opening_element") != 0) { + return; + } + TSNode name_node = ts_node_child_by_field_name(node, "name", 4); + if (ts_node_is_null(name_node)) { + return; + } + char *name = cbm_node_text(ctx->arena, name_node, ctx->source); + if (name && name[0] >= 'A' && name[0] <= 'Z') { + CBMCall call = {0}; + call.callee_name = name; + call.enclosing_func_qn = enclosing_func_qn; + cbm_calls_push(&ctx->result->calls, ctx->arena, call); + } +} + void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state) { if (!spec->call_node_types || !spec->call_node_types[0]) { return; } - const char *kind = ts_node_type(node); - if (cbm_kind_in_set(node, spec->call_node_types)) { char *callee = extract_callee_name(ctx->arena, node, ctx->source, ctx->language); if (callee && callee[0] && !cbm_is_keyword(callee, ctx->language)) { @@ -447,147 +600,12 @@ void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Walk call.callee_name = callee; call.enclosing_func_qn = state->enclosing_func_qn; - /* Extract URL/topic/key from call arguments. - * Strategy: check keyword args first (url=, topic_id=, queue=), - * then first positional string, then resolve constant references. */ TSNode args = ts_node_child_by_field_name(node, "arguments", 9); if (!ts_node_is_null(args)) { - /* Keyword patterns that indicate URL/topic/key */ - static const char *url_keywords[] = {"url", "endpoint", "path", "uri", - "target_url", "base_url", NULL}; - static const char *topic_keywords[] = {"topic", "topic_id", "topic_name", - "queue", "queue_name", "queue_id", - "subject", "channel", NULL}; - - uint32_t nc = ts_node_named_child_count(args); - for (uint32_t ai = 0; ai < nc && !call.first_string_arg; ai++) { - TSNode arg = ts_node_named_child(args, ai); - const char *ak = ts_node_type(arg); - - /* Check keyword_argument nodes: url="...", topic_id="..." */ - if (strcmp(ak, "keyword_argument") == 0 || strcmp(ak, "pair") == 0) { - TSNode key_node = ts_node_child_by_field_name(arg, "name", 4); - TSNode val_node = ts_node_child_by_field_name(arg, "value", 5); - if (ts_node_is_null(key_node)) { - key_node = ts_node_child_by_field_name(arg, "key", 3); - } - if (ts_node_is_null(key_node) || ts_node_is_null(val_node)) { - continue; - } - - char *key = cbm_node_text(ctx->arena, key_node, ctx->source); - if (!key) { - continue; - } - - /* Check if key matches URL or topic patterns */ - bool is_url_kw = false; - bool is_topic_kw = false; - for (int ki = 0; url_keywords[ki]; ki++) { - if (strcmp(key, url_keywords[ki]) == 0) { - is_url_kw = true; - break; - } - } - if (!is_url_kw) { - for (int ki = 0; topic_keywords[ki]; ki++) { - if (strcmp(key, topic_keywords[ki]) == 0) { - is_topic_kw = true; - break; - } - } - } - if (!is_url_kw && !is_topic_kw) { - continue; - } - - /* Extract value — string literal or constant reference */ - const char *vk = ts_node_type(val_node); - if (strcmp(vk, "string") == 0 || strcmp(vk, "string_literal") == 0 || - strcmp(vk, "interpreted_string_literal") == 0 || - strcmp(vk, "raw_string_literal") == 0) { - char *text = cbm_node_text(ctx->arena, val_node, ctx->source); - if (text && text[0]) { - int len = (int)strlen(text); - if (len >= 2 && (text[0] == '"' || text[0] == '\'')) { - text = - cbm_arena_strndup(ctx->arena, text + 1, (size_t)(len - 2)); - } - if (text && text[0]) { - call.first_string_arg = text; - } - } - } else if (strcmp(vk, "identifier") == 0) { - /* Constant reference: url=MY_URL_CONSTANT */ - char *const_name = cbm_node_text(ctx->arena, val_node, ctx->source); - if (const_name) { - const char *resolved = lookup_string_constant(ctx, const_name); - if (resolved) { - call.first_string_arg = resolved; - } - } - } - continue; - } - - /* First positional string argument (fallback) */ - if (ai < 3) { - if (strcmp(ak, "string") == 0 || strcmp(ak, "string_literal") == 0 || - strcmp(ak, "interpreted_string_literal") == 0 || - strcmp(ak, "raw_string_literal") == 0) { - char *text = cbm_node_text(ctx->arena, arg, ctx->source); - if (text && text[0]) { - int len = (int)strlen(text); - if (len >= 2 && (text[0] == '"' || text[0] == '\'')) { - text = - cbm_arena_strndup(ctx->arena, text + 1, (size_t)(len - 2)); - len -= 2; - } - /* Validate printable */ - bool valid = (text != NULL && len > 0 && len < 512); - for (int vi = 0; vi < len && valid; vi++) { - if ((unsigned char)text[vi] < 0x20 && text[vi] != '\t') { - valid = false; - } - } - if (valid) { - call.first_string_arg = text; - } - } - break; - } - /* Positional constant reference: create_task(MY_URL) */ - if (strcmp(ak, "identifier") == 0) { - char *const_name = cbm_node_text(ctx->arena, arg, ctx->source); - if (const_name) { - const char *resolved = lookup_string_constant(ctx, const_name); - if (resolved) { - call.first_string_arg = resolved; - break; - } - } - } - } + call.first_string_arg = extract_url_or_topic_arg(ctx, args); + if (call.first_string_arg && call.first_string_arg[0] == '/') { + call.second_arg_name = extract_handler_arg(ctx, args); } - } - - /* Extract second argument name (handler ref for route registrations). */ - if (call.first_string_arg != NULL && call.first_string_arg[0] == '/' && - !ts_node_is_null(args)) { - uint32_t nc2 = ts_node_named_child_count(args); - for (uint32_t ai = 1; ai < nc2 && ai < 4 && !call.second_arg_name; ai++) { - TSNode arg2 = ts_node_named_child(args, ai); - const char *ak2 = ts_node_type(arg2); - if (strcmp(ak2, "identifier") == 0 || strcmp(ak2, "member_expression") == 0 || - strcmp(ak2, "selector_expression") == 0 || strcmp(ak2, "attribute") == 0 || - strcmp(ak2, "field_expression") == 0) { - call.second_arg_name = cbm_node_text(ctx->arena, arg2, ctx->source); - } - } - } - - /* B2+B3: Capture all arguments with expressions + field chains. */ - if (!ts_node_is_null(args)) { extract_call_args(ctx, args, &call); } @@ -595,20 +613,7 @@ void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Walk } } - // JSX component refs (TSX/JSX) if (ctx->language == CBM_LANG_TSX || ctx->language == CBM_LANG_JAVASCRIPT) { - if (strcmp(kind, "jsx_self_closing_element") == 0 || - strcmp(kind, "jsx_opening_element") == 0) { - TSNode name_node = ts_node_child_by_field_name(node, "name", 4); - if (!ts_node_is_null(name_node)) { - char *name = cbm_node_text(ctx->arena, name_node, ctx->source); - if (name && name[0] >= 'A' && name[0] <= 'Z') { - CBMCall call = {0}; - call.callee_name = name; - call.enclosing_func_qn = state->enclosing_func_qn; - cbm_calls_push(&ctx->result->calls, ctx->arena, call); - } - } - } + extract_jsx_component_ref(ctx, node, ts_node_type(node), state->enclosing_func_qn); } } diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index bb87e3303..a8c802849 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -39,12 +39,343 @@ static TSNode func_name_node(TSNode node) { return ts_node_child_by_field_name(node, "name", 4); } -// Resolve the name node for a function, handling language-specific quirks -// NOLINTNEXTLINE(readability-function-cognitive-complexity,readability-function-size) +// Lua: resolve anonymous function assignment name from parent assignment_statement. +static TSNode resolve_lua_func_name(TSNode node) { + TSNode parent = ts_node_parent(node); + if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "expression_list") == 0) { + parent = ts_node_parent(parent); + } + if (ts_node_is_null(parent) || strcmp(ts_node_type(parent), "assignment_statement") != 0) { + TSNode null_node = {0}; + return null_node; + } + TSNode vars = ts_node_child_by_field_name(parent, "variables", 9); + if (ts_node_is_null(vars)) { + uint32_t n = ts_node_child_count(parent); + for (uint32_t i = 0; i < n; i++) { + TSNode c = ts_node_child(parent, i); + if (strcmp(ts_node_type(c), "variable_list") == 0) { + vars = c; + break; + } + } + } + if (!ts_node_is_null(vars) && ts_node_child_count(vars) > 0) { + return ts_node_child(vars, 0); + } + TSNode null_node = {0}; + return null_node; +} + +// Julia: walk named children to find first identifier. +static TSNode resolve_julia_func_name(TSNode node) { + TSNode current = node; + for (int depth = 0; depth < 4; depth++) { + if (ts_node_named_child_count(current) == 0) { + break; + } + TSNode first = ts_node_named_child(current, 0); + const char *fk = ts_node_type(first); + if (strcmp(fk, "identifier") == 0 || strcmp(fk, "operator_identifier") == 0) { + return first; + } + current = first; + } + TSNode null_node = {0}; + return null_node; +} + +// OCaml: resolve value_definition name from let_binding→pattern. +static TSNode resolve_ocaml_func_name(TSNode node) { + TSNode binding = cbm_find_child_by_kind(node, "let_binding"); + if (!ts_node_is_null(binding)) { + TSNode pattern = ts_node_child_by_field_name(binding, "pattern", 7); + if (!ts_node_is_null(pattern)) { + return pattern; + } + } + TSNode null_node = {0}; + return null_node; +} + +// SQL: resolve create_function name from object_reference→identifier or direct identifier. +static TSNode resolve_sql_func_name(TSNode node) { + TSNode obj_ref = cbm_find_child_by_kind(node, "object_reference"); + if (!ts_node_is_null(obj_ref)) { + TSNode id = cbm_find_child_by_kind(obj_ref, "identifier"); + if (!ts_node_is_null(id)) { + return id; + } + } + return cbm_find_child_by_kind(node, "identifier"); +} + +// Zig: resolve test_declaration name from string→string_content. +static TSNode resolve_zig_test_name(TSNode node) { + TSNode str_node = cbm_find_child_by_kind(node, "string"); + if (!ts_node_is_null(str_node)) { + TSNode content = cbm_find_child_by_kind(str_node, "string_content"); + if (!ts_node_is_null(content)) { + return content; + } + } + TSNode null_node = {0}; + return null_node; +} + +// VimScript: resolve function_definition name from function_declaration child. +static TSNode resolve_vimscript_func_name(TSNode node) { + TSNode decl = cbm_find_child_by_kind(node, "function_declaration"); + if (!ts_node_is_null(decl) && ts_node_named_child_count(decl) > 0) { + return ts_node_named_child(decl, 0); + } + if (ts_node_named_child_count(node) > 0) { + return ts_node_named_child(node, 0); + } + TSNode null_node = {0}; + return null_node; +} + +// Resolve function name for scripting/niche languages (Lua, OCaml, SQL, Zig, VimScript, Julia). +static TSNode resolve_func_name_scripting(TSNode node, CBMLanguage lang, const char *kind) { + if (lang == CBM_LANG_LUA && strcmp(kind, "function_definition") == 0) { + return resolve_lua_func_name(node); + } + if (lang == CBM_LANG_OCAML && strcmp(kind, "value_definition") == 0) { + return resolve_ocaml_func_name(node); + } + if (lang == CBM_LANG_SQL && strcmp(kind, "create_function") == 0) { + return resolve_sql_func_name(node); + } + if (lang == CBM_LANG_ZIG && strcmp(kind, "test_declaration") == 0) { + return resolve_zig_test_name(node); + } + if (lang == CBM_LANG_VIMSCRIPT && strcmp(kind, "function_definition") == 0) { + return resolve_vimscript_func_name(node); + } + if (lang == CBM_LANG_JULIA && strcmp(kind, "function_definition") == 0) { + return resolve_julia_func_name(node); + } + + TSNode null_node = {0}; + return null_node; +} + +// Lean: resolve function name from declId field. +static TSNode resolve_lean_func_name(TSNode node, TSNode name) { + TSNode decl_id = ts_node_child_by_field_name(node, "declId", 6); + if (!ts_node_is_null(decl_id)) { + TSNode id = cbm_find_child_by_kind(decl_id, "ident"); + if (!ts_node_is_null(id)) { + return id; + } + if (ts_node_named_child_count(decl_id) > 0) { + return ts_node_named_child(decl_id, 0); + } + return decl_id; + } + if (!ts_node_is_null(name)) { + return name; + } + return cbm_find_child_by_kind(node, "ident"); +} + +// Haskell: resolve function name from first named child (variable/name). +static TSNode resolve_haskell_func_name(TSNode node) { + if (ts_node_named_child_count(node) > 0) { + TSNode head = ts_node_named_child(node, 0); + const char *hk = ts_node_type(head); + if (strcmp(hk, "variable") == 0 || strcmp(hk, "name") == 0) { + return head; + } + if (ts_node_named_child_count(head) > 0) { + TSNode v = ts_node_named_child(head, 0); + const char *vk = ts_node_type(v); + if (strcmp(vk, "variable") == 0 || strcmp(vk, "name") == 0) { + return v; + } + } + } + TSNode null_node = {0}; + return null_node; +} + +// CommonLisp: resolve defun name from function_name field or defun_header→sym_lit. +static TSNode resolve_commonlisp_func_name(TSNode node) { + TSNode fn = ts_node_child_by_field_name(node, "function_name", FIELD_LEN_FUNCTION_NAME); + if (!ts_node_is_null(fn)) { + return fn; + } + TSNode header = cbm_find_child_by_kind(node, "defun_header"); + if (!ts_node_is_null(header)) { + return cbm_find_child_by_kind(header, "sym_lit"); + } + TSNode null_node = {0}; + return null_node; +} + +// Makefile: resolve rule name from targets child or word fallback. +static TSNode resolve_makefile_func_name(TSNode node) { + TSNode targets = cbm_find_child_by_kind(node, "targets"); + if (!ts_node_is_null(targets) && ts_node_named_child_count(targets) > 0) { + return ts_node_named_child(targets, 0); + } + return cbm_find_child_by_kind(node, "word"); +} + +// Elm: resolve value_declaration name from functionDeclarationLeft field. +static TSNode resolve_elm_func_name(TSNode node) { + TSNode fdl = + ts_node_child_by_field_name(node, "functionDeclarationLeft", FIELD_LEN_FUNCTION_DECL_LEFT); + if (ts_node_is_null(fdl)) { + fdl = cbm_find_child_by_kind(node, "function_declaration_left"); + } + if (!ts_node_is_null(fdl) && ts_node_named_child_count(fdl) > 0) { + return ts_node_named_child(fdl, 0); + } + TSNode null_node = {0}; + return null_node; +} + +// Wolfram: resolve set/set_delayed name from LHS apply→user_symbol. +static TSNode resolve_wolfram_func_name(TSNode node) { + if (ts_node_named_child_count(node) > 0) { + TSNode lhs = ts_node_named_child(node, 0); + if (strcmp(ts_node_type(lhs), "apply") == 0 && ts_node_named_child_count(lhs) > 0) { + TSNode head = ts_node_named_child(lhs, 0); + if (strcmp(ts_node_type(head), "user_symbol") == 0) { + return head; + } + } + } + TSNode null_node = {0}; + return null_node; +} + +// Resolve function name for FP/scientific languages. +static TSNode resolve_func_name_fp(TSNode node, CBMLanguage lang, const char *kind, TSNode name) { + if (lang == CBM_LANG_COMMONLISP && strcmp(kind, "defun") == 0) { + return resolve_commonlisp_func_name(node); + } + + if (lang == CBM_LANG_MAKEFILE && strcmp(kind, "rule") == 0) { + return resolve_makefile_func_name(node); + } + + if (lang == CBM_LANG_HASKELL && strcmp(kind, "function") == 0) { + return resolve_haskell_func_name(node); + } + + if (lang == CBM_LANG_ELM && strcmp(kind, "value_declaration") == 0) { + return resolve_elm_func_name(node); + } + + if (lang == CBM_LANG_MATLAB && strcmp(kind, "function_definition") == 0) { + if (!ts_node_is_null(name)) { + return name; + } + return cbm_find_child_by_kind(node, "identifier"); + } + + if (lang == CBM_LANG_LEAN) { + return resolve_lean_func_name(node, name); + } + + if (lang == CBM_LANG_WOLFRAM && + (strcmp(kind, "set_delayed_top") == 0 || strcmp(kind, "set_top") == 0 || + strcmp(kind, "set_delayed") == 0 || strcmp(kind, "set") == 0)) { + return resolve_wolfram_func_name(node); + } + + TSNode null_node = {0}; + return null_node; +} + +// Check if a node type is a terminal C declarator name. +static bool is_c_terminal_name(const char *dk) { + return strcmp(dk, "identifier") == 0 || strcmp(dk, "field_identifier") == 0 || + strcmp(dk, "operator_name") == 0 || strcmp(dk, "operator_cast") == 0 || + strcmp(dk, "destructor_name") == 0; +} + +// Resolve name from a C++ qualified_identifier/scoped_identifier. +static TSNode resolve_qualified_name(TSNode decl) { + static const char *name_kinds[] = {"operator_name", "operator_cast", "destructor_name", + "identifier", "field_identifier", NULL}; + for (const char **k = name_kinds; *k; k++) { + TSNode found = cbm_find_child_by_kind(decl, *k); + if (!ts_node_is_null(found)) { + return found; + } + } + TSNode null_node = {0}; + return null_node; +} + +// Resolve function name from C/C++/CUDA/GLSL declarator chain. +static TSNode resolve_c_declarator_name(TSNode node) { + TSNode decl = ts_node_child_by_field_name(node, "declarator", 10); + for (int depth = 0; depth < 8 && !ts_node_is_null(decl); depth++) { + const char *dk = ts_node_type(decl); + if (is_c_terminal_name(dk)) { + return decl; + } + if (strcmp(dk, "qualified_identifier") == 0 || strcmp(dk, "scoped_identifier") == 0) { + return resolve_qualified_name(decl); + } + TSNode inner = ts_node_child_by_field_name(decl, "declarator", 10); + if (ts_node_is_null(inner) && ts_node_named_child_count(decl) > 0) { + inner = ts_node_named_child(decl, 0); + } + if (ts_node_is_null(inner)) { + break; + } + decl = inner; + } + TSNode null_node = {0}; + return null_node; +} + +// R: resolve function_definition name from parent binary_operator lhs. +static TSNode resolve_r_func_name(TSNode node) { + TSNode parent = ts_node_parent(node); + if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "binary_operator") == 0) { + TSNode lhs = ts_node_child_by_field_name(parent, "left", 4); + if (ts_node_is_null(lhs)) { + lhs = ts_node_child_by_field_name(parent, "lhs", 3); + } + if (ts_node_is_null(lhs) && ts_node_named_child_count(parent) > 0) { + lhs = ts_node_named_child(parent, 0); + } + if (!ts_node_is_null(lhs)) { + return lhs; + } + } + TSNode null_node = {0}; + return null_node; +} + +// Forward declaration for mutual recursion. +static TSNode resolve_func_name(TSNode node, CBMLanguage lang, const char *source); + +// C++/CUDA: resolve function name from template_declaration's inner function/declaration. +static TSNode resolve_template_inner_func_name(TSNode node, CBMLanguage lang, const char *source) { + uint32_t nc = ts_node_named_child_count(node); + for (uint32_t i = 0; i < nc; i++) { + TSNode ch = ts_node_named_child(node, i); + const char *ck = ts_node_type(ch); + if (strcmp(ck, "function_definition") == 0 || strcmp(ck, "declaration") == 0) { + return resolve_func_name(ch, lang, source); + } + } + TSNode null_node = {0}; + return null_node; +} + +// Resolve the name node for a function, handling language-specific quirks. static TSNode resolve_func_name(TSNode node, CBMLanguage lang, const char *source) { const char *kind = ts_node_type(node); - // Haskell: skip signature nodes if (lang == CBM_LANG_HASKELL && strcmp(kind, "signature") == 0) { TSNode null_node = {0}; return null_node; @@ -52,32 +383,14 @@ static TSNode resolve_func_name(TSNode node, CBMLanguage lang, const char *sourc TSNode name = func_name_node(node); - // R: function_definition — name on parent binary_operator lhs if (lang == CBM_LANG_R && strcmp(kind, "function_definition") == 0) { - TSNode parent = ts_node_parent(node); - if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "binary_operator") == 0) { - // Try field names first - TSNode lhs = ts_node_child_by_field_name(parent, "left", 4); - if (ts_node_is_null(lhs)) { - lhs = ts_node_child_by_field_name(parent, "lhs", 3); - } - // R grammar has no field names — first named child is the identifier - if (ts_node_is_null(lhs) && ts_node_named_child_count(parent) > 0) { - lhs = ts_node_named_child(parent, 0); - } - if (!ts_node_is_null(lhs)) { - return lhs; - } - } - TSNode null_node = {0}; - return null_node; + return resolve_r_func_name(node); } if (!ts_node_is_null(name)) { return name; } - // Swift: function_declaration has simple_identifier (no "name" field) if (lang == CBM_LANG_SWIFT && strcmp(kind, "function_declaration") == 0) { TSNode si = cbm_find_child_by_kind(node, "simple_identifier"); if (!ts_node_is_null(si)) { @@ -85,67 +398,6 @@ static TSNode resolve_func_name(TSNode node, CBMLanguage lang, const char *sourc } } - // Lua: anonymous function assignment — check parent assignment_statement or expression_list - if (lang == CBM_LANG_LUA && strcmp(kind, "function_definition") == 0) { - // Walk up: function_definition -> expression_list -> assignment_statement - TSNode parent = ts_node_parent(node); - if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "expression_list") == 0) { - parent = ts_node_parent(parent); - } - if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "assignment_statement") == 0) { - // Left side of assignment: variable_list -> variable - TSNode vars = ts_node_child_by_field_name(parent, "variables", 9); - if (ts_node_is_null(vars)) { - uint32_t n = ts_node_child_count(parent); - for (uint32_t i = 0; i < n; i++) { - TSNode c = ts_node_child(parent, i); - if (strcmp(ts_node_type(c), "variable_list") == 0) { - vars = c; - break; - } - } - } - if (!ts_node_is_null(vars) && ts_node_child_count(vars) > 0) { - return ts_node_child(vars, 0); - } - } - } - - // OCaml: value_definition -> let_binding with pattern field - if (lang == CBM_LANG_OCAML && strcmp(kind, "value_definition") == 0) { - TSNode binding = cbm_find_child_by_kind(node, "let_binding"); - if (!ts_node_is_null(binding)) { - TSNode pattern = ts_node_child_by_field_name(binding, "pattern", 7); - if (!ts_node_is_null(pattern)) { - return pattern; - } - } - } - - // SQL: create_function — name in object_reference > identifier - if (lang == CBM_LANG_SQL && strcmp(kind, "create_function") == 0) { - TSNode obj_ref = cbm_find_child_by_kind(node, "object_reference"); - if (!ts_node_is_null(obj_ref)) { - TSNode id = cbm_find_child_by_kind(obj_ref, "identifier"); - if (!ts_node_is_null(id)) { - return id; - } - } - return cbm_find_child_by_kind(node, "identifier"); - } - - // Zig: test_declaration — name is in string child - if (lang == CBM_LANG_ZIG && strcmp(kind, "test_declaration") == 0) { - TSNode str_node = cbm_find_child_by_kind(node, "string"); - if (!ts_node_is_null(str_node)) { - TSNode content = cbm_find_child_by_kind(str_node, "string_content"); - if (!ts_node_is_null(content)) { - return content; - } - } - } - - // Arrow function: name on parent variable_declarator if (strcmp(kind, "arrow_function") == 0) { TSNode parent = ts_node_parent(node); if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "variable_declarator") == 0) { @@ -153,240 +405,29 @@ static TSNode resolve_func_name(TSNode node, CBMLanguage lang, const char *sourc } } - // VimScript: function_definition — name is inside function_declaration child - // Both function_definition and function_declaration have production_id=0 (no field names) - if (lang == CBM_LANG_VIMSCRIPT && strcmp(kind, "function_definition") == 0) { - TSNode decl = cbm_find_child_by_kind(node, "function_declaration"); - if (!ts_node_is_null(decl)) { - // function_declaration: first named child is the identifier/name - uint32_t nc = ts_node_named_child_count(decl); - if (nc > 0) { - return ts_node_named_child(decl, 0); - } - } - // Fallback: first named child of function_definition directly - { - uint32_t nc = ts_node_named_child_count(node); - if (nc > 0) { - return ts_node_named_child(node, 0); - } + { + TSNode result = resolve_func_name_scripting(node, lang, kind); + if (!ts_node_is_null(result)) { + return result; } } - // Julia: function_definition — no field names defined (production_id=0) - // Name is inside first named child: identifier or call_expression(identifier, params) - if (lang == CBM_LANG_JULIA && strcmp(kind, "function_definition") == 0) { - TSNode current = node; - for (int depth = 0; depth < 4; depth++) { - uint32_t nc = ts_node_named_child_count(current); - if (nc == 0) { - break; - } - TSNode first = ts_node_named_child(current, 0); - const char *fk = ts_node_type(first); - if (strcmp(fk, "identifier") == 0 || strcmp(fk, "operator_identifier") == 0) { - return first; - } - current = first; - } - TSNode null_node = {0}; - return null_node; - } - - // CommonLisp: defun — name via function_name field (defun grammar uses field_function_name) - if (lang == CBM_LANG_COMMONLISP && strcmp(kind, "defun") == 0) { - TSNode fn = ts_node_child_by_field_name(node, "function_name", FIELD_LEN_FUNCTION_NAME); - if (!ts_node_is_null(fn)) { - return fn; - } - // Fallback: traverse defun_header -> first sym_lit child - TSNode header = cbm_find_child_by_kind(node, "defun_header"); - if (!ts_node_is_null(header)) { - return cbm_find_child_by_kind(header, "sym_lit"); + { + TSNode result = resolve_func_name_fp(node, lang, kind, name); + if (!ts_node_is_null(result)) { + return result; } } - // Makefile: rule — name is first word in the targets group - if (lang == CBM_LANG_MAKEFILE && strcmp(kind, "rule") == 0) { - TSNode targets = cbm_find_child_by_kind(node, "targets"); - if (!ts_node_is_null(targets)) { - uint32_t nc = ts_node_named_child_count(targets); - if (nc > 0) { - return ts_node_named_child(targets, 0); - } - } - // Fallback: first word child directly on rule - return cbm_find_child_by_kind(node, "word"); - } - - // Haskell: function — prod_id 151/152 has inherited field_name (handled above), - // prod_id 77 (no-argument function like `main = ...`) has NO field_name. - // Fallback: first named child of first child contains the variable name. - if (lang == CBM_LANG_HASKELL && strcmp(kind, "function") == 0) { - // func_name_node already returned null — prod_id 77 path - if (ts_node_named_child_count(node) > 0) { - TSNode head = ts_node_named_child(node, 0); - const char *hk = ts_node_type(head); - // Direct variable (function name without patterns) - if (strcmp(hk, "variable") == 0 || strcmp(hk, "name") == 0) { - return head; - } - // Function head node: first named child is the variable - if (ts_node_named_child_count(head) > 0) { - TSNode v = ts_node_named_child(head, 0); - const char *vk = ts_node_type(v); - if (strcmp(vk, "variable") == 0 || strcmp(vk, "name") == 0) { - return v; - } - } - } - TSNode null_node = {0}; - return null_node; - } - - // Elm: value_declaration — name is inside functionDeclarationLeft child (field 22 chars) - if (lang == CBM_LANG_ELM && strcmp(kind, "value_declaration") == 0) { - TSNode fdl = ts_node_child_by_field_name(node, "functionDeclarationLeft", - FIELD_LEN_FUNCTION_DECL_LEFT); - if (ts_node_is_null(fdl)) { - fdl = cbm_find_child_by_kind(node, "function_declaration_left"); - } - if (!ts_node_is_null(fdl) && ts_node_named_child_count(fdl) > 0) { - return ts_node_named_child(fdl, 0); - } - TSNode null_node = {0}; - return null_node; - } - - // MATLAB: function_definition — name via "name" field - if (lang == CBM_LANG_MATLAB && strcmp(kind, "function_definition") == 0) { - // MATLAB grammar: function [ret] = name(args) - // The "name" field should be set; fallback to first identifier child - if (!ts_node_is_null(name)) { - return name; - } - return cbm_find_child_by_kind(node, "identifier"); - } - - // Lean: def/theorem/instance/abbrev — name via declId field - if (lang == CBM_LANG_LEAN) { - // Lean grammar uses "declId" field for the name (6 chars) - TSNode decl_id = ts_node_child_by_field_name(node, "declId", 6); - if (!ts_node_is_null(decl_id)) { - // declId contains an identifier - TSNode id = cbm_find_child_by_kind(decl_id, "ident"); - if (!ts_node_is_null(id)) { - return id; - } - // Fallback: first named child of declId - if (ts_node_named_child_count(decl_id) > 0) { - return ts_node_named_child(decl_id, 0); - } - return decl_id; - } - // Fallback: look for "name" field or first identifier - if (!ts_node_is_null(name)) { - return name; - } - return cbm_find_child_by_kind(node, "ident"); - } - - // FORM: procedure_definition — name via "name" field (standard) - // Magma: function/procedure/intrinsic_definition — name via "name" field (standard) - // Both use standard "name" field which is handled above at line 33 - - // Wolfram: set_delayed_top/set_top — LHS is apply(user_symbol("f"), ...) for f[x_] := ... - if (lang == CBM_LANG_WOLFRAM && - (strcmp(kind, "set_delayed_top") == 0 || strcmp(kind, "set_top") == 0 || - strcmp(kind, "set_delayed") == 0 || strcmp(kind, "set") == 0)) { - if (ts_node_named_child_count(node) > 0) { - TSNode lhs = ts_node_named_child(node, 0); - if (strcmp(ts_node_type(lhs), "apply") == 0 && ts_node_named_child_count(lhs) > 0) { - TSNode head = ts_node_named_child(lhs, 0); - if (strcmp(ts_node_type(head), "user_symbol") == 0) { - return head; - } - } - } - TSNode null_node = {0}; - return null_node; - } - - // C++/CUDA: template_declaration wraps a function_definition — unwrap and resolve inner if ((lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA) && strcmp(kind, "template_declaration") == 0) { - // Find the inner function_definition or declaration child - uint32_t nc = ts_node_named_child_count(node); - for (uint32_t i = 0; i < nc; i++) { - TSNode ch = ts_node_named_child(node, i); - const char *ck = ts_node_type(ch); - if (strcmp(ck, "function_definition") == 0 || strcmp(ck, "declaration") == 0) { - return resolve_func_name(ch, lang, source); - } - } - TSNode null_node = {0}; - return null_node; + return resolve_template_inner_func_name(node, lang, source); } - // C/C++/CUDA/GLSL: function_definition — name is inside the declarator chain - // C grammar: function_definition{declarator:function_declarator{declarator:identifier}} if ((lang == CBM_LANG_C || lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA || lang == CBM_LANG_GLSL) && strcmp(kind, "function_definition") == 0) { - TSNode decl = ts_node_child_by_field_name(node, "declarator", 10); - for (int depth = 0; depth < 8 && !ts_node_is_null(decl); depth++) { - const char *dk = ts_node_type(decl); - if (strcmp(dk, "identifier") == 0) { - return decl; - } - if (strcmp(dk, "field_identifier") == 0) { - return decl; - } - // C++ operator functions: operator+, operator[], operator(), etc. - if (strcmp(dk, "operator_name") == 0 || strcmp(dk, "operator_cast") == 0) { - return decl; - } - // C++ destructor: ~ClassName - if (strcmp(dk, "destructor_name") == 0) { - return decl; - } - // C++ qualified name: Namespace::Function or Class::operator+ - if (strcmp(dk, "qualified_identifier") == 0 || strcmp(dk, "scoped_identifier") == 0) { - // Check for operator_name child first (e.g., Class::operator+) - TSNode op = cbm_find_child_by_kind(decl, "operator_name"); - if (!ts_node_is_null(op)) { - return op; - } - op = cbm_find_child_by_kind(decl, "operator_cast"); - if (!ts_node_is_null(op)) { - return op; - } - op = cbm_find_child_by_kind(decl, "destructor_name"); - if (!ts_node_is_null(op)) { - return op; - } - TSNode id = cbm_find_child_by_kind(decl, "identifier"); - if (!ts_node_is_null(id)) { - return id; - } - id = cbm_find_child_by_kind(decl, "field_identifier"); - if (!ts_node_is_null(id)) { - return id; - } - break; - } - // Unwrap pointer_declarator, reference_declarator, function_declarator, etc. - TSNode inner = ts_node_child_by_field_name(decl, "declarator", 10); - if (ts_node_is_null(inner) && ts_node_named_child_count(decl) > 0) { - inner = ts_node_named_child(decl, 0); - } - if (ts_node_is_null(inner)) { - break; - } - decl = inner; - } - TSNode null_node = {0}; - return null_node; + return resolve_c_declarator_name(node); } TSNode null_node = {0}; @@ -398,68 +439,79 @@ static bool is_js_exported(TSNode node) { return cbm_has_ancestor_kind(node, "export_statement", 4); } -// Extract docstring from the node's leading comment -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Check if a node is a comment node type. +static bool is_comment_node(const char *kind) { + return (strcmp(kind, "comment") == 0 || strcmp(kind, "block_comment") == 0 || + strcmp(kind, "line_comment") == 0); +} + +// Extract comment text, truncating to MAX_COMMENT_LEN. +static char *extract_comment_text(CBMArena *a, TSNode node, const char *source) { + char *text = cbm_node_text(a, node, source); + if (text && strlen(text) > MAX_COMMENT_LEN) { + text[MAX_COMMENT_LEN] = '\0'; + } + return text; +} + +// Go-specific: type_spec/type_alias comment is before the parent type_declaration. +static const char *extract_go_type_docstring(CBMArena *a, TSNode node, const char *source) { + const char *kind = ts_node_type(node); + if (strcmp(kind, "type_spec") != 0 && strcmp(kind, "type_alias") != 0) { + return NULL; + } + TSNode parent = ts_node_parent(node); + if (ts_node_is_null(parent) || strcmp(ts_node_type(parent), "type_declaration") != 0) { + return NULL; + } + TSNode pprev = ts_node_prev_sibling(parent); + if (!ts_node_is_null(pprev) && is_comment_node(ts_node_type(pprev))) { + return extract_comment_text(a, pprev, source); + } + return NULL; +} + +// Python-specific: docstring as first expression_statement -> string in function body. +static const char *extract_python_docstring(CBMArena *a, TSNode node, const char *source) { + TSNode body = ts_node_child_by_field_name(node, "body", 4); + if (ts_node_is_null(body) || ts_node_named_child_count(body) == 0) { + return NULL; + } + TSNode first = ts_node_named_child(body, 0); + if (ts_node_is_null(first) || strcmp(ts_node_type(first), "expression_statement") != 0) { + return NULL; + } + if (ts_node_named_child_count(first) == 0) { + return NULL; + } + TSNode str = ts_node_named_child(first, 0); + if (ts_node_is_null(str)) { + return NULL; + } + const char *sk = ts_node_type(str); + if (strcmp(sk, "string") == 0 || strcmp(sk, "concatenated_string") == 0) { + return extract_comment_text(a, str, source); + } + return NULL; +} + +// Extract docstring from the node's leading comment. static const char *extract_docstring(CBMArena *a, TSNode node, const char *source, CBMLanguage lang) { - // Go: type_spec is inside type_declaration; comment is before type_declaration if (lang == CBM_LANG_GO) { - const char *kind = ts_node_type(node); - if (strcmp(kind, "type_spec") == 0 || strcmp(kind, "type_alias") == 0) { - TSNode parent = ts_node_parent(node); - if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "type_declaration") == 0) { - TSNode pprev = ts_node_prev_sibling(parent); - if (!ts_node_is_null(pprev)) { - const char *ppk = ts_node_type(pprev); - if (strcmp(ppk, "comment") == 0 || strcmp(ppk, "block_comment") == 0 || - strcmp(ppk, "line_comment") == 0) { - char *text = cbm_node_text(a, pprev, source); - if (text && strlen(text) > MAX_COMMENT_LEN) { - text[MAX_COMMENT_LEN] = '\0'; - } - return text; - } - } - } + const char *doc = extract_go_type_docstring(a, node, source); + if (doc) { + return doc; } } - // Check previous sibling for comment TSNode prev = ts_node_prev_sibling(node); - if (!ts_node_is_null(prev)) { - const char *pk = ts_node_type(prev); - if (strcmp(pk, "comment") == 0 || strcmp(pk, "block_comment") == 0 || - strcmp(pk, "line_comment") == 0) { - char *text = cbm_node_text(a, prev, source); - if (text && strlen(text) > MAX_COMMENT_LEN) { - text[MAX_COMMENT_LEN] = '\0'; - } - return text; - } + if (!ts_node_is_null(prev) && is_comment_node(ts_node_type(prev))) { + return extract_comment_text(a, prev, source); } - // Python: docstring as first child expression_statement -> string inside function body if (lang == CBM_LANG_PYTHON) { - TSNode body = ts_node_child_by_field_name(node, "body", 4); - if (!ts_node_is_null(body) && ts_node_named_child_count(body) > 0) { - TSNode first = ts_node_named_child(body, 0); - if (!ts_node_is_null(first) && - strcmp(ts_node_type(first), "expression_statement") == 0) { - if (ts_node_named_child_count(first) > 0) { - TSNode str = ts_node_named_child(first, 0); - if (!ts_node_is_null(str)) { - const char *sk = ts_node_type(str); - if (strcmp(sk, "string") == 0 || strcmp(sk, "concatenated_string") == 0) { - char *text = cbm_node_text(a, str, source); - if (text && strlen(text) > MAX_COMMENT_LEN) { - text[MAX_COMMENT_LEN] = '\0'; - } - return text; - } - } - } - } - } + return extract_python_docstring(a, node, source); } return NULL; } @@ -500,7 +552,76 @@ static const char *decorator_method_name(const char *attr_text) { * Pure AST approach: walks the decorator node's call children to find: * 1. The function/attribute name → infer HTTP method * 2. The first string argument → route path */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Find the arguments node for a decorator call node. +static TSNode find_decorator_args(TSNode call_node) { + TSNode args = ts_node_child_by_field_name(call_node, "arguments", 9); + if (ts_node_is_null(args)) { + for (uint32_t ai = 0; ai < ts_node_named_child_count(call_node); ai++) { + TSNode ac = ts_node_named_child(call_node, ai); + if (strcmp(ts_node_type(ac), "argument_list") == 0) { + return ac; + } + } + } + return args; +} + +// Extract route path from decorator arguments (first string that starts with /). +static const char *extract_route_path_from_args(CBMArena *a, TSNode args, const char *source) { + uint32_t nc = ts_node_named_child_count(args); + for (uint32_t ai = 0; ai < nc && ai < 3; ai++) { + TSNode arg = ts_node_named_child(args, ai); + const char *ak = ts_node_type(arg); + if (strcmp(ak, "string") != 0 && strcmp(ak, "string_literal") != 0 && + strcmp(ak, "interpreted_string_literal") != 0) { + continue; + } + char *path = cbm_node_text(a, arg, source); + if (path) { + int plen = (int)strlen(path); + if (plen >= 2 && (path[0] == '"' || path[0] == '\'')) { + path = cbm_arena_strndup(a, path + 1, (size_t)(plen - 2)); + } + if (path && path[0] == '/') { + return path; + } + } + } + return NULL; +} + +// Try to extract a route from a single decorator call node. +// Returns true if a route method was found (even with fallback path "/"). +static bool try_route_from_decorator_call(CBMArena *a, TSNode dchild, const char *source, + const char **out_path, const char **out_method) { + TSNode fn = ts_node_child_by_field_name(dchild, "function", 8); + if (ts_node_is_null(fn)) { + fn = ts_node_named_child(dchild, 0); + } + if (ts_node_is_null(fn)) { + return false; + } + + char *fn_text = cbm_node_text(a, fn, source); + const char *method = decorator_method_name(fn_text); + if (!method) { + return false; + } + + TSNode args = find_decorator_args(dchild); + if (!ts_node_is_null(args)) { + const char *path = extract_route_path_from_args(a, args, source); + if (path) { + *out_path = path; + *out_method = method; + return true; + } + } + *out_path = "/"; + *out_method = method; + return true; +} + static void extract_route_from_decorators(CBMArena *a, TSNode func_node, const char *source, const CBMLangSpec *spec, const char **out_path, const char **out_method) { @@ -517,68 +638,14 @@ static void extract_route_from_decorators(CBMArena *a, TSNode func_node, const c break; } - /* Walk into the decorator to find a call expression with a path argument. - * Python decorator node structure: decorator → (call → attribute + argument_list) - * Java annotation: annotation → (name + arguments) */ uint32_t dc = ts_node_named_child_count(prev); for (uint32_t di = 0; di < dc; di++) { TSNode dchild = ts_node_named_child(prev, di); - const char *dk = ts_node_type(dchild); - - /* Python/JS: decorator contains a call node */ - if (strcmp(dk, "call") == 0) { - /* Get the function/attribute being called */ - TSNode fn = ts_node_child_by_field_name(dchild, "function", 8); - if (ts_node_is_null(fn)) { - fn = ts_node_named_child(dchild, 0); - } - if (!ts_node_is_null(fn)) { - char *fn_text = cbm_node_text(a, fn, source); - const char *method = decorator_method_name(fn_text); - if (method) { - /* Found a route decorator — extract path from arguments */ - TSNode args = ts_node_child_by_field_name(dchild, "arguments", 9); - if (ts_node_is_null(args)) { - /* Try argument_list as child */ - for (uint32_t ai = 0; ai < ts_node_named_child_count(dchild); ai++) { - TSNode ac = ts_node_named_child(dchild, ai); - if (strcmp(ts_node_type(ac), "argument_list") == 0) { - args = ac; - break; - } - } - } - if (!ts_node_is_null(args)) { - /* First string argument is the path */ - uint32_t nc = ts_node_named_child_count(args); - for (uint32_t ai = 0; ai < nc && ai < 3; ai++) { - TSNode arg = ts_node_named_child(args, ai); - const char *ak = ts_node_type(arg); - if (strcmp(ak, "string") == 0 || - strcmp(ak, "string_literal") == 0 || - strcmp(ak, "interpreted_string_literal") == 0) { - char *path = cbm_node_text(a, arg, source); - if (path) { - int plen = (int)strlen(path); - if (plen >= 2 && (path[0] == '"' || path[0] == '\'')) { - path = - cbm_arena_strndup(a, path + 1, (size_t)(plen - 2)); - } - if (path && path[0] == '/') { - *out_path = path; - *out_method = method; - return; - } - } - } - } - } - /* Route decorator but no path arg → path is "/" */ - *out_path = "/"; - *out_method = method; - return; - } - } + if (strcmp(ts_node_type(dchild), "call") != 0) { + continue; + } + if (try_route_from_decorator_call(a, dchild, source, out_path, out_method)) { + return; } } prev = ts_node_prev_sibling(prev); @@ -586,42 +653,69 @@ static void extract_route_from_decorators(CBMArena *a, TSNode func_node, const c } // Extract decorator names from preceding decorator/annotation nodes -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Count annotations inside a Java/Kotlin/C# "modifiers" node. +static int count_modifier_annotations(TSNode modifiers, const CBMLangSpec *spec) { + int count = 0; + uint32_t mc = ts_node_child_count(modifiers); + for (uint32_t mi = 0; mi < mc; mi++) { + TSNode mchild = ts_node_child(modifiers, mi); + if (cbm_kind_in_set(mchild, spec->decorator_node_types)) { + count++; + } + } + return count; +} + +// Find Java/Kotlin/C# modifiers node with annotations. +static TSNode find_jvm_modifiers(TSNode node, CBMLanguage lang) { + TSNode null_node = {0}; + if (lang != CBM_LANG_JAVA && lang != CBM_LANG_KOTLIN && lang != CBM_LANG_CSHARP) { + return null_node; + } + TSNode modifiers = ts_node_child_by_field_name(node, "modifiers", 9); + if (ts_node_is_null(modifiers)) { + modifiers = cbm_find_child_by_kind(node, "modifiers"); + } + return modifiers; +} + +// Collect decorator texts from a modifiers node into result array starting at idx. +static int collect_modifier_decorators(CBMArena *a, TSNode modifiers, const char *source, + const CBMLangSpec *spec, const char **result, int idx, + int max) { + uint32_t mc = ts_node_child_count(modifiers); + for (uint32_t mi = 0; mi < mc && idx < max; mi++) { + TSNode mchild = ts_node_child(modifiers, mi); + if (cbm_kind_in_set(mchild, spec->decorator_node_types)) { + result[idx++] = cbm_node_text(a, mchild, source); + } + } + return idx; +} + static const char **extract_decorators(CBMArena *a, TSNode node, const char *source, CBMLanguage lang, const CBMLangSpec *spec) { if (!spec->decorator_node_types || !spec->decorator_node_types[0]) { return NULL; } - // Count decorators (preceding siblings matching decorator types) int count = 0; TSNode prev = ts_node_prev_sibling(node); while (!ts_node_is_null(prev)) { if (cbm_kind_in_set(prev, spec->decorator_node_types)) { count++; } else { - break; // stop at first non-decorator + break; } prev = ts_node_prev_sibling(prev); } - // Java/Kotlin/C#: annotations live inside "modifiers" child, not as preceding siblings TSNode modifiers = {0}; int mod_count = 0; - if (count == 0 && - (lang == CBM_LANG_JAVA || lang == CBM_LANG_KOTLIN || lang == CBM_LANG_CSHARP)) { - modifiers = ts_node_child_by_field_name(node, "modifiers", 9); - if (ts_node_is_null(modifiers)) { - modifiers = cbm_find_child_by_kind(node, "modifiers"); - } + if (count == 0) { + modifiers = find_jvm_modifiers(node, lang); if (!ts_node_is_null(modifiers)) { - uint32_t mc = ts_node_child_count(modifiers); - for (uint32_t mi = 0; mi < mc; mi++) { - TSNode mchild = ts_node_child(modifiers, mi); - if (cbm_kind_in_set(mchild, spec->decorator_node_types)) { - mod_count++; - } - } + mod_count = count_modifier_annotations(modifiers, spec); } } @@ -636,7 +730,6 @@ static const char **extract_decorators(CBMArena *a, TSNode node, const char *sou } int idx = 0; - // Preceding siblings prev = ts_node_prev_sibling(node); while (!ts_node_is_null(prev) && idx < count) { if (cbm_kind_in_set(prev, spec->decorator_node_types)) { @@ -646,26 +739,93 @@ static const char **extract_decorators(CBMArena *a, TSNode node, const char *sou } prev = ts_node_prev_sibling(prev); } - // Modifiers children if (!ts_node_is_null(modifiers)) { - uint32_t mc = ts_node_child_count(modifiers); - for (uint32_t mi = 0; mi < mc && idx < total; mi++) { - TSNode mchild = ts_node_child(modifiers, mi); - if (cbm_kind_in_set(mchild, spec->decorator_node_types)) { - result[idx++] = cbm_node_text(a, mchild, source); - } - } + idx = collect_modifier_decorators(a, modifiers, source, spec, result, idx, total); } result[idx] = NULL; return result; } -// Extract base class names from a class node -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Extract base class name text from a single base_class child node. +static char *extract_cpp_base_text(CBMArena *a, TSNode bc, const char *source) { + const char *bk = ts_node_type(bc); + if (strcmp(bk, "access_specifier") == 0) { + return NULL; + } + if (strcmp(bk, "type_identifier") == 0 || strcmp(bk, "qualified_identifier") == 0 || + strcmp(bk, "scoped_identifier") == 0) { + return cbm_node_text(a, bc, source); + } + if (strcmp(bk, "template_type") == 0) { + TSNode tname = ts_node_child_by_field_name(bc, "name", 4); + if (!ts_node_is_null(tname)) { + return cbm_node_text(a, tname, source); + } + } + return NULL; +} + +// Extract base classes from a C++ base_class_clause node. +static const char **extract_cpp_base_classes(CBMArena *a, TSNode clause, const char *source) { + const char *bases[16]; + int base_count = 0; + uint32_t bnc = ts_node_named_child_count(clause); + for (uint32_t bi = 0; bi < bnc && base_count < MAX_BASES_MINUS_1; bi++) { + char *text = extract_cpp_base_text(a, ts_node_named_child(clause, bi), source); + if (text && text[0]) { + bases[base_count++] = text; + } + } + if (base_count > 0) { + const char **result = + (const char **)cbm_arena_alloc(a, (base_count + 1) * sizeof(const char *)); + if (result) { + for (int j = 0; j < base_count; j++) { + result[j] = bases[j]; + } + result[base_count] = NULL; + return result; + } + } + return NULL; +} + +// Build a single-element NULL-terminated base class array. +static const char **make_single_base(CBMArena *a, const char *text) { + if (!text || !text[0]) { + return NULL; + } + const char **result = (const char **)cbm_arena_alloc(a, sizeof(const char *) * 2); + if (result) { + result[0] = text; + result[1] = NULL; + } + return result; +} + +// Search children for a child matching one of the base_types and return its text as single base. +static const char **find_base_from_children(CBMArena *a, TSNode node, const char *source, + const char **base_types) { + uint32_t count = ts_node_child_count(node); + for (uint32_t i = 0; i < count; i++) { + TSNode child = ts_node_child(node, i); + const char *ck = ts_node_type(child); + for (const char **t = base_types; *t; t++) { + if (strcmp(ck, *t) == 0) { + const char **r = make_single_base(a, cbm_node_text(a, child, source)); + if (r) { + return r; + } + } + } + } + return NULL; +} + +// Extract base class names from a class node. static const char **extract_base_classes(CBMArena *a, TSNode node, const char *source, CBMLanguage lang) { (void)lang; - // Try common field names for superclass lists static const char *fields[] = {"superclass", "superclasses", "superinterfaces", @@ -678,63 +838,21 @@ static const char **extract_base_classes(CBMArena *a, TSNode node, const char *s for (const char **f = fields; *f; f++) { TSNode super = ts_node_child_by_field_name(node, *f, (uint32_t)strlen(*f)); if (!ts_node_is_null(super)) { - char *text = cbm_node_text(a, super, source); - if (text && text[0]) { - const char **result = (const char **)cbm_arena_alloc(a, sizeof(const char *) * 2); - if (result) { - result[0] = text; - result[1] = NULL; - return result; - } + const char **r = make_single_base(a, cbm_node_text(a, super, source)); + if (r) { + return r; } } } - // C/C++ specific: handle base_class_clause (contains access specifiers + type names) - { - uint32_t count = ts_node_child_count(node); - for (uint32_t i = 0; i < count; i++) { - TSNode child = ts_node_child(node, i); - if (strcmp(ts_node_type(child), "base_class_clause") == 0) { - // Extract type identifiers from base_class_clause, skipping access specifiers - const char *bases[16]; - int base_count = 0; - uint32_t bnc = ts_node_named_child_count(child); - for (uint32_t bi = 0; bi < bnc && base_count < MAX_BASES_MINUS_1; bi++) { - TSNode bc = ts_node_named_child(child, bi); - const char *bk = ts_node_type(bc); - if (strcmp(bk, "access_specifier") == 0) { - continue; - } - // type_identifier, qualified_identifier, scoped_identifier, template_type - if (strcmp(bk, "type_identifier") == 0 || - strcmp(bk, "qualified_identifier") == 0 || - strcmp(bk, "scoped_identifier") == 0) { - char *text = cbm_node_text(a, bc, source); - if (text && text[0]) { - bases[base_count++] = text; - } - } else if (strcmp(bk, "template_type") == 0) { - // For template base: extract just the template name (not args) - TSNode tname = ts_node_child_by_field_name(bc, "name", 4); - if (!ts_node_is_null(tname)) { - char *text = cbm_node_text(a, tname, source); - if (text && text[0]) { - bases[base_count++] = text; - } - } - } - } - if (base_count > 0) { - const char **result = - (const char **)cbm_arena_alloc(a, (base_count + 1) * sizeof(const char *)); - if (result) { - for (int j = 0; j < base_count; j++) { - result[j] = bases[j]; - } - result[base_count] = NULL; - return result; - } - } + + // C/C++: handle base_class_clause + uint32_t count = ts_node_child_count(node); + for (uint32_t i = 0; i < count; i++) { + TSNode child = ts_node_child(node, i); + if (strcmp(ts_node_type(child), "base_class_clause") == 0) { + const char **result = extract_cpp_base_classes(a, child, source); + if (result) { + return result; } } } @@ -751,26 +869,7 @@ static const char **extract_base_classes(CBMArena *a, TSNode node, const char *s "argument_list", "inheritance_specifier", NULL}; - uint32_t count = ts_node_child_count(node); - for (uint32_t i = 0; i < count; i++) { - TSNode child = ts_node_child(node, i); - const char *ck = ts_node_type(child); - for (const char **t = base_types; *t; t++) { - if (strcmp(ck, *t) == 0) { - char *text = cbm_node_text(a, child, source); - if (text && text[0]) { - const char **result = - (const char **)cbm_arena_alloc(a, sizeof(const char *) * 2); - if (result) { - result[0] = text; - result[1] = NULL; - return result; - } - } - } - } - } - return NULL; + return find_base_from_children(a, node, source, base_types); } // Classify class label from AST node kind @@ -846,7 +945,38 @@ static char *clean_type_name(CBMArena *a, const char *raw) { // Extract param_names from a parameter list node. // Returns NULL-terminated arena-allocated array. -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Extract the parameter name from a single parameter AST node. +static char *resolve_param_name(CBMArena *a, TSNode param, const char *source) { + const char *pk = ts_node_type(param); + + if (strcmp(pk, "parameter_declaration") == 0) { + TSNode nm = ts_node_child_by_field_name(param, "name", 4); + if (!ts_node_is_null(nm)) { + return cbm_node_text(a, nm, source); + } + return NULL; + } + if (strcmp(pk, "identifier") == 0) { + return cbm_node_text(a, param, source); + } + if (strcmp(pk, "formal_parameter") == 0 || strcmp(pk, "parameter") == 0 || + strcmp(pk, "required_parameter") == 0 || strcmp(pk, "optional_parameter") == 0 || + strcmp(pk, "simple_parameter") == 0 || strcmp(pk, "typed_parameter") == 0 || + strcmp(pk, "default_parameter") == 0 || strcmp(pk, "typed_default_parameter") == 0) { + TSNode nm = ts_node_child_by_field_name(param, "name", 4); + if (ts_node_is_null(nm)) { + nm = ts_node_child_by_field_name(param, "pattern", 7); + } + if (!ts_node_is_null(nm)) { + if (strcmp(ts_node_type(nm), "identifier") == 0 || + strcmp(ts_node_type(nm), "simple_identifier") == 0) { + return cbm_node_text(a, nm, source); + } + } + } + return NULL; +} + static const char **extract_param_names(CBMArena *a, TSNode params, const char *source, CBMLanguage lang) { (void)lang; @@ -864,37 +994,7 @@ static const char **extract_param_names(CBMArena *a, TSNode params, const char * continue; } - const char *pk = ts_node_type(param); - char *name_text = NULL; - - // Go: parameter_declaration has "name" field - if (strcmp(pk, "parameter_declaration") == 0) { - TSNode nm = ts_node_child_by_field_name(param, "name", 4); - if (!ts_node_is_null(nm)) { - name_text = cbm_node_text(a, nm, source); - } - } - // Python: identifier is a bare parameter name (def f(x, y)) - else if (strcmp(pk, "identifier") == 0) { - name_text = cbm_node_text(a, param, source); - } - // Generic: try "name" field on parameter nodes - else if (strcmp(pk, "formal_parameter") == 0 || strcmp(pk, "parameter") == 0 || - strcmp(pk, "required_parameter") == 0 || strcmp(pk, "optional_parameter") == 0 || - strcmp(pk, "simple_parameter") == 0 || strcmp(pk, "typed_parameter") == 0 || - strcmp(pk, "default_parameter") == 0 || - strcmp(pk, "typed_default_parameter") == 0) { - TSNode nm = ts_node_child_by_field_name(param, "name", 4); - if (ts_node_is_null(nm)) { - nm = ts_node_child_by_field_name(param, "pattern", 7); - } - if (!ts_node_is_null(nm)) { - if (strcmp(ts_node_type(nm), "identifier") == 0 || - strcmp(ts_node_type(nm), "simple_identifier") == 0) { - name_text = cbm_node_text(a, nm, source); - } - } - } + char *name_text = resolve_param_name(a, param, source); if (name_text && name_text[0]) { names[count++] = name_text; @@ -916,7 +1016,50 @@ static const char **extract_param_names(CBMArena *a, TSNode params, const char * // Extract return_types from a return type node. // Parses Go-style multi-return (T1, T2) and single return types. // Returns NULL-terminated arena-allocated array. -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Clean a type text and add to types array if valid. +static void add_cleaned_type(CBMArena *a, const char **types, int *count, char *type_text) { + if (!type_text || !type_text[0]) { + return; + } + char *cleaned = clean_type_name(a, type_text); + if (cleaned && cleaned[0]) { + types[(*count)++] = cleaned; + } +} + +// Extract Go multi-return types from a parameter_list result node. +static void extract_go_multi_return(CBMArena *a, TSNode rt_node, const char *source, + const char **types, int *count) { + uint32_t nc = ts_node_child_count(rt_node); + for (uint32_t i = 0; i < nc && *count < MAX_RETURN_TYPES_MINUS_1; i++) { + TSNode child = ts_node_child(rt_node, i); + if (ts_node_is_null(child) || !ts_node_is_named(child)) { + continue; + } + if (strcmp(ts_node_type(child), "parameter_declaration") == 0) { + TSNode tn = ts_node_child_by_field_name(child, "type", 4); + if (!ts_node_is_null(tn)) { + add_cleaned_type(a, types, count, cbm_node_text(a, tn, source)); + } + } else { + add_cleaned_type(a, types, count, cbm_node_text(a, child, source)); + } + } +} + +// Build a NULL-terminated arena-allocated string array from a types buffer. +static const char **build_type_array(CBMArena *a, const char **types, int count) { + if (count == 0) { + return NULL; + } + const char **result = (const char **)cbm_arena_alloc(a, (count + 1) * sizeof(const char *)); + for (int i = 0; i < count; i++) { + result[i] = types[i]; + } + result[count] = NULL; + return result; +} + static const char **extract_return_types(CBMArena *a, TSNode rt_node, const char *source, CBMLanguage lang) { (void)lang; @@ -927,73 +1070,131 @@ static const char **extract_return_types(CBMArena *a, TSNode rt_node, const char const char *types[16]; int count = 0; - const char *kind = ts_node_type(rt_node); - - // Go: parameter_list as result type means multi-return - if (strcmp(kind, "parameter_list") == 0) { - uint32_t nc = ts_node_child_count(rt_node); - for (uint32_t i = 0; i < nc && count < MAX_RETURN_TYPES_MINUS_1; i++) { - TSNode child = ts_node_child(rt_node, i); - if (ts_node_is_null(child) || !ts_node_is_named(child)) { - continue; - } - const char *ck = ts_node_type(child); - if (strcmp(ck, "parameter_declaration") == 0) { - // Get the type from the parameter_declaration - TSNode tn = ts_node_child_by_field_name(child, "type", 4); - if (!ts_node_is_null(tn)) { - char *type_text = cbm_node_text(a, tn, source); - if (type_text && type_text[0]) { - char *cleaned = clean_type_name(a, type_text); - if (cleaned && cleaned[0]) { - types[count++] = cleaned; - } - } - } - } else { - // Bare type in result list - char *type_text = cbm_node_text(a, child, source); - if (type_text && type_text[0]) { - char *cleaned = clean_type_name(a, type_text); - if (cleaned && cleaned[0]) { - types[count++] = cleaned; - } - } - } - } + if (strcmp(ts_node_type(rt_node), "parameter_list") == 0) { + extract_go_multi_return(a, rt_node, source, types, &count); } else { - // Single return type - char *type_text = cbm_node_text(a, rt_node, source); - if (type_text && type_text[0]) { - char *cleaned = clean_type_name(a, type_text); - if (cleaned && cleaned[0]) { - types[count++] = cleaned; - } - } + add_cleaned_type(a, types, &count, cbm_node_text(a, rt_node, source)); } - if (count == 0) { - return NULL; - } - - const char **result = (const char **)cbm_arena_alloc(a, (count + 1) * sizeof(const char *)); - for (int i = 0; i < count; i++) { - result[i] = types[i]; - } - result[count] = NULL; - return result; + return build_type_array(a, types, count); } // Extract param_types from a parameter list node. // Returns NULL-terminated arena-allocated array. -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Extract type text from a TypeScript type_annotation child. +static char *extract_ts_param_type(CBMArena *a, TSNode param, const char *source) { + TSNode ta = cbm_find_child_by_kind(param, "type_annotation"); + if (ts_node_is_null(ta)) { + return NULL; + } + uint32_t tanc = ts_node_named_child_count(ta); + for (uint32_t ti = 0; ti < tanc; ti++) { + TSNode tch = ts_node_named_child(ta, ti); + if (ts_node_is_null(tch)) { + continue; + } + const char *tk = ts_node_type(tch); + if (strcmp(tk, "type_identifier") == 0 || strcmp(tk, "generic_type") == 0 || + strcmp(tk, "predefined_type") == 0) { + return cbm_node_text(a, tch, source); + } + } + return NULL; +} + +// Check if a param node type is a generic parameter-like node. +static bool is_generic_param_kind(const char *pk) { + return strcmp(pk, "formal_parameter") == 0 || strcmp(pk, "parameter") == 0 || + strcmp(pk, "parameter_declaration") == 0 || strcmp(pk, "spread_parameter") == 0 || + strcmp(pk, "simple_parameter") == 0 || strcmp(pk, "variadic_parameter") == 0 || + strcmp(pk, "typed_parameter") == 0; +} + +// Resolve param type for JVM/misc languages (Kotlin, Scala, Dart, Groovy, OCaml). +static char *resolve_jvm_param_type(CBMArena *a, TSNode param, const char *pk, const char *source, + CBMLanguage lang) { + if (strcmp(pk, "parameter") != 0 && strcmp(pk, "formal_parameter") != 0) { + return NULL; + } + if (lang == CBM_LANG_KOTLIN) { + TSNode tn = ts_node_child_by_field_name(param, "type", 4); + if (!ts_node_is_null(tn)) { + return cbm_node_text(a, tn, source); + } + TSNode ut = cbm_find_child_by_kind(param, "user_type"); + return ts_node_is_null(ut) ? NULL : cbm_node_text(a, ut, source); + } + if (lang == CBM_LANG_SCALA || lang == CBM_LANG_DART) { + TSNode tid = cbm_find_child_by_kind(param, "type_identifier"); + return ts_node_is_null(tid) ? NULL : cbm_node_text(a, tid, source); + } + if (lang == CBM_LANG_GROOVY) { + TSNode tn = ts_node_child_by_field_name(param, "type", 4); + return ts_node_is_null(tn) ? NULL : cbm_node_text(a, tn, source); + } + if (lang == CBM_LANG_OCAML) { + TSNode tp = cbm_find_child_by_kind(param, "typed_pattern"); + if (!ts_node_is_null(tp)) { + TSNode tn = ts_node_child_by_field_name(tp, "type", 4); + if (!ts_node_is_null(tn)) { + return cbm_node_text(a, tn, source); + } + } + return NULL; + } + return NULL; +} + +// Resolve parameter type text for a single param node. +static char *resolve_param_type_text(CBMArena *a, TSNode param, const char *source, + CBMLanguage lang) { + const char *pk = ts_node_type(param); + + if (lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX) { + if (strcmp(pk, "required_parameter") == 0 || strcmp(pk, "optional_parameter") == 0) { + return extract_ts_param_type(a, param, source); + } + return NULL; + } + + if (lang == CBM_LANG_KOTLIN || lang == CBM_LANG_SCALA || lang == CBM_LANG_DART || + lang == CBM_LANG_GROOVY || lang == CBM_LANG_OCAML) { + return resolve_jvm_param_type(a, param, pk, source, lang); + } + + // Generic: parameter-like nodes with "type" field + if (is_generic_param_kind(pk)) { + TSNode tn = ts_node_child_by_field_name(param, "type", 4); + if (!ts_node_is_null(tn)) { + return cbm_node_text(a, tn, source); + } + } + return NULL; +} + +// Add a cleaned, deduplicated type to the types array. +static void add_dedup_type(CBMArena *a, const char **types, int *count, char *type_text) { + if (!type_text || !type_text[0]) { + return; + } + char *cleaned = clean_type_name(a, type_text); + if (!cleaned || !cleaned[0] || is_builtin_type(cleaned)) { + return; + } + for (int j = 0; j < *count; j++) { + if (strcmp(types[j], cleaned) == 0) { + return; + } + } + types[(*count)++] = cleaned; +} + static const char **extract_param_types(CBMArena *a, TSNode params, const char *source, CBMLanguage lang) { if (ts_node_is_null(params)) { return NULL; } - // Temporary buffer (max 32 param types) const char *types[32]; int count = 0; @@ -1003,132 +1204,13 @@ static const char **extract_param_types(CBMArena *a, TSNode params, const char * if (ts_node_is_null(param) || !ts_node_is_named(param)) { continue; } - - const char *pk = ts_node_type(param); - char *type_text = NULL; - - switch (lang) { - case CBM_LANG_TYPESCRIPT: - case CBM_LANG_TSX: { - // TS: required_parameter/optional_parameter -> type_annotation child - if (strcmp(pk, "required_parameter") == 0 || strcmp(pk, "optional_parameter") == 0) { - TSNode ta = cbm_find_child_by_kind(param, "type_annotation"); - if (!ts_node_is_null(ta)) { - // type_annotation contains ": Type" — get the type identifier - uint32_t tanc = ts_node_named_child_count(ta); - for (uint32_t ti = 0; ti < tanc; ti++) { - TSNode tch = ts_node_named_child(ta, ti); - if (!ts_node_is_null(tch)) { - const char *tk = ts_node_type(tch); - if (strcmp(tk, "type_identifier") == 0 || - strcmp(tk, "generic_type") == 0 || - strcmp(tk, "predefined_type") == 0) { - type_text = cbm_node_text(a, tch, source); - break; - } - } - } - } - } - break; - } - case CBM_LANG_KOTLIN: { - // Kotlin: parameter -> "type" field or user_type child - if (strcmp(pk, "parameter") == 0) { - TSNode tn = ts_node_child_by_field_name(param, "type", 4); - if (!ts_node_is_null(tn)) { - type_text = cbm_node_text(a, tn, source); - } else { - TSNode ut = cbm_find_child_by_kind(param, "user_type"); - if (!ts_node_is_null(ut)) { - type_text = cbm_node_text(a, ut, source); - } - } - } - break; - } - case CBM_LANG_SCALA: { - // Scala: parameter -> type_identifier child - if (strcmp(pk, "parameter") == 0) { - TSNode tid = cbm_find_child_by_kind(param, "type_identifier"); - if (!ts_node_is_null(tid)) { - type_text = cbm_node_text(a, tid, source); - } - } - break; - } - case CBM_LANG_DART: { - // Dart: formal_parameter -> type_identifier child - if (strcmp(pk, "formal_parameter") == 0) { - TSNode tid = cbm_find_child_by_kind(param, "type_identifier"); - if (!ts_node_is_null(tid)) { - type_text = cbm_node_text(a, tid, source); - } - } - break; - } - case CBM_LANG_GROOVY: { - // Groovy: parameter -> "type" field - if (strcmp(pk, "parameter") == 0) { - TSNode tn = ts_node_child_by_field_name(param, "type", 4); - if (!ts_node_is_null(tn)) { - type_text = cbm_node_text(a, tn, source); - } - } - break; - } - case CBM_LANG_OCAML: { - // OCaml: parameter -> typed_pattern -> "type" field - if (strcmp(pk, "parameter") == 0) { - TSNode tp = cbm_find_child_by_kind(param, "typed_pattern"); - if (!ts_node_is_null(tp)) { - TSNode tn = ts_node_child_by_field_name(tp, "type", 4); - if (!ts_node_is_null(tn)) { - type_text = cbm_node_text(a, tn, source); - } - } - } - break; - } - default: { - // Generic: formal_parameter, parameter, parameter_declaration, - // spread_parameter, simple_parameter, variadic_parameter -> "type" field - if (strcmp(pk, "formal_parameter") == 0 || strcmp(pk, "parameter") == 0 || - strcmp(pk, "parameter_declaration") == 0 || strcmp(pk, "spread_parameter") == 0 || - strcmp(pk, "simple_parameter") == 0 || strcmp(pk, "variadic_parameter") == 0 || - strcmp(pk, "typed_parameter") == 0) { - TSNode tn = ts_node_child_by_field_name(param, "type", 4); - if (!ts_node_is_null(tn)) { - type_text = cbm_node_text(a, tn, source); - } - } - break; - } - } - - if (type_text && type_text[0]) { - char *cleaned = clean_type_name(a, type_text); - if (cleaned && cleaned[0] && !is_builtin_type(cleaned)) { - // Deduplicate - bool dup = false; - for (int j = 0; j < count; j++) { - if (strcmp(types[j], cleaned) == 0) { - dup = true; - break; - } - } - if (!dup) { - types[count++] = cleaned; - } - } - } + add_dedup_type(a, types, &count, resolve_param_type_text(a, param, source, lang)); } if (count == 0) { return NULL; } - // Build NULL-terminated array const char **result = (const char **)cbm_arena_alloc(a, (count + 1) * sizeof(const char *)); for (int i = 0; i < count; i++) { result[i] = types[i]; @@ -1139,7 +1221,63 @@ static const char **extract_param_types(CBMArena *a, TSNode params, const char * // --- Function definition extraction --- -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// For C++/CUDA template_declaration, find the inner function_definition or declaration. +static TSNode unwrap_template_inner(TSNode node, CBMLanguage lang) { + if ((lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA) && + strcmp(ts_node_type(node), "template_declaration") == 0) { + uint32_t nc = ts_node_named_child_count(node); + for (uint32_t i = 0; i < nc; i++) { + TSNode ch = ts_node_named_child(node, i); + const char *ck = ts_node_type(ch); + if (strcmp(ck, "function_definition") == 0 || strcmp(ck, "declaration") == 0) { + return ch; + } + } + } + return node; +} + +// C/C++/CUDA/GLSL: parameters live on function_declarator inside declarator chain. +static TSNode find_c_params(TSNode func_node) { + TSNode decl = ts_node_child_by_field_name(func_node, "declarator", 10); + for (int d = 0; d < 5 && !ts_node_is_null(decl); d++) { + TSNode params = ts_node_child_by_field_name(decl, "parameters", 10); + if (!ts_node_is_null(params)) { + return params; + } + decl = ts_node_child_by_field_name(decl, "declarator", 10); + } + TSNode null_node = {0}; + return null_node; +} + +// C++: resolve trailing return type (auto f() -> Type) on a declarator node. +// Updates def->return_type and def->return_types if trailing type found. +static void resolve_cpp_trailing_return(CBMArena *a, TSNode func_node, const char *source, + CBMDefinition *def) { + TSNode declarator = ts_node_child_by_field_name(func_node, "declarator", 10); + if (ts_node_is_null(declarator)) { + return; + } + uint32_t nc = ts_node_named_child_count(declarator); + for (uint32_t i = 0; i < nc; i++) { + TSNode ch = ts_node_named_child(declarator, i); + if (strcmp(ts_node_type(ch), "trailing_return_type") == 0) { + TSNode type_desc = ts_node_named_child_count(ch) > 0 ? ts_node_named_child(ch, 0) : ch; + def->return_type = cbm_node_text(a, type_desc, source); + if (def->return_type && def->return_type[0]) { + const char **rt = (const char **)cbm_arena_alloc(a, 2 * sizeof(const char *)); + if (rt) { + rt[0] = def->return_type; + rt[1] = NULL; + def->return_types = rt; + } + } + break; + } + } +} + static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) { CBMArena *a = ctx->arena; @@ -1153,21 +1291,7 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec return; } - // For template_declaration, use the inner function_definition for field lookups - // (parameters, return type, etc. are on the inner node, not the template wrapper) - TSNode func_node = node; - if ((ctx->language == CBM_LANG_CPP || ctx->language == CBM_LANG_CUDA) && - strcmp(ts_node_type(node), "template_declaration") == 0) { - uint32_t nc = ts_node_named_child_count(node); - for (uint32_t i = 0; i < nc; i++) { - TSNode ch = ts_node_named_child(node, i); - const char *ck = ts_node_type(ch); - if (strcmp(ck, "function_definition") == 0 || strcmp(ck, "declaration") == 0) { - func_node = ch; - break; - } - } - } + TSNode func_node = unwrap_template_inner(node, ctx->language); CBMDefinition def; memset(&def, 0, sizeof(def)); @@ -1183,18 +1307,10 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec // Parameters — use func_node (inner function for templates) TSNode params = ts_node_child_by_field_name(func_node, "parameters", 10); - // C/C++/CUDA/GLSL: parameters live on function_declarator inside declarator chain if (ts_node_is_null(params) && (ctx->language == CBM_LANG_C || ctx->language == CBM_LANG_CPP || ctx->language == CBM_LANG_CUDA || ctx->language == CBM_LANG_GLSL)) { - TSNode decl = ts_node_child_by_field_name(func_node, "declarator", 10); - for (int d = 0; d < 5 && !ts_node_is_null(decl); d++) { - params = ts_node_child_by_field_name(decl, "parameters", 10); - if (!ts_node_is_null(params)) { - break; - } - decl = ts_node_child_by_field_name(decl, "declarator", 10); - } + params = find_c_params(func_node); } if (!ts_node_is_null(params)) { def.signature = cbm_node_text(a, params, ctx->source); @@ -1213,35 +1329,10 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec } } - // C++: trailing return type (auto f() -> Type) — override "auto" with actual type + // C++: trailing return type (auto f() -> Type) if (def.return_type && strcmp(def.return_type, "auto") == 0 && (ctx->language == CBM_LANG_CPP || ctx->language == CBM_LANG_CUDA)) { - TSNode declarator = ts_node_child_by_field_name(func_node, "declarator", 10); - if (!ts_node_is_null(declarator)) { - // trailing_return_type is a child of the function_declarator - uint32_t nc = ts_node_named_child_count(declarator); - for (uint32_t i = 0; i < nc; i++) { - TSNode ch = ts_node_named_child(declarator, i); - if (strcmp(ts_node_type(ch), "trailing_return_type") == 0) { - // trailing_return_type contains a type_descriptor as first named child - TSNode type_desc = - ts_node_named_child_count(ch) > 0 ? ts_node_named_child(ch, 0) : ch; - def.return_type = cbm_node_text(a, type_desc, ctx->source); - // Also update return_types array (so registration uses trailing type, not - // "auto") - if (def.return_type && def.return_type[0]) { - const char **rt = - (const char **)cbm_arena_alloc(a, 2 * sizeof(const char *)); - if (rt) { - rt[0] = def.return_type; - rt[1] = NULL; - def.return_types = rt; - } - } - break; - } - } - } + resolve_cpp_trailing_return(a, func_node, ctx->source, &def); } // Receiver (Go methods) @@ -1281,204 +1372,170 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec // --- Class definition extraction --- -// NOLINTNEXTLINE(readability-function-cognitive-complexity,readability-function-size) +// Push a simple class definition (used by config language extractors). +static void push_simple_class_def(CBMExtractCtx *ctx, TSNode node, char *name, const char *label) { + CBMArena *a = ctx->arena; + CBMDefinition def; + memset(&def, 0, sizeof(def)); + def.name = name; + def.qualified_name = cbm_fqn_compute(a, ctx->project, ctx->rel_path, name); + def.label = label; + def.file_path = ctx->rel_path; + def.start_line = ts_node_start_point(node).row + 1; + def.end_line = ts_node_end_point(node).row + 1; + def.is_exported = true; + cbm_defs_push(&ctx->result->defs, a, def); +} + +// Find TOML table key name from children. +static char *find_toml_key_name(CBMArena *a, TSNode node, const char *source) { + uint32_t nc = ts_node_child_count(node); + for (uint32_t i = 0; i < nc; i++) { + TSNode child = ts_node_child(node, i); + const char *ck = ts_node_type(child); + if (strcmp(ck, "bare_key") == 0 || strcmp(ck, "dotted_key") == 0 || + strcmp(ck, "quoted_key") == 0 || strcmp(ck, "key") == 0) { + return cbm_node_text(a, child, source); + } + } + return NULL; +} + +// Extract XML element name from start_tag/self_closing_tag children. +static char *find_xml_element_name(CBMArena *a, TSNode node, const char *source) { + uint32_t nc = ts_node_child_count(node); + for (uint32_t i = 0; i < nc; i++) { + TSNode child = ts_node_child(node, i); + const char *ck = ts_node_type(child); + if (strcmp(ck, "start_tag") == 0 || strcmp(ck, "self_closing_tag") == 0 || + strcmp(ck, "STag") == 0 || strcmp(ck, "EmptyElemTag") == 0) { + uint32_t tnc = ts_node_child_count(child); + for (uint32_t j = 0; j < tnc; j++) { + TSNode tag = ts_node_child(child, j); + const char *tk = ts_node_type(tag); + if (strcmp(tk, "tag_name") == 0 || strcmp(tk, "Name") == 0) { + return cbm_node_text(a, tag, source); + } + } + } + } + // Fallback: try "Name" field directly for some XML grammars + TSNode name_child = cbm_find_child_by_kind(node, "Name"); + if (!ts_node_is_null(name_child)) { + return cbm_node_text(a, name_child, source); + } + return NULL; +} + +// Extract text from an atx_heading node (# Title). +static char *extract_atx_heading_text(CBMArena *a, TSNode node, const char *source) { + uint32_t nc = ts_node_child_count(node); + for (uint32_t i = 0; i < nc; i++) { + TSNode child = ts_node_child(node, i); + const char *ck = ts_node_type(child); + if (strcmp(ck, "heading_content") == 0 || strcmp(ck, "inline") == 0) { + return cbm_node_text(a, child, source); + } + } + // Fallback: strip leading # and spaces from full text + char *full = cbm_node_text(a, node, source); + if (full) { + char *p = full; + while (*p == '#') { + p++; + } + while (*p == ' ') { + p++; + } + if (*p) { + return cbm_arena_strdup(a, p); + } + } + return NULL; +} + +// Trim trailing whitespace/newlines from a heading name in-place. +static char *trim_heading_name(char *name) { + if (!name || !name[0]) { + return NULL; + } + size_t len = strlen(name); + while (len > 0 && (name[len - 1] == '\n' || name[len - 1] == '\r' || name[len - 1] == ' ')) { + name[len - 1] = '\0'; + len--; + } + return (name[0]) ? name : NULL; +} + +// Extract Markdown heading name from atx_heading or setext_heading. +static char *extract_markdown_heading_name(CBMArena *a, TSNode node, const char *kind, + const char *source) { + char *name = NULL; + if (strcmp(kind, "atx_heading") == 0) { + name = extract_atx_heading_text(a, node, source); + } else { + if (ts_node_child_count(node) > 0) { + name = cbm_node_text(a, ts_node_child(node, 0), source); + } + } + return trim_heading_name(name); +} + +// INI: extract section name from section node. +static char *find_ini_section_name(CBMArena *a, TSNode node, const char *source) { + uint32_t nc = ts_node_child_count(node); + for (uint32_t i = 0; i < nc; i++) { + if (strcmp(ts_node_type(ts_node_child(node, i)), "section_name") == 0) { + return cbm_node_text(a, ts_node_child(node, i), source); + } + } + return NULL; +} + +// HCL: extract block name from identifier child. +static char *find_hcl_block_name(CBMArena *a, TSNode node, const char *source) { + TSNode id = cbm_find_child_by_kind(node, "identifier"); + if (!ts_node_is_null(id)) { + return cbm_node_text(a, id, source); + } + return NULL; +} + +// Handle config language class nodes (TOML, INI, XML, Markdown, HCL). +// Returns true if handled (caller should return early). +static bool extract_config_class_def(CBMExtractCtx *ctx, TSNode node, const char *kind) { + CBMArena *a = ctx->arena; + char *name = NULL; + const char *label = "Class"; + + if (ctx->language == CBM_LANG_TOML && + (strcmp(kind, "table") == 0 || strcmp(kind, "table_array_element") == 0)) { + name = find_toml_key_name(a, node, ctx->source); + } else if (ctx->language == CBM_LANG_INI && strcmp(kind, "section") == 0) { + name = find_ini_section_name(a, node, ctx->source); + } else if (ctx->language == CBM_LANG_XML && strcmp(kind, "element") == 0) { + name = find_xml_element_name(a, node, ctx->source); + } else if (ctx->language == CBM_LANG_MARKDOWN && + (strcmp(kind, "atx_heading") == 0 || strcmp(kind, "setext_heading") == 0)) { + name = extract_markdown_heading_name(a, node, kind, ctx->source); + label = "Section"; + } else if (ctx->language == CBM_LANG_HCL && strcmp(kind, "block") == 0) { + name = find_hcl_block_name(a, node, ctx->source); + } else { + return false; + } + + if (name && name[0]) { + push_simple_class_def(ctx, node, name, label); + } + return true; +} + static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) { CBMArena *a = ctx->arena; const char *kind = ts_node_type(node); - // Config language class extraction (TOML tables, INI sections, XML elements, Markdown headings) - if (ctx->language == CBM_LANG_TOML && - (strcmp(kind, "table") == 0 || strcmp(kind, "table_array_element") == 0)) { - // TOML table: name from first bare_key/dotted_key/quoted_key child, - // or from the nested key within a bracket header - char *name = NULL; - uint32_t nc = ts_node_child_count(node); - for (uint32_t i = 0; i < nc && !name; i++) { - TSNode child = ts_node_child(node, i); - const char *ck = ts_node_type(child); - if (strcmp(ck, "bare_key") == 0 || strcmp(ck, "dotted_key") == 0 || - strcmp(ck, "quoted_key") == 0 || strcmp(ck, "key") == 0) { - name = cbm_node_text(a, child, ctx->source); - } - } - if (!name || !name[0]) { - return; - } - CBMDefinition def; - memset(&def, 0, sizeof(def)); - def.name = name; - def.qualified_name = cbm_fqn_compute(a, ctx->project, ctx->rel_path, name); - def.label = "Class"; - def.file_path = ctx->rel_path; - def.start_line = ts_node_start_point(node).row + 1; - def.end_line = ts_node_end_point(node).row + 1; - def.is_exported = true; - cbm_defs_push(&ctx->result->defs, a, def); - return; - } - - if (ctx->language == CBM_LANG_INI && strcmp(kind, "section") == 0) { - // INI section: name from section_name child - char *name = NULL; - uint32_t nc = ts_node_child_count(node); - for (uint32_t i = 0; i < nc && !name; i++) { - TSNode child = ts_node_child(node, i); - const char *ck = ts_node_type(child); - if (strcmp(ck, "section_name") == 0) { - name = cbm_node_text(a, child, ctx->source); - } - } - if (!name || !name[0]) { - return; - } - CBMDefinition def; - memset(&def, 0, sizeof(def)); - def.name = name; - def.qualified_name = cbm_fqn_compute(a, ctx->project, ctx->rel_path, name); - def.label = "Class"; - def.file_path = ctx->rel_path; - def.start_line = ts_node_start_point(node).row + 1; - def.end_line = ts_node_end_point(node).row + 1; - def.is_exported = true; - cbm_defs_push(&ctx->result->defs, a, def); - return; - } - - if (ctx->language == CBM_LANG_XML && strcmp(kind, "element") == 0) { - // XML element: name from start_tag > tag_name or self_closing_tag > tag_name - char *name = NULL; - uint32_t nc = ts_node_child_count(node); - for (uint32_t i = 0; i < nc && !name; i++) { - TSNode child = ts_node_child(node, i); - const char *ck = ts_node_type(child); - if (strcmp(ck, "start_tag") == 0 || strcmp(ck, "self_closing_tag") == 0 || - strcmp(ck, "STag") == 0 || strcmp(ck, "EmptyElemTag") == 0) { - // Find Name or tag_name child - uint32_t tnc = ts_node_child_count(child); - for (uint32_t j = 0; j < tnc; j++) { - TSNode tag = ts_node_child(child, j); - const char *tk = ts_node_type(tag); - if (strcmp(tk, "tag_name") == 0 || strcmp(tk, "Name") == 0) { - name = cbm_node_text(a, tag, ctx->source); - break; - } - } - } - } - // Fallback: try "Name" field directly for some XML grammars - if (!name) { - TSNode name_child = cbm_find_child_by_kind(node, "Name"); - if (!ts_node_is_null(name_child)) { - name = cbm_node_text(a, name_child, ctx->source); - } - } - if (!name || !name[0]) { - return; - } - CBMDefinition def; - memset(&def, 0, sizeof(def)); - def.name = name; - def.qualified_name = cbm_fqn_compute(a, ctx->project, ctx->rel_path, name); - def.label = "Class"; - def.file_path = ctx->rel_path; - def.start_line = ts_node_start_point(node).row + 1; - def.end_line = ts_node_end_point(node).row + 1; - def.is_exported = true; - cbm_defs_push(&ctx->result->defs, a, def); - return; - } - - if (ctx->language == CBM_LANG_MARKDOWN && - (strcmp(kind, "atx_heading") == 0 || strcmp(kind, "setext_heading") == 0)) { - // Markdown heading: extract text content as name, use "Section" label - // For atx_heading: children are atx_h[1-6]_marker + inline content - // For setext_heading: children are paragraph + setext_h[12]_underline - char *name = NULL; - if (strcmp(kind, "atx_heading") == 0) { - // Find heading_content or inline child (skip the marker) - uint32_t nc = ts_node_child_count(node); - for (uint32_t i = 0; i < nc; i++) { - TSNode child = ts_node_child(node, i); - const char *ck = ts_node_type(child); - if (strcmp(ck, "heading_content") == 0 || strcmp(ck, "inline") == 0) { - name = cbm_node_text(a, child, ctx->source); - break; - } - } - // Fallback: extract everything after the # marker - if (!name) { - char *full = cbm_node_text(a, node, ctx->source); - if (full) { - // Skip leading # and space - char *p = full; - while (*p == '#') { - p++; - } - while (*p == ' ') { - p++; - } - if (*p) { - name = cbm_arena_strdup(a, p); - } - } - } - } else { - // setext_heading: first child is the heading text (paragraph) - if (ts_node_child_count(node) > 0) { - TSNode first = ts_node_child(node, 0); - const char *fk = ts_node_type(first); - (void)fk; // any child type: extract text - name = cbm_node_text(a, first, ctx->source); - } - } - if (!name || !name[0]) { - return; - } - // Trim trailing whitespace/newlines - size_t len = strlen(name); - while (len > 0 && - (name[len - 1] == '\n' || name[len - 1] == '\r' || name[len - 1] == ' ')) { - name[len - 1] = '\0'; - len--; - } - if (!name[0]) { - return; - } - CBMDefinition def; - memset(&def, 0, sizeof(def)); - def.name = name; - def.qualified_name = cbm_fqn_compute(a, ctx->project, ctx->rel_path, name); - def.label = "Section"; // NOT "Class" — avoids polluting class queries - def.file_path = ctx->rel_path; - def.start_line = ts_node_start_point(node).row + 1; - def.end_line = ts_node_end_point(node).row + 1; - def.is_exported = true; - cbm_defs_push(&ctx->result->defs, a, def); - return; - } - - // HCL blocks need special handling - if (ctx->language == CBM_LANG_HCL && strcmp(kind, "block") == 0) { - // Simple: use first identifier child as name - TSNode id = cbm_find_child_by_kind(node, "identifier"); - if (ts_node_is_null(id)) { - return; - } - char *name = cbm_node_text(a, id, ctx->source); - if (!name || !name[0]) { - return; - } - - CBMDefinition def; - memset(&def, 0, sizeof(def)); - def.name = name; - def.qualified_name = cbm_fqn_compute(a, ctx->project, ctx->rel_path, name); - def.label = "Class"; - def.file_path = ctx->rel_path; - def.start_line = ts_node_start_point(node).row + 1; - def.end_line = ts_node_end_point(node).row + 1; - def.is_exported = true; - cbm_defs_push(&ctx->result->defs, a, def); + if (extract_config_class_def(ctx, node, kind)) { return; } @@ -1596,8 +1653,42 @@ static TSNode find_class_body(TSNode class_node, CBMLanguage lang) { return null_node; } -// Helper: try to extract method name from a node, with fallbacks -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Dart: resolve method name from method_signature/function_signature. +static TSNode resolve_dart_method_name(TSNode child, const char *ck) { + if (strcmp(ck, "method_signature") == 0) { + TSNode func_sig = cbm_find_child_by_kind(child, "function_signature"); + if (!ts_node_is_null(func_sig)) { + TSNode name_node = func_name_node(func_sig); + if (!ts_node_is_null(name_node)) { + return name_node; + } + return cbm_find_child_by_kind(func_sig, "identifier"); + } + } + if (strcmp(ck, "function_signature") == 0) { + return cbm_find_child_by_kind(child, "identifier"); + } + TSNode null_node = {0}; + return null_node; +} + +// Arrow function: name on parent variable_declarator/field_definition. +static TSNode resolve_arrow_func_name(TSNode child) { + TSNode parent = ts_node_parent(child); + if (!ts_node_is_null(parent)) { + const char *pk = ts_node_type(parent); + if (strcmp(pk, "field_definition") == 0) { + return ts_node_child_by_field_name(parent, "property", 8); + } + if (strcmp(pk, "public_field_definition") == 0 || strcmp(pk, "variable_declarator") == 0) { + return ts_node_child_by_field_name(parent, "name", 4); + } + } + TSNode null_node = {0}; + return null_node; +} + +// Try to extract method name from a node, with language-specific fallbacks. static TSNode resolve_method_name(TSNode child, CBMLanguage lang) { TSNode name_node = func_name_node(child); if (!ts_node_is_null(name_node)) { @@ -1606,17 +1697,12 @@ static TSNode resolve_method_name(TSNode child, CBMLanguage lang) { const char *ck = ts_node_type(child); - // C/C++/CUDA: function_definition inside class body — use resolve_func_name which - // handles the declarator chain including operator_name, destructor_name, etc. if ((lang == CBM_LANG_C || lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA || lang == CBM_LANG_GLSL) && strcmp(ck, "function_definition") == 0) { return resolve_func_name(child, lang, NULL); } - // Groovy: function_definition uses field_function for the method name (not field_name). - // e.g., "String greet()" → {field_type:String, field_function:greet, ...} - // Using the first identifier would pick up the return type instead of the name. if (lang == CBM_LANG_GROOVY && strcmp(ck, "function_definition") == 0) { TSNode fn = ts_node_child_by_field_name(child, "function", 8); if (!ts_node_is_null(fn)) { @@ -1625,46 +1711,20 @@ static TSNode resolve_method_name(TSNode child, CBMLanguage lang) { return cbm_find_child_by_kind(child, "identifier"); } - // Dart: method_signature > function_signature > name or identifier if (lang == CBM_LANG_DART) { - if (strcmp(ck, "method_signature") == 0) { - TSNode func_sig = cbm_find_child_by_kind(child, "function_signature"); - if (!ts_node_is_null(func_sig)) { - name_node = func_name_node(func_sig); - if (!ts_node_is_null(name_node)) { - return name_node; - } - return cbm_find_child_by_kind(func_sig, "identifier"); - } - } - if (strcmp(ck, "function_signature") == 0) { - return cbm_find_child_by_kind(child, "identifier"); - } + return resolve_dart_method_name(child, ck); } - // ObjC: method_definition has identifier child (no "name" field) if (lang == CBM_LANG_OBJC && strcmp(ck, "method_definition") == 0) { return cbm_find_child_by_kind(child, "identifier"); } - // Swift: function_declaration has simple_identifier child (no "name" field) if (lang == CBM_LANG_SWIFT && strcmp(ck, "function_declaration") == 0) { return cbm_find_child_by_kind(child, "simple_identifier"); } - // Arrow function: name on parent variable_declarator if (strcmp(ck, "arrow_function") == 0) { - TSNode parent = ts_node_parent(child); - if (!ts_node_is_null(parent)) { - const char *pk = ts_node_type(parent); - if (strcmp(pk, "field_definition") == 0) { - return ts_node_child_by_field_name(parent, "property", 8); - } - if (strcmp(pk, "public_field_definition") == 0 || - strcmp(pk, "variable_declarator") == 0) { - return ts_node_child_by_field_name(parent, "name", 4); - } - } + return resolve_arrow_func_name(child); } TSNode null_node = {0}; @@ -1672,7 +1732,6 @@ static TSNode resolve_method_name(TSNode child, CBMLanguage lang) { } // Push a single method definition -// NOLINTNEXTLINE(readability-function-cognitive-complexity) static void push_method_def(CBMExtractCtx *ctx, TSNode child, const char *class_qn, const CBMLangSpec *spec, TSNode name_node) { CBMArena *a = ctx->arena; @@ -1717,28 +1776,7 @@ static void push_method_def(CBMExtractCtx *ctx, TSNode child, const char *class_ // C++: trailing return type (auto method() -> Type) if (def.return_type && strcmp(def.return_type, "auto") == 0 && (ctx->language == CBM_LANG_CPP || ctx->language == CBM_LANG_CUDA)) { - TSNode declarator = ts_node_child_by_field_name(child, "declarator", 10); - if (!ts_node_is_null(declarator)) { - uint32_t nc = ts_node_named_child_count(declarator); - for (uint32_t i = 0; i < nc; i++) { - TSNode ch = ts_node_named_child(declarator, i); - if (strcmp(ts_node_type(ch), "trailing_return_type") == 0) { - TSNode type_desc = - ts_node_named_child_count(ch) > 0 ? ts_node_named_child(ch, 0) : ch; - def.return_type = cbm_node_text(a, type_desc, ctx->source); - if (def.return_type && def.return_type[0]) { - const char **rt = - (const char **)cbm_arena_alloc(a, 2 * sizeof(const char *)); - if (rt) { - rt[0] = def.return_type; - rt[1] = NULL; - def.return_types = rt; - } - } - break; - } - } - } + resolve_cpp_trailing_return(a, child, ctx->source, &def); } def.decorators = extract_decorators(a, child, ctx->source, ctx->language, spec); @@ -1752,8 +1790,25 @@ static void push_method_def(CBMExtractCtx *ctx, TSNode child, const char *class_ cbm_defs_push(&ctx->result->defs, a, def); } +// Extract methods from an ObjC implementation_definition node. +static void extract_objc_impl_methods(CBMExtractCtx *ctx, TSNode impl_node, const char *class_qn, + const CBMLangSpec *spec) { + uint32_t nc = ts_node_child_count(impl_node); + for (uint32_t j = 0; j < nc; j++) { + TSNode inner = ts_node_child(impl_node, j); + if (ts_node_is_null(inner)) { + continue; + } + if (cbm_kind_in_set(inner, spec->function_node_types)) { + TSNode nm = resolve_method_name(inner, ctx->language); + if (!ts_node_is_null(nm)) { + push_method_def(ctx, inner, class_qn, spec, nm); + } + } + } +} + // Extract methods inside a class body -// NOLINTNEXTLINE(readability-function-cognitive-complexity) static void extract_class_methods(CBMExtractCtx *ctx, TSNode class_node, const char *class_qn, const CBMLangSpec *spec) { TSNode body = find_class_body(class_node, ctx->language); @@ -1768,23 +1823,9 @@ static void extract_class_methods(CBMExtractCtx *ctx, TSNode class_node, const c continue; } - // ObjC: class_implementation has implementation_definition children - // which each contain a method_definition if (ctx->language == CBM_LANG_OBJC && strcmp(ts_node_type(child), "implementation_definition") == 0) { - uint32_t nc = ts_node_child_count(child); - for (uint32_t j = 0; j < nc; j++) { - TSNode inner = ts_node_child(child, j); - if (ts_node_is_null(inner)) { - continue; - } - if (cbm_kind_in_set(inner, spec->function_node_types)) { - TSNode nm = resolve_method_name(inner, ctx->language); - if (!ts_node_is_null(nm)) { - push_method_def(ctx, inner, class_qn, spec, nm); - } - } - } + extract_objc_impl_methods(ctx, child, class_qn, spec); continue; } @@ -1885,11 +1926,96 @@ static void extract_rust_impl(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec // --- Elixir def/defp/defmodule --- -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Get the "arguments" node for an Elixir call, with fallback to second child. +static TSNode elixir_call_args(TSNode node) { + TSNode args = ts_node_child_by_field_name(node, "arguments", 9); + if (ts_node_is_null(args) && ts_node_child_count(node) > 1) { + args = ts_node_child(node, 1); + } + return args; +} + +// Handle Elixir def/defp/defmacro — extract function definition. +static void extract_elixir_func_def(CBMExtractCtx *ctx, TSNode node, const char *macro) { + CBMArena *a = ctx->arena; + TSNode args = elixir_call_args(node); + if (ts_node_is_null(args)) { + return; + } + + TSNode first_arg = ts_node_child(args, 0); + if (ts_node_is_null(first_arg)) { + return; + } + + const char *fk = ts_node_type(first_arg); + char *name = NULL; + if (strcmp(fk, "call") == 0 && ts_node_child_count(first_arg) > 0) { + name = cbm_node_text(a, ts_node_child(first_arg, 0), ctx->source); + } else if (strcmp(fk, "identifier") == 0) { + name = cbm_node_text(a, first_arg, ctx->source); + } + if (!name || !name[0]) { + return; + } + + CBMDefinition def; + memset(&def, 0, sizeof(def)); + def.name = name; + def.qualified_name = cbm_fqn_compute(a, ctx->project, ctx->rel_path, name); + def.label = "Function"; + def.file_path = ctx->rel_path; + def.start_line = ts_node_start_point(node).row + 1; + def.end_line = ts_node_end_point(node).row + 1; + def.is_exported = (strcmp(macro, "def") == 0 || strcmp(macro, "defmacro") == 0); + cbm_defs_push(&ctx->result->defs, a, def); +} + +// Handle Elixir defmodule — extract class definition and recurse into do_block. +static void extract_elixir_module_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) { + CBMArena *a = ctx->arena; + TSNode args = elixir_call_args(node); + if (ts_node_is_null(args)) { + return; + } + + TSNode name_node = ts_node_child(args, 0); + if (ts_node_is_null(name_node)) { + return; + } + + char *name = cbm_node_text(a, name_node, ctx->source); + if (!name || !name[0]) { + return; + } + + CBMDefinition def; + memset(&def, 0, sizeof(def)); + def.name = name; + def.qualified_name = cbm_fqn_compute(a, ctx->project, ctx->rel_path, name); + def.label = "Class"; + def.file_path = ctx->rel_path; + def.start_line = ts_node_start_point(node).row + 1; + def.end_line = ts_node_end_point(node).row + 1; + def.is_exported = true; + cbm_defs_push(&ctx->result->defs, a, def); + + // Recurse into do_block for nested def/defp/defmacro + TSNode do_block = cbm_find_child_by_kind(node, "do_block"); + if (!ts_node_is_null(do_block)) { + uint32_t dbc = ts_node_child_count(do_block); + for (uint32_t di = 0; di < dbc; di++) { + TSNode dchild = ts_node_child(do_block, di); + if (!ts_node_is_null(dchild) && strcmp(ts_node_type(dchild), "call") == 0) { + extract_elixir_call(ctx, dchild, spec); + } + } + } +} + static void extract_elixir_call(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) { CBMArena *a = ctx->arena; - // First child is the macro name (def, defp, defmodule, defmacro) if (ts_node_child_count(node) == 0) { return; } @@ -1903,85 +2029,9 @@ static void extract_elixir_call(CBMExtractCtx *ctx, TSNode node, const CBMLangSp } if (strcmp(macro, "def") == 0 || strcmp(macro, "defp") == 0 || strcmp(macro, "defmacro") == 0) { - // Second child is arguments with the function name - TSNode args = ts_node_child_by_field_name(node, "arguments", 9); - if (ts_node_is_null(args)) { - if (ts_node_child_count(node) > 1) { - args = ts_node_child(node, 1); - } - } - if (ts_node_is_null(args)) { - return; - } - - // The function name is the first child of args (a call or identifier) - TSNode first_arg = ts_node_child(args, 0); - if (ts_node_is_null(first_arg)) { - return; - } - - const char *fk = ts_node_type(first_arg); - char *name = NULL; - if (strcmp(fk, "call") == 0 && ts_node_child_count(first_arg) > 0) { - name = cbm_node_text(a, ts_node_child(first_arg, 0), ctx->source); - } else if (strcmp(fk, "identifier") == 0) { - name = cbm_node_text(a, first_arg, ctx->source); - } - if (!name || !name[0]) { - return; - } - - CBMDefinition def; - memset(&def, 0, sizeof(def)); - def.name = name; - def.qualified_name = cbm_fqn_compute(a, ctx->project, ctx->rel_path, name); - def.label = "Function"; - def.file_path = ctx->rel_path; - def.start_line = ts_node_start_point(node).row + 1; - def.end_line = ts_node_end_point(node).row + 1; - def.is_exported = (strcmp(macro, "def") == 0 || strcmp(macro, "defmacro") == 0); - cbm_defs_push(&ctx->result->defs, a, def); + extract_elixir_func_def(ctx, node, macro); } else if (strcmp(macro, "defmodule") == 0) { - TSNode args = ts_node_child_by_field_name(node, "arguments", 9); - if (ts_node_is_null(args) && ts_node_child_count(node) > 1) { - args = ts_node_child(node, 1); - } - if (ts_node_is_null(args)) { - return; - } - - TSNode name_node = ts_node_child(args, 0); - if (ts_node_is_null(name_node)) { - return; - } - - char *name = cbm_node_text(a, name_node, ctx->source); - if (!name || !name[0]) { - return; - } - - CBMDefinition def; - memset(&def, 0, sizeof(def)); - def.name = name; - def.qualified_name = cbm_fqn_compute(a, ctx->project, ctx->rel_path, name); - def.label = "Class"; - def.file_path = ctx->rel_path; - def.start_line = ts_node_start_point(node).row + 1; - def.end_line = ts_node_end_point(node).row + 1; - def.is_exported = true; - cbm_defs_push(&ctx->result->defs, a, def); - - // Recurse into do_block for nested def/defp/defmacro - TSNode do_block = cbm_find_child_by_kind(node, "do_block"); - if (!ts_node_is_null(do_block)) { - uint32_t dbc = ts_node_child_count(do_block); - for (uint32_t di = 0; di < dbc; di++) { - TSNode dchild = ts_node_child(do_block, di); - if (!ts_node_is_null(dchild) && strcmp(ts_node_type(dchild), "call") == 0) { - extract_elixir_call(ctx, dchild, spec); - } - } - } + extract_elixir_module_def(ctx, node, spec); } } @@ -2063,7 +2113,58 @@ static const char *extract_java_field_name(CBMArena *a, TSNode field, const char /* ── Variable name extractors by language group ─────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// C# variable extraction: handle field_declaration with nested variable_declaration. +static void extract_csharp_vars(CBMExtractCtx *ctx, TSNode node, CBMArena *a) { + const char *fname = extract_java_field_name(a, node, ctx->source); + if (fname) { + push_var_def(ctx, fname, node); + return; + } + uint32_t n = ts_node_named_child_count(node); + for (uint32_t i = 0; i < n; i++) { + TSNode child = ts_node_named_child(node, i); + if (strcmp(ts_node_type(child), "variable_declaration") != 0) { + continue; + } + uint32_t nc = ts_node_named_child_count(child); + for (uint32_t j = 0; j < nc; j++) { + TSNode decl = ts_node_named_child(child, j); + if (strcmp(ts_node_type(decl), "variable_declarator") == 0) { + TSNode id = ts_node_child_by_field_name(decl, "name", 4); + if (ts_node_is_null(id)) { + id = cbm_find_child_by_kind(decl, "identifier"); + } + if (!ts_node_is_null(id)) { + push_var_def(ctx, cbm_node_text(a, id, ctx->source), decl); + } + } + } + } +} + +// JS/TS variable extraction: skip function-assigned declarators. +static void extract_js_vars(CBMExtractCtx *ctx, TSNode node, CBMArena *a) { + uint32_t n = ts_node_named_child_count(node); + for (uint32_t i = 0; i < n; i++) { + TSNode child = ts_node_named_child(node, i); + if (strcmp(ts_node_type(child), "variable_declarator") != 0) { + continue; + } + TSNode value = ts_node_child_by_field_name(child, "value", 5); + if (!ts_node_is_null(value)) { + const char *vk = ts_node_type(value); + if (strcmp(vk, "arrow_function") == 0 || strcmp(vk, "function_expression") == 0 || + strcmp(vk, "generator_function") == 0) { + continue; + } + } + TSNode vname = ts_node_child_by_field_name(child, "name", 4); + if (!ts_node_is_null(vname)) { + push_var_def(ctx, cbm_node_text(a, vname, ctx->source), child); + } + } +} + static void extract_vars_mainstream(CBMExtractCtx *ctx, TSNode node, CBMArena *a, const char *kind) { (void)kind; @@ -2091,28 +2192,9 @@ static void extract_vars_mainstream(CBMExtractCtx *ctx, TSNode node, CBMArena *a } case CBM_LANG_JAVASCRIPT: case CBM_LANG_TYPESCRIPT: - case CBM_LANG_TSX: { - uint32_t n = ts_node_named_child_count(node); - for (uint32_t i = 0; i < n; i++) { - TSNode child = ts_node_named_child(node, i); - if (strcmp(ts_node_type(child), "variable_declarator") != 0) { - continue; - } - TSNode value = ts_node_child_by_field_name(child, "value", 5); - if (!ts_node_is_null(value)) { - const char *vk = ts_node_type(value); - if (strcmp(vk, "arrow_function") == 0 || strcmp(vk, "function_expression") == 0 || - strcmp(vk, "generator_function") == 0) { - continue; - } - } - TSNode vname = ts_node_child_by_field_name(child, "name", 4); - if (!ts_node_is_null(vname)) { - push_var_def(ctx, cbm_node_text(a, vname, ctx->source), child); - } - } + case CBM_LANG_TSX: + extract_js_vars(ctx, node, a); break; - } case CBM_LANG_JAVA: { const char *fname = extract_java_field_name(a, node, ctx->source); if (fname) { @@ -2120,33 +2202,9 @@ static void extract_vars_mainstream(CBMExtractCtx *ctx, TSNode node, CBMArena *a } break; } - case CBM_LANG_CSHARP: { - const char *fname = extract_java_field_name(a, node, ctx->source); - if (fname) { - push_var_def(ctx, fname, node); - } else { - uint32_t n = ts_node_named_child_count(node); - for (uint32_t i = 0; i < n; i++) { - TSNode child = ts_node_named_child(node, i); - if (strcmp(ts_node_type(child), "variable_declaration") == 0) { - uint32_t nc = ts_node_named_child_count(child); - for (uint32_t j = 0; j < nc; j++) { - TSNode decl = ts_node_named_child(child, j); - if (strcmp(ts_node_type(decl), "variable_declarator") == 0) { - TSNode id = ts_node_child_by_field_name(decl, "name", 4); - if (ts_node_is_null(id)) { - id = cbm_find_child_by_kind(decl, "identifier"); - } - if (!ts_node_is_null(id)) { - push_var_def(ctx, cbm_node_text(a, id, ctx->source), decl); - } - } - } - } - } - } + case CBM_LANG_CSHARP: + extract_csharp_vars(ctx, node, a); break; - } case CBM_LANG_CPP: case CBM_LANG_C: case CBM_LANG_OBJC: { @@ -2168,56 +2226,138 @@ static void extract_vars_mainstream(CBMExtractCtx *ctx, TSNode node, CBMArena *a } } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Lua variable extraction: handle assignment_statement with function-def filtering. +static void extract_lua_vars(CBMExtractCtx *ctx, TSNode node, CBMArena *a) { + uint32_t n = ts_node_named_child_count(node); + for (uint32_t i = 0; i < n; i++) { + TSNode child = ts_node_named_child(node, i); + if (strcmp(ts_node_type(child), "assignment_statement") != 0) { + continue; + } + TSNode expr_list = cbm_find_child_by_kind(child, "expression_list"); + if (!ts_node_is_null(expr_list) && ts_node_named_child_count(expr_list) > 0) { + TSNode val = ts_node_named_child(expr_list, 0); + if (!ts_node_is_null(val) && strcmp(ts_node_type(val), "function_definition") == 0) { + continue; + } + } + TSNode vars = ts_node_child_by_field_name(child, "variables", 9); + if (ts_node_is_null(vars)) { + vars = cbm_find_child_by_kind(child, "variable_list"); + } + if (!ts_node_is_null(vars) && ts_node_named_child_count(vars) > 0) { + TSNode first = ts_node_named_child(vars, 0); + if (!ts_node_is_null(first)) { + push_var_def(ctx, cbm_node_text(a, first, ctx->source), node); + } + } + } +} + +// Strip Perl sigil ($, @, %) from name. +static char *strip_perl_sigil(char *name) { + if (name && (name[0] == '$' || name[0] == '@' || name[0] == '%')) { + return name + 1; + } + return name; +} + +// Check if a node type is a Perl variable type. +static bool is_perl_var_type(const char *ck) { + return strcmp(ck, "scalar_variable") == 0 || strcmp(ck, "array_variable") == 0 || + strcmp(ck, "hash_variable") == 0 || strcmp(ck, "variable_declarator") == 0 || + strcmp(ck, "scalar") == 0 || strcmp(ck, "array") == 0 || strcmp(ck, "hash") == 0; +} + +// Perl variable extraction: handle direct variable nodes and assignment_expression. +static void extract_perl_vars(CBMExtractCtx *ctx, TSNode node, CBMArena *a) { + uint32_t n = ts_node_named_child_count(node); + for (uint32_t i = 0; i < n; i++) { + TSNode child = ts_node_named_child(node, i); + const char *ck = ts_node_type(child); + if (is_perl_var_type(ck)) { + push_var_def(ctx, strip_perl_sigil(cbm_node_text(a, child, ctx->source)), node); + return; + } + if (strcmp(ck, "assignment_expression") != 0) { + continue; + } + TSNode left = ts_node_child_by_field_name(child, "left", 4); + if (ts_node_is_null(left) && ts_node_named_child_count(child) > 0) { + left = ts_node_named_child(child, 0); + } + if (ts_node_is_null(left)) { + continue; + } + if (strcmp(ts_node_type(left), "variable_declaration") == 0) { + uint32_t lnc = ts_node_named_child_count(left); + for (uint32_t li = 0; li < lnc; li++) { + TSNode var_node = ts_node_named_child(left, li); + if (is_perl_var_type(ts_node_type(var_node))) { + left = var_node; + break; + } + } + } + push_var_def(ctx, strip_perl_sigil(cbm_node_text(a, left, ctx->source)), node); + return; + } +} + +// R variable extraction: skip function-definitions, then extract left/lhs. +static void extract_r_vars(CBMExtractCtx *ctx, TSNode node, CBMArena *a) { + uint32_t rnc = ts_node_named_child_count(node); + for (uint32_t ri = 0; ri < rnc; ri++) { + TSNode rch = ts_node_named_child(node, ri); + if (!ts_node_is_null(rch) && strcmp(ts_node_type(rch), "function_definition") == 0) { + return; + } + } + TSNode left = ts_node_child_by_field_name(node, "left", 4); + if (ts_node_is_null(left)) { + left = ts_node_child_by_field_name(node, "lhs", 3); + } + if (ts_node_is_null(left) && ts_node_named_child_count(node) > 0) { + left = ts_node_named_child(node, 0); + } + if (!ts_node_is_null(left)) { + const char *lk = ts_node_type(left); + if (strcmp(lk, "identifier") == 0 || strcmp(lk, "constant") == 0 || + strcmp(lk, "string") == 0) { + push_var_def(ctx, cbm_node_text(a, left, ctx->source), node); + } + } +} + +// PHP variable extraction from expression_statement. +static void extract_php_vars(CBMExtractCtx *ctx, TSNode node, CBMArena *a, const char *kind) { + if (strcmp(kind, "expression_statement") != 0) { + return; + } + uint32_t nc = ts_node_named_child_count(node); + for (uint32_t j = 0; j < nc; j++) { + TSNode inner = ts_node_named_child(node, j); + if (strcmp(ts_node_type(inner), "assignment_expression") == 0) { + TSNode left = ts_node_child_by_field_name(inner, "left", 4); + if (!ts_node_is_null(left)) { + char *name = cbm_node_text(a, left, ctx->source); + if (name && name[0] == '$') { + name++; + } + push_var_def(ctx, name, node); + } + } + } +} + static void extract_vars_dynamic(CBMExtractCtx *ctx, TSNode node, CBMArena *a, const char *kind) { switch (ctx->language) { - case CBM_LANG_PHP: { - if (strcmp(kind, "expression_statement") == 0) { - uint32_t nc = ts_node_named_child_count(node); - for (uint32_t j = 0; j < nc; j++) { - TSNode inner = ts_node_named_child(node, j); - if (strcmp(ts_node_type(inner), "assignment_expression") == 0) { - TSNode left = ts_node_child_by_field_name(inner, "left", 4); - if (!ts_node_is_null(left)) { - char *name = cbm_node_text(a, left, ctx->source); - if (name && name[0] == '$') { - name++; - } - push_var_def(ctx, name, node); - } - } - } - } + case CBM_LANG_PHP: + extract_php_vars(ctx, node, a, kind); break; - } - case CBM_LANG_LUA: { - uint32_t n = ts_node_named_child_count(node); - for (uint32_t i = 0; i < n; i++) { - TSNode child = ts_node_named_child(node, i); - const char *ck = ts_node_type(child); - if (strcmp(ck, "assignment_statement") == 0) { - TSNode expr_list = cbm_find_child_by_kind(child, "expression_list"); - if (!ts_node_is_null(expr_list) && ts_node_named_child_count(expr_list) > 0) { - TSNode val = ts_node_named_child(expr_list, 0); - if (!ts_node_is_null(val) && - strcmp(ts_node_type(val), "function_definition") == 0) { - continue; - } - } - TSNode vars = ts_node_child_by_field_name(child, "variables", 9); - if (ts_node_is_null(vars)) { - vars = cbm_find_child_by_kind(child, "variable_list"); - } - if (!ts_node_is_null(vars) && ts_node_named_child_count(vars) > 0) { - TSNode first = ts_node_named_child(vars, 0); - if (!ts_node_is_null(first)) { - push_var_def(ctx, cbm_node_text(a, first, ctx->source), node); - } - } - } - } + case CBM_LANG_LUA: + extract_lua_vars(ctx, node, a); break; - } case CBM_LANG_RUBY: { TSNode left = ts_node_child_by_field_name(node, "left", 4); if (!ts_node_is_null(left)) { @@ -2228,83 +2368,43 @@ static void extract_vars_dynamic(CBMExtractCtx *ctx, TSNode node, CBMArena *a, c } break; } - case CBM_LANG_R: { - uint32_t rnc = ts_node_named_child_count(node); - for (uint32_t ri = 0; ri < rnc; ri++) { - TSNode rch = ts_node_named_child(node, ri); - if (!ts_node_is_null(rch) && strcmp(ts_node_type(rch), "function_definition") == 0) { - return; // will be extracted as Function - } - } - TSNode left = ts_node_child_by_field_name(node, "left", 4); - if (ts_node_is_null(left)) { - left = ts_node_child_by_field_name(node, "lhs", 3); - } - if (ts_node_is_null(left) && ts_node_named_child_count(node) > 0) { - left = ts_node_named_child(node, 0); - } - if (!ts_node_is_null(left)) { - const char *lk = ts_node_type(left); - if (strcmp(lk, "identifier") == 0 || strcmp(lk, "constant") == 0 || - strcmp(lk, "string") == 0) { - push_var_def(ctx, cbm_node_text(a, left, ctx->source), node); - } - } + case CBM_LANG_R: + extract_r_vars(ctx, node, a); break; - } - case CBM_LANG_PERL: { - uint32_t n = ts_node_named_child_count(node); - bool found = false; - for (uint32_t i = 0; i < n && !found; i++) { - TSNode child = ts_node_named_child(node, i); - const char *ck = ts_node_type(child); - if (strcmp(ck, "scalar_variable") == 0 || strcmp(ck, "array_variable") == 0 || - strcmp(ck, "hash_variable") == 0 || strcmp(ck, "variable_declarator") == 0 || - strcmp(ck, "scalar") == 0 || strcmp(ck, "array") == 0 || strcmp(ck, "hash") == 0) { - char *name = cbm_node_text(a, child, ctx->source); - if (name && (name[0] == '$' || name[0] == '@' || name[0] == '%')) { - name++; - } - push_var_def(ctx, name, node); - found = true; - } else if (strcmp(ck, "assignment_expression") == 0) { - TSNode left = ts_node_child_by_field_name(child, "left", 4); - if (ts_node_is_null(left) && ts_node_named_child_count(child) > 0) { - left = ts_node_named_child(child, 0); - } - if (!ts_node_is_null(left)) { - const char *lk = ts_node_type(left); - if (strcmp(lk, "variable_declaration") == 0) { - uint32_t lnc = ts_node_named_child_count(left); - for (uint32_t li = 0; li < lnc; li++) { - TSNode var_node = ts_node_named_child(left, li); - const char *vk = ts_node_type(var_node); - if (strcmp(vk, "scalar") == 0 || strcmp(vk, "array") == 0 || - strcmp(vk, "hash") == 0 || strcmp(vk, "scalar_variable") == 0 || - strcmp(vk, "array_variable") == 0 || - strcmp(vk, "hash_variable") == 0) { - left = var_node; - break; - } - } - } - char *name = cbm_node_text(a, left, ctx->source); - if (name && (name[0] == '$' || name[0] == '@' || name[0] == '%')) { - name++; - } - push_var_def(ctx, name, node); - found = true; - } - } - } + case CBM_LANG_PERL: + extract_perl_vars(ctx, node, a); break; - } default: break; } } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Kotlin variable name resolution: name > simple_identifier > identifier > variable_declaration. +static TSNode resolve_kotlin_var_name(TSNode node) { + TSNode name_node = ts_node_child_by_field_name(node, "name", 4); + if (!ts_node_is_null(name_node)) { + return name_node; + } + name_node = cbm_find_child_by_kind(node, "simple_identifier"); + if (!ts_node_is_null(name_node)) { + return name_node; + } + name_node = cbm_find_child_by_kind(node, "identifier"); + if (!ts_node_is_null(name_node)) { + return name_node; + } + TSNode var_decl = cbm_find_child_by_kind(node, "variable_declaration"); + if (!ts_node_is_null(var_decl)) { + name_node = cbm_find_child_by_kind(var_decl, "simple_identifier"); + if (!ts_node_is_null(name_node)) { + return name_node; + } + return cbm_find_child_by_kind(var_decl, "identifier"); + } + TSNode null_node = {0}; + return null_node; +} + static void extract_vars_jvm(CBMExtractCtx *ctx, TSNode node, CBMArena *a) { switch (ctx->language) { case CBM_LANG_SCALA: { @@ -2320,22 +2420,7 @@ static void extract_vars_jvm(CBMExtractCtx *ctx, TSNode node, CBMArena *a) { break; } case CBM_LANG_KOTLIN: { - TSNode name_node = ts_node_child_by_field_name(node, "name", 4); - if (ts_node_is_null(name_node)) { - name_node = cbm_find_child_by_kind(node, "simple_identifier"); - } - if (ts_node_is_null(name_node)) { - name_node = cbm_find_child_by_kind(node, "identifier"); - } - if (ts_node_is_null(name_node)) { - TSNode var_decl = cbm_find_child_by_kind(node, "variable_declaration"); - if (!ts_node_is_null(var_decl)) { - name_node = cbm_find_child_by_kind(var_decl, "simple_identifier"); - if (ts_node_is_null(name_node)) { - name_node = cbm_find_child_by_kind(var_decl, "identifier"); - } - } - } + TSNode name_node = resolve_kotlin_var_name(node); if (!ts_node_is_null(name_node)) { push_var_def(ctx, cbm_node_text(a, name_node, ctx->source), node); } @@ -2361,7 +2446,99 @@ static void extract_vars_jvm(CBMExtractCtx *ctx, TSNode node, CBMArena *a) { } } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Trim leading/trailing whitespace from a name in-place. +static char *trim_whitespace(char *name) { + if (!name) { + return name; + } + while (*name == ' ' || *name == '\t') { + name++; + } + size_t nlen = strlen(name); + while (nlen > 0 && (name[nlen - 1] == ' ' || name[nlen - 1] == '\t')) { + name[nlen - 1] = '\0'; + nlen--; + } + return name; +} + +// INI variable extraction: find setting_name/name child, with fallback to first child. +static void extract_ini_vars(CBMExtractCtx *ctx, TSNode node, CBMArena *a) { + uint32_t nc = ts_node_child_count(node); + for (uint32_t i = 0; i < nc; i++) { + TSNode child = ts_node_child(node, i); + const char *ck = ts_node_type(child); + if (strcmp(ck, "setting_name") == 0 || strcmp(ck, "name") == 0) { + push_var_def(ctx, trim_whitespace(cbm_node_text(a, child, ctx->source)), node); + return; + } + } + if (nc > 0) { + bool found_name = false; + for (uint32_t i = 0; i < nc; i++) { + const char *ck = ts_node_type(ts_node_child(node, i)); + if (strcmp(ck, "setting_name") == 0 || strcmp(ck, "name") == 0) { + found_name = true; + break; + } + } + if (!found_name) { + push_var_def( + ctx, trim_whitespace(cbm_node_text(a, ts_node_child(node, 0), ctx->source)), node); + } + } +} + +// Find first named child matching one of the given types and push as var def. +static void push_first_matching_child(CBMExtractCtx *ctx, TSNode node, CBMArena *a, + const char **match_types) { + uint32_t n = ts_node_named_child_count(node); + for (uint32_t i = 0; i < n; i++) { + TSNode child = ts_node_named_child(node, i); + const char *ck = ts_node_type(child); + for (const char **t = match_types; *t; t++) { + if (strcmp(ck, *t) == 0) { + push_var_def(ctx, cbm_node_text(a, child, ctx->source), node); + return; + } + } + } +} + +// JSON variable extraction: strip quotes from key. +static void extract_json_var(CBMExtractCtx *ctx, TSNode node, CBMArena *a) { + TSNode key_node = ts_node_child_by_field_name(node, "key", 3); + if (ts_node_is_null(key_node)) { + return; + } + char *raw = cbm_node_text(a, key_node, ctx->source); + if (raw) { + size_t rlen = strlen(raw); + if (rlen >= 2 && raw[0] == '"' && raw[rlen - 1] == '"') { + raw[rlen - 1] = '\0'; + raw++; + } + push_var_def(ctx, raw, node); + } +} + +// SCSS variable extraction: try property > name > property_name > variable_name. +static void extract_scss_var(CBMExtractCtx *ctx, TSNode node, CBMArena *a) { + TSNode prop = ts_node_child_by_field_name(node, "property", 8); + if (ts_node_is_null(prop)) { + prop = ts_node_child_by_field_name(node, "name", 4); + } + if (ts_node_is_null(prop)) { + prop = cbm_find_child_by_kind(node, "property_name"); + } + if (ts_node_is_null(prop)) { + prop = cbm_find_child_by_kind(node, "variable_name"); + } + if (!ts_node_is_null(prop)) { + push_var_def(ctx, cbm_node_text(a, prop, ctx->source), node); + } +} + static void extract_vars_config(CBMExtractCtx *ctx, TSNode node, CBMArena *a, const char *kind) { switch (ctx->language) { case CBM_LANG_YAML: { @@ -2372,106 +2549,28 @@ static void extract_vars_config(CBMExtractCtx *ctx, TSNode node, CBMArena *a, co break; } case CBM_LANG_TOML: { - uint32_t nc = ts_node_child_count(node); - for (uint32_t i = 0; i < nc; i++) { - TSNode child = ts_node_child(node, i); - const char *ck = ts_node_type(child); - if (strcmp(ck, "bare_key") == 0 || strcmp(ck, "dotted_key") == 0 || - strcmp(ck, "quoted_key") == 0 || strcmp(ck, "key") == 0) { - push_var_def(ctx, cbm_node_text(a, child, ctx->source), node); - break; - } + char *name = find_toml_key_name(a, node, ctx->source); + if (name) { + push_var_def(ctx, name, node); } break; } - case CBM_LANG_JSON: { - TSNode key_node = ts_node_child_by_field_name(node, "key", 3); - if (!ts_node_is_null(key_node)) { - char *raw = cbm_node_text(a, key_node, ctx->source); - if (raw) { - size_t rlen = strlen(raw); - if (rlen >= 2 && raw[0] == '"' && raw[rlen - 1] == '"') { - raw[rlen - 1] = '\0'; - raw++; - } - push_var_def(ctx, raw, node); - } - } + case CBM_LANG_JSON: + extract_json_var(ctx, node, a); break; - } - case CBM_LANG_INI: { - uint32_t nc = ts_node_child_count(node); - for (uint32_t i = 0; i < nc; i++) { - TSNode child = ts_node_child(node, i); - const char *ck = ts_node_type(child); - if (strcmp(ck, "setting_name") == 0 || strcmp(ck, "name") == 0) { - char *name = cbm_node_text(a, child, ctx->source); - if (name) { - while (*name == ' ' || *name == '\t') { - name++; - } - size_t nlen = strlen(name); - while (nlen > 0 && (name[nlen - 1] == ' ' || name[nlen - 1] == '\t')) { - name[nlen - 1] = '\0'; - nlen--; - } - } - push_var_def(ctx, name, node); - return; - } - } - if (nc > 0) { - bool found_name = false; - for (uint32_t i = 0; i < nc; i++) { - const char *ck = ts_node_type(ts_node_child(node, i)); - if (strcmp(ck, "setting_name") == 0 || strcmp(ck, "name") == 0) { - found_name = true; - break; - } - } - if (!found_name) { - TSNode first = ts_node_child(node, 0); - char *name = cbm_node_text(a, first, ctx->source); - if (name) { - while (*name == ' ' || *name == '\t') { - name++; - } - size_t nlen = strlen(name); - while (nlen > 0 && (name[nlen - 1] == ' ' || name[nlen - 1] == '\t')) { - name[nlen - 1] = '\0'; - nlen--; - } - } - push_var_def(ctx, name, node); - } - } + case CBM_LANG_INI: + extract_ini_vars(ctx, node, a); break; - } case CBM_LANG_ERLANG: { if (strcmp(kind, "pp_define") == 0 || strcmp(kind, "record_decl") == 0) { - uint32_t n = ts_node_named_child_count(node); - for (uint32_t i = 0; i < n; i++) { - TSNode child = ts_node_named_child(node, i); - const char *ck = ts_node_type(child); - if (strcmp(ck, "atom") == 0 || strcmp(ck, "var") == 0 || - strcmp(ck, "macro_lhs") == 0) { - push_var_def(ctx, cbm_node_text(a, child, ctx->source), node); - break; - } - } + static const char *erlang_var_types[] = {"atom", "var", "macro_lhs", NULL}; + push_first_matching_child(ctx, node, a, erlang_var_types); } break; } case CBM_LANG_SQL: { - uint32_t n = ts_node_named_child_count(node); - for (uint32_t i = 0; i < n; i++) { - TSNode child = ts_node_named_child(node, i); - const char *ck = ts_node_type(child); - if (strcmp(ck, "identifier") == 0 || strcmp(ck, "object_reference") == 0) { - push_var_def(ctx, cbm_node_text(a, child, ctx->source), node); - break; - } - } + static const char *sql_var_types[] = {"identifier", "object_reference", NULL}; + push_first_matching_child(ctx, node, a, sql_var_types); break; } case CBM_LANG_BASH: { @@ -2479,34 +2578,14 @@ static void extract_vars_config(CBMExtractCtx *ctx, TSNode node, CBMArena *a, co if (!ts_node_is_null(name_node)) { push_var_def(ctx, cbm_node_text(a, name_node, ctx->source), node); } else { - uint32_t n = ts_node_named_child_count(node); - for (uint32_t i = 0; i < n; i++) { - TSNode child = ts_node_named_child(node, i); - const char *ck = ts_node_type(child); - if (strcmp(ck, "variable_name") == 0 || strcmp(ck, "word") == 0) { - push_var_def(ctx, cbm_node_text(a, child, ctx->source), node); - break; - } - } + static const char *bash_var_types[] = {"variable_name", "word", NULL}; + push_first_matching_child(ctx, node, a, bash_var_types); } break; } - case CBM_LANG_SCSS: { - TSNode prop = ts_node_child_by_field_name(node, "property", 8); - if (ts_node_is_null(prop)) { - prop = ts_node_child_by_field_name(node, "name", 4); - } - if (ts_node_is_null(prop)) { - prop = cbm_find_child_by_kind(node, "property_name"); - } - if (ts_node_is_null(prop)) { - prop = cbm_find_child_by_kind(node, "variable_name"); - } - if (!ts_node_is_null(prop)) { - push_var_def(ctx, cbm_node_text(a, prop, ctx->source), node); - } + case CBM_LANG_SCSS: + extract_scss_var(ctx, node, a); break; - } default: break; } @@ -2670,7 +2749,50 @@ static void extract_variables(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec // Extract typed struct/class fields for cross-file LSP resolution (C/C++/CUDA/Go/Java/Rust etc.) // Creates "Field" label definitions with return_type set to the field's type text. // These are later collected by DefsToLSPDefs to build FieldDefs pipe-separated strings. -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Check if a field_declaration has a function-pointer declarator chain. +static bool is_func_ptr_field(TSNode field) { + TSNode decl = ts_node_child_by_field_name(field, "declarator", 10); + for (int depth = 0; depth < 5 && !ts_node_is_null(decl); depth++) { + if (strcmp(ts_node_type(decl), "function_declarator") == 0) { + return true; + } + TSNode inner = ts_node_child_by_field_name(decl, "declarator", 10); + if (ts_node_is_null(inner)) { + uint32_t nc = ts_node_named_child_count(decl); + for (uint32_t k = 0; k < nc; k++) { + inner = ts_node_named_child(decl, k); + if (!ts_node_is_null(inner)) { + break; + } + } + } + decl = inner; + } + return false; +} + +// Resolve the name node for a field declaration, unwrapping C pointer/array declarators. +static TSNode resolve_field_name_node(TSNode child) { + TSNode name_node = ts_node_child_by_field_name(child, "declarator", 10); + if (ts_node_is_null(name_node)) { + name_node = ts_node_child_by_field_name(child, "name", 4); + } + if (ts_node_is_null(name_node)) { + TSNode null_node = {0}; + return null_node; + } + const char *nk = ts_node_type(name_node); + if (strcmp(nk, "pointer_declarator") == 0 || strcmp(nk, "array_declarator") == 0) { + TSNode inner = ts_node_child_by_field_name(name_node, "declarator", 10); + if (!ts_node_is_null(inner)) { + return inner; + } + TSNode null_node = {0}; + return null_node; + } + return name_node; +} + static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const char *class_qn, const CBMLangSpec *spec) { if (!spec->field_node_types || !spec->field_node_types[0]) { @@ -2690,33 +2812,8 @@ static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const ch continue; } - // Skip function-pointer fields: walk the declarator chain — if any level - // is function_declarator, this is a function pointer, not a data field. - { - TSNode decl = ts_node_child_by_field_name(child, "declarator", 10); - bool is_func = false; - for (int depth = 0; depth < 5 && !ts_node_is_null(decl); depth++) { - const char *dk = ts_node_type(decl); - if (strcmp(dk, "function_declarator") == 0) { - is_func = true; - break; - } - // Descend through parenthesized_declarator, pointer_declarator, etc. - TSNode inner = ts_node_child_by_field_name(decl, "declarator", 10); - if (ts_node_is_null(inner)) { - uint32_t nc = ts_node_named_child_count(decl); - for (uint32_t k = 0; k < nc; k++) { - inner = ts_node_named_child(decl, k); - if (!ts_node_is_null(inner)) { - break; - } - } - } - decl = inner; - } - if (is_func) { - continue; - } + if (is_func_ptr_field(child)) { + continue; } // Extract type from "type" field @@ -2729,31 +2826,11 @@ static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const ch continue; } - // Extract field name from "declarator" field (C/C++/CUDA/GLSL) - // or "name" field (Go, Java, Rust) - TSNode name_node = ts_node_child_by_field_name(child, "declarator", 10); - if (ts_node_is_null(name_node)) { - name_node = ts_node_child_by_field_name(child, "name", 4); - } + TSNode name_node = resolve_field_name_node(child); if (ts_node_is_null(name_node)) { continue; } - // For C/C++: declarator might be a pointer_declarator or array_declarator wrapping - // field_identifier - const char *nk = ts_node_type(name_node); - if (strcmp(nk, "pointer_declarator") == 0 || strcmp(nk, "array_declarator") == 0) { - // Prepend pointer/array to type, get inner identifier - char *decl_text = cbm_node_text(a, name_node, ctx->source); - TSNode inner = ts_node_child_by_field_name(name_node, "declarator", 10); - if (!ts_node_is_null(inner)) { - name_node = inner; - } else { - (void)decl_text; - continue; - } - } - char *name = cbm_node_text(a, name_node, ctx->source); if (!name || !name[0]) { continue; @@ -2838,7 +2915,64 @@ static void push_nested_class_nodes(TSNode body, const CBMLangSpec *spec, walk_d } } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Check if a C++/CUDA template_declaration wraps a class/struct/union (not a function). +static bool is_template_class_node(TSNode node, CBMLanguage lang) { + if ((lang != CBM_LANG_CPP && lang != CBM_LANG_CUDA) || + strcmp(ts_node_type(node), "template_declaration") != 0) { + return false; + } + uint32_t nc = ts_node_named_child_count(node); + for (uint32_t i = 0; i < nc; i++) { + const char *ck = ts_node_type(ts_node_named_child(node, i)); + if (strcmp(ck, "class_specifier") == 0 || strcmp(ck, "struct_specifier") == 0 || + strcmp(ck, "union_specifier") == 0) { + return true; + } + } + return false; +} + +// Compute the enclosing class QN for a class node (for nested class context). +static const char *compute_class_qn(CBMExtractCtx *ctx, TSNode node, const char *saved_enclosing) { + TSNode name_node = ts_node_child_by_field_name(node, "name", 4); + if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_OBJC) { + name_node = cbm_find_child_by_kind(node, "identifier"); + } + if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_SWIFT) { + name_node = cbm_find_child_by_kind(node, "type_identifier"); + } + if (!ts_node_is_null(name_node)) { + char *cname = cbm_node_text(ctx->arena, name_node, ctx->source); + if (cname && cname[0]) { + if (saved_enclosing) { + return cbm_arena_sprintf(ctx->arena, "%s.%s", saved_enclosing, cname); + } + return cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, cname); + } + } + return saved_enclosing; +} + +// Push nested class children from a class body container onto the walk stack. +static void push_class_body_children(TSNode node, const CBMLangSpec *spec, walk_defs_frame_t *stack, + int *top, const char *new_enclosing) { + uint32_t nc = ts_node_child_count(node); + for (uint32_t ci = 0; ci < nc; ci++) { + TSNode child = ts_node_child(node, ci); + const char *ck = ts_node_type(child); + if (strcmp(ck, "field_declaration_list") == 0 || strcmp(ck, "class_body") == 0 || + strcmp(ck, "declaration_list") == 0 || strcmp(ck, "body") == 0 || + strcmp(ck, "block") == 0 || strcmp(ck, "suite") == 0) { + push_nested_class_nodes(child, spec, stack, top, new_enclosing); + return; + } + } + // No body found — push all children directly + for (int ci = (int)nc - 1; ci >= 0 && *top < CBM_WALK_DEFS_STACK_CAP; ci--) { + stack[(*top)++] = (walk_defs_frame_t){ts_node_child(node, (uint32_t)ci), new_enclosing}; + } +} + static void walk_defs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec, int depth_unused) { (void)depth_unused; walk_defs_frame_t stack[CBM_WALK_DEFS_STACK_CAP]; @@ -2851,92 +2985,32 @@ static void walk_defs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec, ctx->enclosing_class_qn = frame.enclosing_class_qn; const char *kind = ts_node_type(node); - // Elixir: all defs are call nodes if (ctx->language == CBM_LANG_ELIXIR && strcmp(kind, "call") == 0) { extract_elixir_call(ctx, node, spec); continue; } - // Function types if (cbm_kind_in_set(node, spec->function_node_types)) { - bool is_template_class = false; - if ((ctx->language == CBM_LANG_CPP || ctx->language == CBM_LANG_CUDA) && - strcmp(kind, "template_declaration") == 0) { - uint32_t nc = ts_node_named_child_count(node); - for (uint32_t i = 0; i < nc; i++) { - const char *ck = ts_node_type(ts_node_named_child(node, i)); - if (strcmp(ck, "class_specifier") == 0 || strcmp(ck, "struct_specifier") == 0 || - strcmp(ck, "union_specifier") == 0) { - is_template_class = true; - break; - } - } - } - if (!is_template_class) { + if (!is_template_class_node(node, ctx->language)) { extract_func_def(ctx, node, spec); if (ctx->language != CBM_LANG_WOLFRAM) { - continue; // don't push function body children + continue; } } } - // Rust impl blocks if (ctx->language == CBM_LANG_RUST && strcmp(kind, "impl_item") == 0) { extract_rust_impl(ctx, node, spec); continue; } - // Class types if (cbm_kind_in_set(node, spec->class_node_types)) { - const char *saved_enclosing = frame.enclosing_class_qn; extract_class_def(ctx, node, spec); - // Compute new enclosing class QN for nested classes - const char *new_enclosing = saved_enclosing; - { - TSNode name_node = ts_node_child_by_field_name(node, "name", 4); - if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_OBJC) { - name_node = cbm_find_child_by_kind(node, "identifier"); - } - if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_SWIFT) { - name_node = cbm_find_child_by_kind(node, "type_identifier"); - } - if (!ts_node_is_null(name_node)) { - char *cname = cbm_node_text(ctx->arena, name_node, ctx->source); - if (cname && cname[0]) { - if (saved_enclosing) { - new_enclosing = - cbm_arena_sprintf(ctx->arena, "%s.%s", saved_enclosing, cname); - } else { - new_enclosing = - cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, cname); - } - } - } - } - // Push nested class children with the new enclosing context - bool found_body = false; - uint32_t nc = ts_node_child_count(node); - for (uint32_t ci = 0; ci < nc; ci++) { - TSNode child = ts_node_child(node, ci); - const char *ck = ts_node_type(child); - if (strcmp(ck, "field_declaration_list") == 0 || strcmp(ck, "class_body") == 0 || - strcmp(ck, "declaration_list") == 0 || strcmp(ck, "body") == 0 || - strcmp(ck, "block") == 0 || strcmp(ck, "suite") == 0) { - push_nested_class_nodes(child, spec, stack, &top, new_enclosing); - found_body = true; - break; - } - } - if (!found_body) { - for (int ci = (int)nc - 1; ci >= 0 && top < CBM_WALK_DEFS_STACK_CAP; ci--) { - stack[top++] = - (walk_defs_frame_t){ts_node_child(node, (uint32_t)ci), new_enclosing}; - } - } + const char *new_enclosing = compute_class_qn(ctx, node, frame.enclosing_class_qn); + push_class_body_children(node, spec, stack, &top, new_enclosing); continue; } - // Default: push all children (reverse order for left-to-right processing) uint32_t count = ts_node_child_count(node); for (int i = (int)count - 1; i >= 0 && top < CBM_WALK_DEFS_STACK_CAP; i--) { stack[top++] = diff --git a/internal/cbm/extract_imports.c b/internal/cbm/extract_imports.c index 7293cf4cd..51a6f353b 100644 --- a/internal/cbm/extract_imports.c +++ b/internal/cbm/extract_imports.c @@ -52,10 +52,27 @@ static const char *path_last(CBMArena *a, const char *path) { // --- Go imports --- // import_declaration -> import_spec_list -> import_spec -> (name, path) -// NOLINTNEXTLINE(readability-function-cognitive-complexity) -static void parse_go_imports(CBMExtractCtx *ctx) { +// Parse a single Go import_spec node. +static void parse_go_import_spec(CBMExtractCtx *ctx, TSNode spec) { CBMArena *a = ctx->arena; + TSNode path_node = ts_node_child_by_field_name(spec, "path", 4); + if (ts_node_is_null(path_node)) { + return; + } + char *path = strip_quotes(a, cbm_node_text(a, path_node, ctx->source)); + if (!path || !path[0]) { + return; + } + TSNode name_node = ts_node_child_by_field_name(spec, "name", 4); + const char *local_name = + !ts_node_is_null(name_node) ? cbm_node_text(a, name_node, ctx->source) : path_last(a, path); + + CBMImport imp = {.local_name = local_name, .module_path = path}; + cbm_imports_push(&ctx->result->imports, a, imp); +} + +static void parse_go_imports(CBMExtractCtx *ctx) { TSTreeCursor cursor = ts_tree_cursor_new(ctx->root); if (!ts_tree_cursor_goto_first_child(&cursor)) { ts_tree_cursor_delete(&cursor); @@ -71,58 +88,15 @@ static void parse_go_imports(CBMExtractCtx *ctx) { for (uint32_t j = 0; j < dc; j++) { TSNode child = ts_node_child(decl, j); const char *ck = ts_node_type(child); - if (strcmp(ck, "import_spec") != 0 && strcmp(ck, "import_spec_list") != 0) { - continue; - } - if (strcmp(ck, "import_spec") == 0) { - TSNode path_node = ts_node_child_by_field_name(child, "path", 4); - if (ts_node_is_null(path_node)) { - continue; - } - char *path = strip_quotes(a, cbm_node_text(a, path_node, ctx->source)); - if (!path || !path[0]) { - continue; - } - - TSNode name_node = ts_node_child_by_field_name(child, "name", 4); - const char *local_name; - if (!ts_node_is_null(name_node)) { - local_name = cbm_node_text(a, name_node, ctx->source); - } else { - local_name = path_last(a, path); - } - - CBMImport imp = {.local_name = local_name, .module_path = path}; - cbm_imports_push(&ctx->result->imports, a, imp); - } else { - // import_spec_list: contains multiple import_spec children + parse_go_import_spec(ctx, child); + } else if (strcmp(ck, "import_spec_list") == 0) { uint32_t sc = ts_node_child_count(child); for (uint32_t k = 0; k < sc; k++) { TSNode spec = ts_node_child(child, k); - if (strcmp(ts_node_type(spec), "import_spec") != 0) { - continue; + if (strcmp(ts_node_type(spec), "import_spec") == 0) { + parse_go_import_spec(ctx, spec); } - - TSNode path_node = ts_node_child_by_field_name(spec, "path", 4); - if (ts_node_is_null(path_node)) { - continue; - } - char *path = strip_quotes(a, cbm_node_text(a, path_node, ctx->source)); - if (!path || !path[0]) { - continue; - } - - TSNode name_node = ts_node_child_by_field_name(spec, "name", 4); - const char *local_name; - if (!ts_node_is_null(name_node)) { - local_name = cbm_node_text(a, name_node, ctx->source); - } else { - local_name = path_last(a, path); - } - - CBMImport imp = {.local_name = local_name, .module_path = path}; - cbm_imports_push(&ctx->result->imports, a, imp); } } } @@ -134,10 +108,106 @@ static void parse_go_imports(CBMExtractCtx *ctx) { // import_statement: import X, import X as Y // import_from_statement: from X import Y, from X import Y as Z -// NOLINTNEXTLINE(readability-function-cognitive-complexity) -static void parse_python_imports(CBMExtractCtx *ctx) { +// Emit a Python aliased_import (import X as Y / from X import Y as Z). +static void emit_py_aliased_import(CBMExtractCtx *ctx, TSNode child, const char *mod_prefix) { CBMArena *a = ctx->arena; + TSNode mod_node = ts_node_child_by_field_name(child, "name", 4); + TSNode alias_node = ts_node_child_by_field_name(child, "alias", 5); + if (ts_node_is_null(mod_node)) { + return; + } + char *name = cbm_node_text(a, mod_node, ctx->source); + if (!name || !name[0]) { + return; + } + const char *local = !ts_node_is_null(alias_node) ? cbm_node_text(a, alias_node, ctx->source) + : path_last(a, name); + const char *full = mod_prefix ? cbm_arena_sprintf(a, "%s.%s", mod_prefix, name) : name; + CBMImport imp = {.local_name = local, .module_path = full}; + cbm_imports_push(&ctx->result->imports, a, imp); +} +// Process a single Python import_statement node (import X, import X as Y). +static void process_py_import_stmt(CBMExtractCtx *ctx, TSNode node) { + CBMArena *a = ctx->arena; + TSNode name_node = ts_node_child_by_field_name(node, "name", 4); + if (ts_node_is_null(name_node)) { + uint32_t nc = ts_node_child_count(node); + for (uint32_t j = 0; j < nc; j++) { + TSNode child = ts_node_child(node, j); + const char *ck = ts_node_type(child); + if (strcmp(ck, "dotted_name") == 0 || strcmp(ck, "identifier") == 0) { + char *mod = cbm_node_text(a, child, ctx->source); + if (mod && mod[0]) { + CBMImport imp = {.local_name = path_last(a, mod), .module_path = mod}; + cbm_imports_push(&ctx->result->imports, a, imp); + } + } else if (strcmp(ck, "aliased_import") == 0) { + emit_py_aliased_import(ctx, child, NULL); + } + } + } else { + char *mod = cbm_node_text(a, name_node, ctx->source); + if (mod && mod[0]) { + CBMImport imp = {.local_name = path_last(a, mod), .module_path = mod}; + cbm_imports_push(&ctx->result->imports, a, imp); + } + } +} + +// Resolve the module_name node for a Python import_from_statement. +static TSNode resolve_py_module_node(TSNode node) { + TSNode module_node = ts_node_child_by_field_name(node, "module_name", FIELD_LEN_MODULE_NAME); + if (ts_node_is_null(module_node)) { + uint32_t nc = ts_node_child_count(node); + for (uint32_t j = 0; j < nc; j++) { + TSNode c = ts_node_child(node, j); + if (strcmp(ts_node_type(c), "dotted_name") == 0 || + strcmp(ts_node_type(c), "relative_import") == 0) { + return c; + } + } + } + return module_node; +} + +// Emit a Python import-from name child (identifier/dotted_name). +static void emit_py_import_from_name(CBMExtractCtx *ctx, TSNode child, const char *mod_path) { + CBMArena *a = ctx->arena; + char *name = cbm_node_text(a, child, ctx->source); + if (name && name[0]) { + const char *full = mod_path ? cbm_arena_sprintf(a, "%s.%s", mod_path, name) : name; + CBMImport imp = {.local_name = name, .module_path = full}; + cbm_imports_push(&ctx->result->imports, a, imp); + } +} + +// Process a single Python import_from_statement node (from X import Y [as Z]). +static void process_py_import_from(CBMExtractCtx *ctx, TSNode node) { + CBMArena *a = ctx->arena; + TSNode module_node = resolve_py_module_node(node); + // NOLINTBEGIN(readability-implicit-bool-conversion) + char *mod_path = + ts_node_is_null(module_node) ? NULL : cbm_node_text(a, module_node, ctx->source); + // NOLINTEND(readability-implicit-bool-conversion) + + uint32_t nc = ts_node_child_count(node); + for (uint32_t j = 0; j < nc; j++) { + TSNode child = ts_node_child(node, j); + const char *ck = ts_node_type(child); + if (strcmp(ck, "identifier") == 0 || strcmp(ck, "dotted_name") == 0) { + if (!ts_node_is_null(module_node) && + ts_node_start_byte(child) == ts_node_start_byte(module_node)) { + continue; + } + emit_py_import_from_name(ctx, child, mod_path); + } else if (strcmp(ck, "aliased_import") == 0) { + emit_py_aliased_import(ctx, child, mod_path); + } + } +} + +static void parse_python_imports(CBMExtractCtx *ctx) { TSTreeCursor cursor = ts_tree_cursor_new(ctx->root); if (!ts_tree_cursor_goto_first_child(&cursor)) { ts_tree_cursor_delete(&cursor); @@ -148,95 +218,9 @@ static void parse_python_imports(CBMExtractCtx *ctx) { const char *kind = ts_node_type(node); if (strcmp(kind, "import_statement") == 0) { - // import X [as Y] - TSNode name_node = ts_node_child_by_field_name(node, "name", 4); - if (ts_node_is_null(name_node)) { - // Try children: dotted_name nodes - uint32_t nc = ts_node_child_count(node); - for (uint32_t j = 0; j < nc; j++) { - TSNode child = ts_node_child(node, j); - const char *ck = ts_node_type(child); - if (strcmp(ck, "dotted_name") == 0 || strcmp(ck, "identifier") == 0) { - char *mod = cbm_node_text(a, child, ctx->source); - if (mod && mod[0]) { - const char *local = path_last(a, mod); - CBMImport imp = {.local_name = local, .module_path = mod}; - cbm_imports_push(&ctx->result->imports, a, imp); - } - } else if (strcmp(ck, "aliased_import") == 0) { - TSNode mod_node = ts_node_child_by_field_name(child, "name", 4); - TSNode alias_node = ts_node_child_by_field_name(child, "alias", 5); - if (!ts_node_is_null(mod_node)) { - char *mod = cbm_node_text(a, mod_node, ctx->source); - const char *local = !ts_node_is_null(alias_node) - ? cbm_node_text(a, alias_node, ctx->source) - : path_last(a, mod); - CBMImport imp = {.local_name = local, .module_path = mod}; - cbm_imports_push(&ctx->result->imports, a, imp); - } - } - } - } else { - char *mod = cbm_node_text(a, name_node, ctx->source); - if (mod && mod[0]) { - CBMImport imp = {.local_name = path_last(a, mod), .module_path = mod}; - cbm_imports_push(&ctx->result->imports, a, imp); - } - } + process_py_import_stmt(ctx, node); } else if (strcmp(kind, "import_from_statement") == 0) { - // from X import Y [as Z] - TSNode module_node = - ts_node_child_by_field_name(node, "module_name", FIELD_LEN_MODULE_NAME); - if (ts_node_is_null(module_node)) { - // Try alternative field names - uint32_t nc = ts_node_child_count(node); - for (uint32_t j = 0; j < nc; j++) { - TSNode c = ts_node_child(node, j); - if (strcmp(ts_node_type(c), "dotted_name") == 0 || - strcmp(ts_node_type(c), "relative_import") == 0) { - module_node = c; - break; - } - } - } - // NOLINTBEGIN(readability-implicit-bool-conversion) - char *mod_path = - ts_node_is_null(module_node) ? NULL : cbm_node_text(a, module_node, ctx->source); - // NOLINTEND(readability-implicit-bool-conversion) - - // Find imported names - uint32_t nc = ts_node_child_count(node); - for (uint32_t j = 0; j < nc; j++) { - TSNode child = ts_node_child(node, j); - const char *ck = ts_node_type(child); - if (strcmp(ck, "identifier") == 0 || strcmp(ck, "dotted_name") == 0) { - // Skip the module name node - if (!ts_node_is_null(module_node) && - ts_node_start_byte(child) == ts_node_start_byte(module_node)) { - continue; - } - char *name = cbm_node_text(a, child, ctx->source); - if (name && name[0]) { - const char *full = - mod_path ? cbm_arena_sprintf(a, "%s.%s", mod_path, name) : name; - CBMImport imp = {.local_name = name, .module_path = full}; - cbm_imports_push(&ctx->result->imports, a, imp); - } - } else if (strcmp(ck, "aliased_import") == 0) { - TSNode name_n = ts_node_child_by_field_name(child, "name", 4); - TSNode alias_n = ts_node_child_by_field_name(child, "alias", 5); - if (!ts_node_is_null(name_n)) { - char *name = cbm_node_text(a, name_n, ctx->source); - const char *local = !ts_node_is_null(alias_n) - ? cbm_node_text(a, alias_n, ctx->source) - : name; - const char *full = - mod_path ? cbm_arena_sprintf(a, "%s.%s", mod_path, name) : name; - CBMImport imp = {.local_name = local, .module_path = full}; - cbm_imports_push(&ctx->result->imports, a, imp); - } - } - } + process_py_import_from(ctx, node); } } while (ts_tree_cursor_goto_next_sibling(&cursor)); ts_tree_cursor_delete(&cursor); @@ -246,25 +230,87 @@ static void parse_python_imports(CBMExtractCtx *ctx) { // import X from "Y"; import {A, B} from "Y"; import * as X from "Y" // const X = require("Y") -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Find the source string node in an ES import_statement. +static TSNode find_es_source_node(TSNode node) { + TSNode source_node = ts_node_child_by_field_name(node, "source", 6); + if (ts_node_is_null(source_node)) { + uint32_t nc = ts_node_child_count(node); + for (int j = (int)nc - 1; j >= 0; j--) { + TSNode c = ts_node_child(node, (uint32_t)j); + const char *ck = ts_node_type(c); + if (strcmp(ck, "string") == 0 || strcmp(ck, "string_literal") == 0) { + return c; + } + } + } + return source_node; +} + +// Process named_imports: import {A, B as C} from "path". +static bool process_named_imports(CBMExtractCtx *ctx, TSNode sub, const char *path) { + CBMArena *a = ctx->arena; + bool found = false; + uint32_t nc2 = ts_node_child_count(sub); + for (uint32_t m = 0; m < nc2; m++) { + TSNode imp_spec = ts_node_child(sub, m); + if (strcmp(ts_node_type(imp_spec), "import_specifier") != 0) { + continue; + } + TSNode local = ts_node_child_by_field_name(imp_spec, "alias", 5); + TSNode orig = ts_node_child_by_field_name(imp_spec, "name", 4); + if (ts_node_is_null(orig) && ts_node_child_count(imp_spec) > 0) { + orig = ts_node_child(imp_spec, 0); + } + if (!ts_node_is_null(orig)) { + char *local_name = !ts_node_is_null(local) ? cbm_node_text(a, local, ctx->source) + : cbm_node_text(a, orig, ctx->source); + CBMImport imp = {.local_name = local_name, .module_path = path}; + cbm_imports_push(&ctx->result->imports, a, imp); + found = true; + } + } + return found; +} + +// Process an import_clause node: default, namespace, and named imports. +static bool process_import_clause(CBMExtractCtx *ctx, TSNode clause, const char *path) { + CBMArena *a = ctx->arena; + bool found = false; + uint32_t cc = ts_node_child_count(clause); + for (uint32_t k = 0; k < cc; k++) { + TSNode sub = ts_node_child(clause, k); + const char *sk = ts_node_type(sub); + if (strcmp(sk, "identifier") == 0) { + char *name = cbm_node_text(a, sub, ctx->source); + CBMImport imp = {.local_name = name, .module_path = path}; + cbm_imports_push(&ctx->result->imports, a, imp); + found = true; + } else if (strcmp(sk, "namespace_import") == 0) { + TSNode as_name = ts_node_child_by_field_name(sub, "name", 4); + if (ts_node_is_null(as_name) && ts_node_child_count(sub) > 0) { + as_name = ts_node_child(sub, ts_node_child_count(sub) - 1); + } + if (!ts_node_is_null(as_name)) { + char *name = cbm_node_text(a, as_name, ctx->source); + CBMImport imp = {.local_name = name, .module_path = path}; + cbm_imports_push(&ctx->result->imports, a, imp); + found = true; + } + } else if (strcmp(sk, "named_imports") == 0) { + if (process_named_imports(ctx, sub, path)) { + found = true; + } + } + } + return found; +} + static void walk_es_imports(CBMExtractCtx *ctx, TSNode node) { CBMArena *a = ctx->arena; const char *kind = ts_node_type(node); if (strcmp(kind, "import_statement") == 0) { - TSNode source_node = ts_node_child_by_field_name(node, "source", 6); - if (ts_node_is_null(source_node)) { - // Try last string child - uint32_t nc = ts_node_child_count(node); - for (int j = (int)nc - 1; j >= 0; j--) { - TSNode c = ts_node_child(node, (uint32_t)j); - const char *ck = ts_node_type(c); - if (strcmp(ck, "string") == 0 || strcmp(ck, "string_literal") == 0) { - source_node = c; - break; - } - } - } + TSNode source_node = find_es_source_node(node); if (ts_node_is_null(source_node)) { goto recurse; } @@ -274,7 +320,6 @@ static void walk_es_imports(CBMExtractCtx *ctx, TSNode node) { goto recurse; } - // Default import, namespace import, named imports uint32_t nc = ts_node_child_count(node); bool found = false; for (uint32_t j = 0; j < nc; j++) { @@ -282,60 +327,18 @@ static void walk_es_imports(CBMExtractCtx *ctx, TSNode node) { const char *ck = ts_node_type(child); if (strcmp(ck, "identifier") == 0) { - // Default import: import X from "Y" char *name = cbm_node_text(a, child, ctx->source); CBMImport imp = {.local_name = name, .module_path = path}; cbm_imports_push(&ctx->result->imports, a, imp); found = true; } else if (strcmp(ck, "import_clause") == 0) { - // Walk into import clause for named/default imports - uint32_t cc = ts_node_child_count(child); - for (uint32_t k = 0; k < cc; k++) { - TSNode sub = ts_node_child(child, k); - const char *sk = ts_node_type(sub); - if (strcmp(sk, "identifier") == 0) { - char *name = cbm_node_text(a, sub, ctx->source); - CBMImport imp = {.local_name = name, .module_path = path}; - cbm_imports_push(&ctx->result->imports, a, imp); - found = true; - } else if (strcmp(sk, "namespace_import") == 0) { - TSNode as_name = ts_node_child_by_field_name(sub, "name", 4); - if (ts_node_is_null(as_name) && ts_node_child_count(sub) > 0) { - as_name = ts_node_child(sub, ts_node_child_count(sub) - 1); - } - if (!ts_node_is_null(as_name)) { - char *name = cbm_node_text(a, as_name, ctx->source); - CBMImport imp = {.local_name = name, .module_path = path}; - cbm_imports_push(&ctx->result->imports, a, imp); - found = true; - } - } else if (strcmp(sk, "named_imports") == 0) { - uint32_t nc2 = ts_node_child_count(sub); - for (uint32_t m = 0; m < nc2; m++) { - TSNode imp_spec = ts_node_child(sub, m); - if (strcmp(ts_node_type(imp_spec), "import_specifier") == 0) { - TSNode local = ts_node_child_by_field_name(imp_spec, "alias", 5); - TSNode orig = ts_node_child_by_field_name(imp_spec, "name", 4); - if (ts_node_is_null(orig) && ts_node_child_count(imp_spec) > 0) { - orig = ts_node_child(imp_spec, 0); - } - if (!ts_node_is_null(orig)) { - char *local_name = !ts_node_is_null(local) - ? cbm_node_text(a, local, ctx->source) - : cbm_node_text(a, orig, ctx->source); - CBMImport imp = {.local_name = local_name, .module_path = path}; - cbm_imports_push(&ctx->result->imports, a, imp); - found = true; - } - } - } - } + if (process_import_clause(ctx, child, path)) { + found = true; } } } if (!found) { - // Side-effect import: import "Y" CBMImport imp = {.local_name = path_last(a, path), .module_path = path}; cbm_imports_push(&ctx->result->imports, a, imp); } @@ -427,7 +430,33 @@ static void parse_rust_imports(CBMExtractCtx *ctx) { // --- C/C++ imports --- // preproc_include -> path or string_literal -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Find the path node inside a preproc_include/preproc_import node. +static TSNode find_include_path_node(TSNode node) { + TSNode path_node = ts_node_child_by_field_name(node, "path", 4); + if (ts_node_is_null(path_node)) { + uint32_t nc = ts_node_child_count(node); + for (uint32_t j = 0; j < nc; j++) { + TSNode c = ts_node_child(node, j); + const char *ck = ts_node_type(c); + if (strcmp(ck, "string_literal") == 0 || strcmp(ck, "system_lib_string") == 0) { + return c; + } + } + } + return path_node; +} + +// Strip angle brackets from a system include path ( → stdio.h). +static char *strip_angle_brackets(CBMArena *a, char *path) { + if (path && path[0] == '<') { + size_t len = strlen(path); + if (len > 1 && path[len - 1] == '>') { + return cbm_arena_strndup(a, path + 1, len - 2); + } + } + return path; +} + static void parse_c_imports(CBMExtractCtx *ctx) { CBMArena *a = ctx->arena; @@ -443,31 +472,13 @@ static void parse_c_imports(CBMExtractCtx *ctx) { continue; } - TSNode path_node = ts_node_child_by_field_name(node, "path", 4); - if (ts_node_is_null(path_node)) { - // Try system_lib_string or string_literal - uint32_t nc = ts_node_child_count(node); - for (uint32_t j = 0; j < nc; j++) { - TSNode c = ts_node_child(node, j); - const char *ck = ts_node_type(c); - if (strcmp(ck, "string_literal") == 0 || strcmp(ck, "system_lib_string") == 0) { - path_node = c; - break; - } - } - } + TSNode path_node = find_include_path_node(node); if (ts_node_is_null(path_node)) { continue; } char *path = strip_quotes(a, cbm_node_text(a, path_node, ctx->source)); - // Also strip angle brackets - if (path && path[0] == '<') { - size_t len = strlen(path); - if (len > 1 && path[len - 1] == '>') { - path = cbm_arena_strndup(a, path + 1, len - 2); - } - } + path = strip_angle_brackets(a, path); if (!path || !path[0]) { continue; } @@ -481,12 +492,48 @@ static void parse_c_imports(CBMExtractCtx *ctx) { // --- Ruby imports --- // call nodes: require("X"), require_relative("X") -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Check if a Ruby call node is a require/require_relative, return method name or NULL. +static const char *ruby_require_method(CBMArena *a, TSNode node, const char *source) { + TSNode method = ts_node_child_by_field_name(node, "method", 6); + if (ts_node_is_null(method) && ts_node_child_count(node) > 0) { + method = ts_node_child(node, 0); + } + if (ts_node_is_null(method)) { + return NULL; + } + char *name = cbm_node_text(a, method, source); + if (!name || (strcmp(name, "require") != 0 && strcmp(name, "require_relative") != 0)) { + return NULL; + } + return name; +} + +// Extract string argument from a Ruby require/require_relative call. +static char *extract_ruby_require_arg(CBMArena *a, TSNode node, const char *source) { + TSNode args = ts_node_child_by_field_name(node, "arguments", 9); + if (ts_node_is_null(args)) { + if (ts_node_child_count(node) > 1) { + args = ts_node_child(node, 1); + } + } + if (ts_node_is_null(args)) { + return NULL; + } + + uint32_t ac = ts_node_child_count(args); + for (uint32_t j = 0; j < ac; j++) { + TSNode c = ts_node_child(args, j); + const char *ck = ts_node_type(c); + if (strcmp(ck, "string") == 0 || strcmp(ck, "string_literal") == 0) { + return strip_quotes(a, cbm_node_text(a, c, source)); + } + } + return strip_quotes(a, cbm_node_text(a, args, source)); +} + static void parse_ruby_imports(CBMExtractCtx *ctx) { CBMArena *a = ctx->arena; - // Walk for call nodes with "require" or "require_relative" - // Simple: walk top-level children TSTreeCursor cursor = ts_tree_cursor_new(ctx->root); if (!ts_tree_cursor_goto_first_child(&cursor)) { ts_tree_cursor_delete(&cursor); @@ -499,45 +546,11 @@ static void parse_ruby_imports(CBMExtractCtx *ctx) { continue; } - TSNode method = ts_node_child_by_field_name(node, "method", 6); - if (ts_node_is_null(method) && ts_node_child_count(node) > 0) { - method = ts_node_child(node, 0); - } - if (ts_node_is_null(method)) { + if (!ruby_require_method(a, node, ctx->source)) { continue; } - char *name = cbm_node_text(a, method, ctx->source); - if (!name || (strcmp(name, "require") != 0 && strcmp(name, "require_relative") != 0)) { - continue; - } - - // Get the argument (string literal) - TSNode args = ts_node_child_by_field_name(node, "arguments", 9); - if (ts_node_is_null(args)) { - // Try second child - if (ts_node_child_count(node) > 1) { - args = ts_node_child(node, 1); - } - } - if (ts_node_is_null(args)) { - continue; - } - - // Find string literal in args - char *arg_text = NULL; - uint32_t ac = ts_node_child_count(args); - for (uint32_t j = 0; j < ac; j++) { - TSNode c = ts_node_child(args, j); - const char *ck = ts_node_type(c); - if (strcmp(ck, "string") == 0 || strcmp(ck, "string_literal") == 0) { - arg_text = strip_quotes(a, cbm_node_text(a, c, ctx->source)); - break; - } - } - if (!arg_text) { - arg_text = strip_quotes(a, cbm_node_text(a, args, ctx->source)); - } + char *arg_text = extract_ruby_require_arg(a, node, ctx->source); if (!arg_text || !arg_text[0]) { continue; } @@ -611,15 +624,47 @@ static void parse_lua_imports(CBMExtractCtx *ctx) { // --- Generic import parsing for languages with simple import_declaration --- -// NOLINTNEXTLINE(readability-function-cognitive-complexity) -static void parse_generic_imports(CBMExtractCtx *ctx, const char *node_type) { +// Try known field names (path/source/module/name) to extract import path. +static bool try_generic_path_fields(CBMExtractCtx *ctx, TSNode node) { CBMArena *a = ctx->arena; + static const char *path_fields[] = {"path", "source", "module", "name", NULL}; + for (const char **f = path_fields; *f; f++) { + TSNode path_node = ts_node_child_by_field_name(node, *f, (uint32_t)strlen(*f)); + if (!ts_node_is_null(path_node)) { + char *path = strip_quotes(a, cbm_node_text(a, path_node, ctx->source)); + if (path && path[0]) { + CBMImport imp = {.local_name = path_last(a, path), .module_path = path}; + cbm_imports_push(&ctx->result->imports, a, imp); + } + return true; + } + } + return false; +} - /* Use TSTreeCursor for O(1)-per-step sibling traversal. - * ts_node_child(root, i) and ts_node_next_sibling() both call - * ts_node_child_with_descendant() internally, making naive iteration - * O(N²) on roots with thousands of children (e.g. generated .d.ts files). - * ts_tree_cursor_goto_next_sibling() maintains cursor state and is O(1). */ +// Fallback: extract import path from full node text, stripping keyword and semicolon. +static void generic_import_from_text(CBMExtractCtx *ctx, TSNode node) { + CBMArena *a = ctx->arena; + char *text = cbm_node_text(a, node, ctx->source); + if (!text || !text[0]) { + return; + } + char *space = strchr(text, ' '); + if (space) { + text = space + 1; + } + size_t len = strlen(text); + if (len > 0 && text[len - 1] == ';') { + text[len - 1] = '\0'; + } + if (text[0]) { + CBMImport imp = {.local_name = path_last(a, text), .module_path = text}; + cbm_imports_push(&ctx->result->imports, a, imp); + } +} + +static void parse_generic_imports(CBMExtractCtx *ctx, const char *node_type) { + /* Use TSTreeCursor for O(1)-per-step sibling traversal. */ TSTreeCursor cursor = ts_tree_cursor_new(ctx->root); if (!ts_tree_cursor_goto_first_child(&cursor)) { ts_tree_cursor_delete(&cursor); @@ -631,39 +676,8 @@ static void parse_generic_imports(CBMExtractCtx *ctx, const char *node_type) { continue; } - // Try to find a path/source/module field - static const char *path_fields[] = {"path", "source", "module", "name", NULL}; - for (const char **f = path_fields; *f; f++) { - TSNode path_node = ts_node_child_by_field_name(node, *f, (uint32_t)strlen(*f)); - if (!ts_node_is_null(path_node)) { - char *path = strip_quotes(a, cbm_node_text(a, path_node, ctx->source)); - if (path && path[0]) { - CBMImport imp = {.local_name = path_last(a, path), .module_path = path}; - cbm_imports_push(&ctx->result->imports, a, imp); - } - break; - } - } - - // Fallback: use full node text minus keyword - if (ctx->result->imports.count == 0) { - char *text = cbm_node_text(a, node, ctx->source); - if (text && text[0]) { - // Strip leading keyword - char *space = strchr(text, ' '); - if (space) { - text = space + 1; - } - // Strip trailing semicolon - size_t len = strlen(text); - if (len > 0 && text[len - 1] == ';') { - text[len - 1] = '\0'; - } - if (text[0]) { - CBMImport imp = {.local_name = path_last(a, text), .module_path = text}; - cbm_imports_push(&ctx->result->imports, a, imp); - } - } + if (!try_generic_path_fields(ctx, node)) { + generic_import_from_text(ctx, node); } } while (ts_tree_cursor_goto_next_sibling(&cursor)); ts_tree_cursor_delete(&cursor); @@ -673,48 +687,57 @@ static void parse_generic_imports(CBMExtractCtx *ctx, const char *node_type) { // get_top: << "package" (Get["file"]) // apply where first child is builtin_symbol "Needs" with string arg -// NOLINTNEXTLINE(readability-function-cognitive-complexity) -static void walk_wolfram_imports(CBMExtractCtx *ctx, TSNode node) { +// Handle Wolfram get_top: << "path" → import. +static void process_wolfram_get_top(CBMExtractCtx *ctx, TSNode node) { CBMArena *a = ctx->arena; + uint32_t nc = ts_node_named_child_count(node); + for (uint32_t i = 0; i < nc; i++) { + TSNode child = ts_node_named_child(node, i); + const char *ck = ts_node_type(child); + if (strcmp(ck, "string") == 0 || strcmp(ck, "user_symbol") == 0) { + char *text = cbm_node_text(a, child, ctx->source); + if (text && text[0]) { + char *path = strip_quotes(a, text); + CBMImport imp = {.local_name = path_last(a, path), .module_path = path}; + cbm_imports_push(&ctx->result->imports, a, imp); + } + break; + } + } +} + +// Handle Wolfram Needs["package`"] — apply where head is builtin_symbol "Needs". +static void process_wolfram_needs(CBMExtractCtx *ctx, TSNode node) { + CBMArena *a = ctx->arena; + if (ts_node_named_child_count(node) < 2) { + return; + } + TSNode head = ts_node_named_child(node, 0); + if (strcmp(ts_node_type(head), "builtin_symbol") != 0) { + return; + } + char *name = cbm_node_text(a, head, ctx->source); + if (!name || strcmp(name, "Needs") != 0) { + return; + } + TSNode arg = ts_node_named_child(node, 1); + char *text = cbm_node_text(a, arg, ctx->source); + if (text && text[0]) { + char *path = strip_quotes(a, text); + CBMImport imp = {.local_name = path_last(a, path), .module_path = path}; + cbm_imports_push(&ctx->result->imports, a, imp); + } +} + +static void walk_wolfram_imports(CBMExtractCtx *ctx, TSNode node) { const char *kind = ts_node_type(node); - // get_top: << "path" → import if (strcmp(kind, "get_top") == 0) { - uint32_t nc = ts_node_named_child_count(node); - for (uint32_t i = 0; i < nc; i++) { - TSNode child = ts_node_named_child(node, i); - const char *ck = ts_node_type(child); - if (strcmp(ck, "string") == 0 || strcmp(ck, "user_symbol") == 0) { - char *text = cbm_node_text(a, child, ctx->source); - if (text && text[0]) { - char *path = strip_quotes(a, text); - CBMImport imp = {.local_name = path_last(a, path), .module_path = path}; - cbm_imports_push(&ctx->result->imports, a, imp); - } - break; - } - } + process_wolfram_get_top(ctx, node); + } else if (strcmp(kind, "apply") == 0) { + process_wolfram_needs(ctx, node); } - // Needs["package`"] — apply where head is builtin_symbol "Needs" - if (strcmp(kind, "apply") == 0 && ts_node_named_child_count(node) >= 2) { - TSNode head = ts_node_named_child(node, 0); - if (strcmp(ts_node_type(head), "builtin_symbol") == 0) { - char *name = cbm_node_text(a, head, ctx->source); - if (name && strcmp(name, "Needs") == 0) { - // Second named child should be the string argument - TSNode arg = ts_node_named_child(node, 1); - char *text = cbm_node_text(a, arg, ctx->source); - if (text && text[0]) { - char *path = strip_quotes(a, text); - CBMImport imp = {.local_name = path_last(a, path), .module_path = path}; - cbm_imports_push(&ctx->result->imports, a, imp); - } - } - } - } - - // Recurse uint32_t count = ts_node_child_count(node); for (uint32_t i = 0; i < count; i++) { walk_wolfram_imports(ctx, ts_node_child(node, i)); diff --git a/internal/cbm/extract_k8s.c b/internal/cbm/extract_k8s.c index 3f10e1cb3..446761a97 100644 --- a/internal/cbm/extract_k8s.c +++ b/internal/cbm/extract_k8s.c @@ -91,11 +91,51 @@ static void emit_kustomize_sequence(CBMExtractCtx *ctx, TSNode seq_node, const c } } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) -static void extract_kustomize(CBMExtractCtx *ctx) { - CBMArena *a = ctx->arena; +// Unwrap a YAML node through optional block_node wrapper to get block_mapping. +// Returns null node if not a block_mapping. +static TSNode unwrap_block_mapping(TSNode doc_child) { + TSNode mapping = ts_node_named_child(doc_child, 0); + if (ts_node_is_null(mapping)) { + return mapping; + } + if (strcmp(ts_node_type(mapping), "block_node") == 0) { + mapping = ts_node_named_child(mapping, 0); + } + if (ts_node_is_null(mapping) || strcmp(ts_node_type(mapping), "block_mapping") != 0) { + TSNode null_node = {0}; + return null_node; + } + return mapping; +} - // Traverse: stream -> document -> block_node -> block_mapping -> block_mapping_pair +// Process a single block_mapping_pair for kustomize list keys. +static void process_kustomize_pair(CBMExtractCtx *ctx, TSNode pair) { + if (strcmp(ts_node_type(pair), "block_mapping_pair") != 0) { + return; + } + TSNode key_node = ts_node_named_child(pair, 0); + if (ts_node_is_null(key_node)) { + return; + } + const char *key_text = get_scalar_text(ctx->arena, key_node, ctx->source); + if (!key_text || !is_kustomize_list_key(key_text)) { + return; + } + + TSNode val_node = ts_node_named_child(pair, 1); + if (ts_node_is_null(val_node)) { + return; + } + if (strcmp(ts_node_type(val_node), "block_node") == 0) { + val_node = ts_node_named_child(val_node, 0); + } + if (ts_node_is_null(val_node) || strcmp(ts_node_type(val_node), "block_sequence") != 0) { + return; + } + emit_kustomize_sequence(ctx, val_node, key_text); +} + +static void extract_kustomize(CBMExtractCtx *ctx) { TSNode root = ctx->root; uint32_t root_n = ts_node_child_count(root); for (uint32_t si = 0; si < root_n; si++) { @@ -103,50 +143,14 @@ static void extract_kustomize(CBMExtractCtx *ctx) { if (strcmp(ts_node_type(stream_child), "document") != 0) { continue; } - // Find block_mapping inside the document (may be wrapped in block_node) - TSNode mapping = ts_node_named_child(stream_child, 0); + TSNode mapping = unwrap_block_mapping(stream_child); if (ts_node_is_null(mapping)) { continue; } - // Some grammars wrap in block_node - if (strcmp(ts_node_type(mapping), "block_node") == 0) { - mapping = ts_node_named_child(mapping, 0); - } - if (ts_node_is_null(mapping) || strcmp(ts_node_type(mapping), "block_mapping") != 0) { - continue; - } uint32_t pair_n = ts_node_child_count(mapping); for (uint32_t pi = 0; pi < pair_n; pi++) { - TSNode pair = ts_node_child(mapping, pi); - if (strcmp(ts_node_type(pair), "block_mapping_pair") != 0) { - continue; - } - - // First named child = key - TSNode key_node = ts_node_named_child(pair, 0); - if (ts_node_is_null(key_node)) { - continue; - } - const char *key_text = get_scalar_text(a, key_node, ctx->source); - if (!key_text || !is_kustomize_list_key(key_text)) { - continue; - } - - // Second named child = value (should be a block_sequence or block_node wrapping one) - TSNode val_node = ts_node_named_child(pair, 1); - if (ts_node_is_null(val_node)) { - continue; - } - if (strcmp(ts_node_type(val_node), "block_node") == 0) { - val_node = ts_node_named_child(val_node, 0); - } - if (ts_node_is_null(val_node) || - strcmp(ts_node_type(val_node), "block_sequence") != 0) { - continue; - } - - emit_kustomize_sequence(ctx, val_node, key_text); + process_kustomize_pair(ctx, ts_node_child(mapping, pi)); } } } @@ -155,9 +159,51 @@ static void extract_kustomize(CBMExtractCtx *ctx) { // K8s manifest extraction // --------------------------------------------------------------------------- -// Descend into the first block_mapping of a document and extract apiVersion, -// kind, and metadata.name. Returns void; fills kind_buf and meta_name_buf. -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Extract the "name" scalar from a metadata block_mapping. +static void extract_metadata_name(CBMArena *a, TSNode meta_mapping, const char *source, + char *meta_name_buf, size_t meta_sz) { + if (ts_node_is_null(meta_mapping) || strcmp(ts_node_type(meta_mapping), "block_mapping") != 0) { + return; + } + uint32_t mn = ts_node_child_count(meta_mapping); + for (uint32_t mi = 0; mi < mn; mi++) { + TSNode mpair = ts_node_child(meta_mapping, mi); + if (strcmp(ts_node_type(mpair), "block_mapping_pair") != 0) { + continue; + } + TSNode mkey = ts_node_named_child(mpair, 0); + if (ts_node_is_null(mkey)) { + continue; + } + const char *mkey_text = get_scalar_text(a, mkey, source); + if (!mkey_text || strcmp(mkey_text, "name") != 0) { + continue; + } + TSNode mval = ts_node_named_child(mpair, 1); + if (ts_node_is_null(mval)) { + continue; + } + const char *meta_name = get_scalar_text(a, mval, source); + if (meta_name) { + snprintf(meta_name_buf, meta_sz, "%s", meta_name); + } + } +} + +// Unwrap a block_mapping_pair value through optional block_node. +static TSNode unwrap_pair_value(TSNode pair) { + TSNode val_node = ts_node_named_child(pair, 1); + if (ts_node_is_null(val_node)) { + return val_node; + } + if (strcmp(ts_node_type(val_node), "block_node") == 0) { + val_node = ts_node_named_child(val_node, 0); + } + return val_node; +} + +// Descend into the first block_mapping of a document and extract +// kind and metadata.name. Returns void; fills kind_buf and meta_name_buf. static void extract_k8s_scalars(CBMExtractCtx *ctx, TSNode mapping, char *kind_buf, size_t kind_sz, char *meta_name_buf, size_t meta_sz) { CBMArena *a = ctx->arena; @@ -179,14 +225,7 @@ static void extract_k8s_scalars(CBMExtractCtx *ctx, TSNode mapping, char *kind_b continue; } - TSNode val_node = ts_node_named_child(pair, 1); - if (ts_node_is_null(val_node)) { - continue; - } - // Unwrap block_node if present - if (strcmp(ts_node_type(val_node), "block_node") == 0) { - val_node = ts_node_named_child(val_node, 0); - } + TSNode val_node = unwrap_pair_value(pair); if (ts_node_is_null(val_node)) { continue; } @@ -197,36 +236,7 @@ static void extract_k8s_scalars(CBMExtractCtx *ctx, TSNode mapping, char *kind_b snprintf(kind_buf, kind_sz, "%s", v); } } else if (strcmp(key, "metadata") == 0) { - // Descend into metadata block_mapping to find "name" - // val_node is already unwrapped from block_node above. - TSNode meta_mapping = val_node; - if (ts_node_is_null(meta_mapping) || - strcmp(ts_node_type(meta_mapping), "block_mapping") != 0) { - continue; - } - uint32_t mn = ts_node_child_count(meta_mapping); - for (uint32_t mi = 0; mi < mn; mi++) { - TSNode mpair = ts_node_child(meta_mapping, mi); - if (strcmp(ts_node_type(mpair), "block_mapping_pair") != 0) { - continue; - } - TSNode mkey = ts_node_named_child(mpair, 0); - if (ts_node_is_null(mkey)) { - continue; - } - const char *mkey_text = get_scalar_text(a, mkey, ctx->source); - if (!mkey_text || strcmp(mkey_text, "name") != 0) { - continue; - } - TSNode mval = ts_node_named_child(mpair, 1); - if (ts_node_is_null(mval)) { - continue; - } - const char *meta_name = get_scalar_text(a, mval, ctx->source); - if (meta_name) { - snprintf(meta_name_buf, meta_sz, "%s", meta_name); - } - } + extract_metadata_name(a, val_node, ctx->source, meta_name_buf, meta_sz); } } } diff --git a/internal/cbm/extract_semantic.c b/internal/cbm/extract_semantic.c index f61886048..2dac60610 100644 --- a/internal/cbm/extract_semantic.c +++ b/internal/cbm/extract_semantic.c @@ -12,45 +12,77 @@ // --- Throw/Raise extraction --- -// Process a single node for throw extraction (called from iterative walker). -// NOLINTNEXTLINE(readability-function-cognitive-complexity) -static void process_throw_node(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) { - const char *kind = ts_node_type(node); - - if (cbm_kind_in_set(node, spec->throw_node_types)) { - char *exc_name = NULL; - uint32_t nc = ts_node_child_count(node); - for (uint32_t i = 0; i < nc; i++) { - TSNode child = ts_node_child(node, i); - const char *ck = ts_node_type(child); - if (strcmp(ck, "raise") == 0 || strcmp(ck, "throw") == 0) { - continue; - } - if (ck[0] == ';' || ck[0] == '(' || ck[0] == ')') { - continue; - } - if (strcmp(ck, "call") == 0 || strcmp(ck, "call_expression") == 0 || - strcmp(ck, "new_expression") == 0 || - strcmp(ck, "object_creation_expression") == 0 || - strcmp(ck, "instance_expression") == 0) { - TSNode fn = ts_node_child_by_field_name(child, "function", 8); - if (ts_node_is_null(fn)) { - fn = ts_node_child_by_field_name(child, "constructor", FIELD_LEN_CONSTRUCTOR); - } - if (ts_node_is_null(fn)) { - fn = ts_node_child_by_field_name(child, "type", 4); - } - if (ts_node_is_null(fn) && ts_node_named_child_count(child) > 0) { - fn = ts_node_named_child(child, 0); - } - if (!ts_node_is_null(fn)) { - exc_name = cbm_node_text(ctx->arena, fn, ctx->source); - } - } else { - exc_name = cbm_node_text(ctx->arena, child, ctx->source); - } - break; +// Resolve exception name from the first meaningful child of a throw/raise node. +static char *resolve_exception_name(CBMArena *a, TSNode throw_node, const char *source) { + uint32_t nc = ts_node_child_count(throw_node); + for (uint32_t i = 0; i < nc; i++) { + TSNode child = ts_node_child(throw_node, i); + const char *ck = ts_node_type(child); + if (strcmp(ck, "raise") == 0 || strcmp(ck, "throw") == 0) { + continue; } + if (ck[0] == ';' || ck[0] == '(' || ck[0] == ')') { + continue; + } + + if (strcmp(ck, "call") == 0 || strcmp(ck, "call_expression") == 0 || + strcmp(ck, "new_expression") == 0 || strcmp(ck, "object_creation_expression") == 0 || + strcmp(ck, "instance_expression") == 0) { + TSNode fn = ts_node_child_by_field_name(child, "function", 8); + if (ts_node_is_null(fn)) { + fn = ts_node_child_by_field_name(child, "constructor", FIELD_LEN_CONSTRUCTOR); + } + if (ts_node_is_null(fn)) { + fn = ts_node_child_by_field_name(child, "type", 4); + } + if (ts_node_is_null(fn) && ts_node_named_child_count(child) > 0) { + fn = ts_node_named_child(child, 0); + } + if (!ts_node_is_null(fn)) { + return cbm_node_text(a, fn, source); + } + } else { + return cbm_node_text(a, child, source); + } + break; + } + return NULL; +} + +// Extract exception types from a Java-style throws clause. +static void extract_throws_clause(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, + const char *func_qn) { + if (!spec->throws_clause_field || !spec->throws_clause_field[0]) { + return; + } + const char *kind = ts_node_type(node); + if (strcmp(kind, "method_declaration") != 0 && strcmp(kind, "constructor_declaration") != 0) { + return; + } + TSNode throws_clause = ts_node_child_by_field_name(node, spec->throws_clause_field, + (uint32_t)strlen(spec->throws_clause_field)); + if (ts_node_is_null(throws_clause)) { + return; + } + uint32_t nc = ts_node_child_count(throws_clause); + for (uint32_t i = 0; i < nc; i++) { + TSNode child = ts_node_child(throws_clause, i); + const char *ck = ts_node_type(child); + if (strcmp(ck, "type_identifier") == 0 || strcmp(ck, "identifier") == 0 || + strcmp(ck, "scoped_type_identifier") == 0) { + char *exc = cbm_node_text(ctx->arena, child, ctx->source); + if (exc && exc[0]) { + CBMThrow thr = {.exception_name = exc, .enclosing_func_qn = func_qn}; + cbm_throws_push(&ctx->result->throws, ctx->arena, thr); + } + } + } +} + +// Process a single node for throw extraction (called from iterative walker). +static void process_throw_node(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) { + if (cbm_kind_in_set(node, spec->throw_node_types)) { + char *exc_name = resolve_exception_name(ctx->arena, node, ctx->source); if (exc_name && exc_name[0]) { if (strlen(exc_name) > 100) { exc_name[100] = '\0'; @@ -62,29 +94,7 @@ static void process_throw_node(CBMExtractCtx *ctx, TSNode node, const CBMLangSpe } } - if (spec->throws_clause_field && spec->throws_clause_field[0]) { - if (strcmp(kind, "method_declaration") == 0 || - strcmp(kind, "constructor_declaration") == 0) { - TSNode throws_clause = ts_node_child_by_field_name( - node, spec->throws_clause_field, (uint32_t)strlen(spec->throws_clause_field)); - if (!ts_node_is_null(throws_clause)) { - uint32_t nc = ts_node_child_count(throws_clause); - for (uint32_t i = 0; i < nc; i++) { - TSNode child = ts_node_child(throws_clause, i); - const char *ck = ts_node_type(child); - if (strcmp(ck, "type_identifier") == 0 || strcmp(ck, "identifier") == 0 || - strcmp(ck, "scoped_type_identifier") == 0) { - char *exc = cbm_node_text(ctx->arena, child, ctx->source); - if (exc && exc[0]) { - const char *func_qn = cbm_enclosing_func_qn_cached(ctx, node); - CBMThrow thr = {.exception_name = exc, .enclosing_func_qn = func_qn}; - cbm_throws_push(&ctx->result->throws, ctx->arena, thr); - } - } - } - } - } - } + extract_throws_clause(ctx, node, spec, cbm_enclosing_func_qn_cached(ctx, node)); } // Iterative throw walker @@ -105,8 +115,32 @@ static void walk_throws(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec // --- Read/Write detection (iterative) --- +// Try to emit a write for an assignment node. +static void try_emit_assignment_write(CBMExtractCtx *ctx, TSNode node, const char *func_qn) { + TSNode left = ts_node_child_by_field_name(node, "left", 4); + if (ts_node_is_null(left)) { + if (ts_node_child_count(node) > 0) { + left = ts_node_child(node, 0); + } + } + if (ts_node_is_null(left)) { + return; + } + const char *lk = ts_node_type(left); + if (strcmp(lk, "identifier") != 0 && strcmp(lk, "simple_identifier") != 0) { + return; + } + char *name = cbm_node_text(ctx->arena, left, ctx->source); + if (name && name[0] && !cbm_is_keyword(name, ctx->language)) { + CBMReadWrite rw; + rw.var_name = name; + rw.is_write = true; + rw.enclosing_func_qn = func_qn; + cbm_rw_push(&ctx->result->rw, ctx->arena, rw); + } +} + #define READWRITE_STACK_CAP 512 -// NOLINTNEXTLINE(readability-function-cognitive-complexity) static void walk_readwrites(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec) { TSNode stack[READWRITE_STACK_CAP]; int top = 0; @@ -114,25 +148,7 @@ static void walk_readwrites(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec * while (top > 0) { TSNode node = stack[--top]; if (cbm_kind_in_set(node, spec->assignment_node_types)) { - TSNode left = ts_node_child_by_field_name(node, "left", 4); - if (ts_node_is_null(left)) { - if (ts_node_child_count(node) > 0) { - left = ts_node_child(node, 0); - } - } - if (!ts_node_is_null(left)) { - const char *lk = ts_node_type(left); - if (strcmp(lk, "identifier") == 0 || strcmp(lk, "simple_identifier") == 0) { - char *name = cbm_node_text(ctx->arena, left, ctx->source); - if (name && name[0] && !cbm_is_keyword(name, ctx->language)) { - CBMReadWrite rw; - rw.var_name = name; - rw.is_write = true; - rw.enclosing_func_qn = cbm_enclosing_func_qn_cached(ctx, node); - cbm_rw_push(&ctx->result->rw, ctx->arena, rw); - } - } - } + try_emit_assignment_write(ctx, node, cbm_enclosing_func_qn_cached(ctx, node)); } uint32_t count = ts_node_child_count(node); for (int i = (int)count - 1; i >= 0 && top < READWRITE_STACK_CAP; i--) { @@ -161,7 +177,6 @@ void cbm_extract_semantic(CBMExtractCtx *ctx) { // --- Unified handlers --- -// NOLINTNEXTLINE(readability-function-cognitive-complexity) void handle_throws(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state) { bool has_throws = spec->throw_node_types && spec->throw_node_types[0]; bool has_clause = spec->throws_clause_field && spec->throws_clause_field[0]; @@ -169,45 +184,8 @@ void handle_throws(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Wal return; } - const char *kind = ts_node_type(node); - if (has_throws && cbm_kind_in_set(node, spec->throw_node_types)) { - char *exc_name = NULL; - uint32_t nc = ts_node_child_count(node); - for (uint32_t i = 0; i < nc; i++) { - TSNode child = ts_node_child(node, i); - const char *ck = ts_node_type(child); - if (strcmp(ck, "raise") == 0 || strcmp(ck, "throw") == 0) { - continue; - } - if (ck[0] == ';' || ck[0] == '(' || ck[0] == ')') { - continue; - } - - if (strcmp(ck, "call") == 0 || strcmp(ck, "call_expression") == 0 || - strcmp(ck, "new_expression") == 0 || - strcmp(ck, "object_creation_expression") == 0 || - strcmp(ck, "instance_expression") == 0) { - TSNode fn = ts_node_child_by_field_name(child, "function", 8); - if (ts_node_is_null(fn)) { - fn = ts_node_child_by_field_name(child, "constructor", FIELD_LEN_CONSTRUCTOR); - } - if (ts_node_is_null(fn)) { - fn = ts_node_child_by_field_name(child, "type", 4); - } - if (ts_node_is_null(fn) && ts_node_named_child_count(child) > 0) { - fn = ts_node_named_child(child, 0); - } - if (!ts_node_is_null(fn)) { - exc_name = cbm_node_text(ctx->arena, fn, ctx->source); - } - } else { - // identifier, type_identifier, or any other expression - exc_name = cbm_node_text(ctx->arena, child, ctx->source); - } - break; - } - + char *exc_name = resolve_exception_name(ctx->arena, node, ctx->source); if (exc_name && exc_name[0]) { if (strlen(exc_name) > 100) { exc_name[100] = '\0'; @@ -219,30 +197,7 @@ void handle_throws(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Wal } } - // Java: throws clause on method/constructor declarations - if (has_clause) { - if (strcmp(kind, "method_declaration") == 0 || - strcmp(kind, "constructor_declaration") == 0) { - TSNode throws_clause = ts_node_child_by_field_name( - node, spec->throws_clause_field, (uint32_t)strlen(spec->throws_clause_field)); - if (!ts_node_is_null(throws_clause)) { - uint32_t nc = ts_node_child_count(throws_clause); - for (uint32_t i = 0; i < nc; i++) { - TSNode child = ts_node_child(throws_clause, i); - const char *ck = ts_node_type(child); - if (strcmp(ck, "type_identifier") == 0 || strcmp(ck, "identifier") == 0 || - strcmp(ck, "scoped_type_identifier") == 0) { - char *exc = cbm_node_text(ctx->arena, child, ctx->source); - if (exc && exc[0]) { - CBMThrow thr = {.exception_name = exc, - .enclosing_func_qn = state->enclosing_func_qn}; - cbm_throws_push(&ctx->result->throws, ctx->arena, thr); - } - } - } - } - } - } + extract_throws_clause(ctx, node, spec, state->enclosing_func_qn); } void handle_readwrites(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state) { diff --git a/internal/cbm/extract_type_assigns.c b/internal/cbm/extract_type_assigns.c index 8a3b808f7..cc189daae 100644 --- a/internal/cbm/extract_type_assigns.c +++ b/internal/cbm/extract_type_assigns.c @@ -8,41 +8,42 @@ #include #include +// Extract type from new_expression / object_creation_expression. +static const char *extract_new_expr_type(CBMArena *a, TSNode rhs, const char *source) { + TSNode type_node = ts_node_child_by_field_name(rhs, "type", 4); + if (!ts_node_is_null(type_node)) { + const char *tk = ts_node_type(type_node); + if (strcmp(tk, "type_identifier") == 0 || strcmp(tk, "identifier") == 0 || + strcmp(tk, "simple_identifier") == 0) { + return cbm_node_text(a, type_node, source); + } + if (strcmp(tk, "generic_type") == 0 && ts_node_child_count(type_node) > 0) { + return cbm_node_text(a, ts_node_child(type_node, 0), source); + } + return cbm_node_text(a, type_node, source); + } + // Fallback: first identifier child + for (uint32_t i = 0; i < ts_node_child_count(rhs); i++) { + TSNode child = ts_node_child(rhs, i); + const char *ck = ts_node_type(child); + if (strcmp(ck, "identifier") == 0 || strcmp(ck, "type_identifier") == 0 || + strcmp(ck, "simple_identifier") == 0) { + return cbm_node_text(a, child, source); + } + } + return NULL; +} + // Extract class/type name from a constructor expression. // e.g., new Foo() -> "Foo", Foo() -> "Foo" (if uppercase), Foo{} -> "Foo" -// NOLINTNEXTLINE(readability-function-cognitive-complexity) static const char *extract_constructor_type(CBMArena *a, TSNode rhs, const char *source, CBMLanguage lang) { const char *kind = ts_node_type(rhs); - // new_expression / object_creation_expression -> type field or first child if (strcmp(kind, "new_expression") == 0 || strcmp(kind, "object_creation_expression") == 0) { - TSNode type_node = ts_node_child_by_field_name(rhs, "type", 4); - if (!ts_node_is_null(type_node)) { - const char *tk = ts_node_type(type_node); - if (strcmp(tk, "type_identifier") == 0 || strcmp(tk, "identifier") == 0 || - strcmp(tk, "simple_identifier") == 0) { - return cbm_node_text(a, type_node, source); - } - // generic_type: get base - if (strcmp(tk, "generic_type") == 0 && ts_node_child_count(type_node) > 0) { - return cbm_node_text(a, ts_node_child(type_node, 0), source); - } - return cbm_node_text(a, type_node, source); - } - // Fallback: first named child - for (uint32_t i = 0; i < ts_node_child_count(rhs); i++) { - TSNode child = ts_node_child(rhs, i); - const char *ck = ts_node_type(child); - if (strcmp(ck, "identifier") == 0 || strcmp(ck, "type_identifier") == 0 || - strcmp(ck, "simple_identifier") == 0) { - return cbm_node_text(a, child, source); - } - } + return extract_new_expr_type(a, rhs, source); } - // call_expression where function is an uppercase identifier (Python/Kotlin/Scala class - // construction) if (strcmp(kind, "call") == 0 || strcmp(kind, "call_expression") == 0) { TSNode func = ts_node_child_by_field_name(rhs, "function", 8); if (ts_node_is_null(func) && ts_node_child_count(rhs) > 0) { @@ -56,7 +57,6 @@ static const char *extract_constructor_type(CBMArena *a, TSNode rhs, const char } } - // Go composite_literal: Type{...} if (strcmp(kind, "composite_literal") == 0) { TSNode type_node = ts_node_child_by_field_name(rhs, "type", 4); if (!ts_node_is_null(type_node)) { @@ -64,116 +64,108 @@ static const char *extract_constructor_type(CBMArena *a, TSNode rhs, const char } } - // Rust: Type::new() or Type { ... } - if (lang == CBM_LANG_RUST) { - if (strcmp(kind, "struct_expression") == 0) { - TSNode name = ts_node_child_by_field_name(rhs, "name", 4); - if (!ts_node_is_null(name)) { - return cbm_node_text(a, name, source); - } + if (lang == CBM_LANG_RUST && strcmp(kind, "struct_expression") == 0) { + TSNode name = ts_node_child_by_field_name(rhs, "name", 4); + if (!ts_node_is_null(name)) { + return cbm_node_text(a, name, source); } } return NULL; } -// Walk AST for assignment patterns where RHS is a constructor call. -// NOLINTNEXTLINE(readability-function-cognitive-complexity) -static void walk_type_assigns(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) { +// Emit a type assignment if var_name and constructor type are valid. +static void try_emit_type_assign(CBMExtractCtx *ctx, TSNode var_node, TSNode rhs_node, + const char *func_qn) { + char *var_name = cbm_node_text(ctx->arena, var_node, ctx->source); + const char *type_name = + extract_constructor_type(ctx->arena, rhs_node, ctx->source, ctx->language); + if (var_name && var_name[0] && type_name && type_name[0]) { + CBMTypeAssign ta; + ta.var_name = var_name; + ta.type_name = type_name; + ta.enclosing_func_qn = func_qn; + cbm_typeassign_push(&ctx->result->type_assigns, ctx->arena, ta); + } +} + +// Process assignment-type nodes (left/right fields with identifier check). +static void process_assignment_type_assign(CBMExtractCtx *ctx, TSNode node, const char *func_qn) { + TSNode left = ts_node_child_by_field_name(node, "left", 4); + TSNode right = ts_node_child_by_field_name(node, "right", 5); + if (ts_node_is_null(right)) { + right = ts_node_child_by_field_name(node, "value", 5); + } + if (!ts_node_is_null(left) && !ts_node_is_null(right)) { + const char *lk = ts_node_type(left); + if (strcmp(lk, "identifier") == 0 || strcmp(lk, "simple_identifier") == 0) { + try_emit_type_assign(ctx, left, right, func_qn); + } + } +} + +// Process Go short_var_declaration/var_spec nodes. +static void process_go_var_type_assign(CBMExtractCtx *ctx, TSNode node, const char *func_qn) { + TSNode left = ts_node_child_by_field_name(node, "name", 4); + if (ts_node_is_null(left)) { + left = ts_node_child_by_field_name(node, "left", 4); + } + TSNode right = ts_node_child_by_field_name(node, "value", 5); + if (ts_node_is_null(right)) { + right = ts_node_child_by_field_name(node, "right", 5); + } + if (!ts_node_is_null(left) && !ts_node_is_null(right)) { + try_emit_type_assign(ctx, left, right, func_qn); + } +} + +// Process JS/TS variable_declarator nodes (name + value with identifier check). +static void process_declarator_type_assign(CBMExtractCtx *ctx, TSNode node, const char *func_qn) { + TSNode name_node = ts_node_child_by_field_name(node, "name", 4); + TSNode value_node = ts_node_child_by_field_name(node, "value", 5); + if (!ts_node_is_null(name_node) && !ts_node_is_null(value_node)) { + const char *nk = ts_node_type(name_node); + if (strcmp(nk, "identifier") == 0 || strcmp(nk, "simple_identifier") == 0) { + try_emit_type_assign(ctx, name_node, value_node, func_qn); + } + } +} + +// Process Rust let_declaration nodes (pattern + value). +static void process_rust_let_type_assign(CBMExtractCtx *ctx, TSNode node, const char *func_qn) { + TSNode pat = ts_node_child_by_field_name(node, "pattern", 7); + TSNode val = ts_node_child_by_field_name(node, "value", 5); + if (!ts_node_is_null(pat) && !ts_node_is_null(val)) { + if (strcmp(ts_node_type(pat), "identifier") == 0) { + try_emit_type_assign(ctx, pat, val, func_qn); + } + } +} + +// Process assignment nodes (assignment, short_var_declaration, variable_declarator, +// let_declaration). +static void process_type_assign_node(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, + const char *func_qn) { const char *kind = ts_node_type(node); - // Assignment: var = Constructor() if (cbm_kind_in_set(node, spec->assignment_node_types)) { - TSNode left = ts_node_child_by_field_name(node, "left", 4); - TSNode right = ts_node_child_by_field_name(node, "right", 5); - if (ts_node_is_null(right)) { - right = ts_node_child_by_field_name(node, "value", 5); - } - if (!ts_node_is_null(left) && !ts_node_is_null(right)) { - const char *lk = ts_node_type(left); - if (strcmp(lk, "identifier") == 0 || strcmp(lk, "simple_identifier") == 0) { - char *var_name = cbm_node_text(ctx->arena, left, ctx->source); - const char *type_name = - extract_constructor_type(ctx->arena, right, ctx->source, ctx->language); - if (var_name && var_name[0] && type_name && type_name[0]) { - CBMTypeAssign ta; - ta.var_name = var_name; - ta.type_name = type_name; - ta.enclosing_func_qn = cbm_enclosing_func_qn_cached(ctx, node); - cbm_typeassign_push(&ctx->result->type_assigns, ctx->arena, ta); - } - } - } + process_assignment_type_assign(ctx, node, func_qn); } - - // Variable declarations with constructor RHS - // Go: short_var_declaration, var_spec - // JS/TS: variable_declarator (const x = new Foo()) - // Python: assignment already handled above - // Rust: let_declaration if (strcmp(kind, "short_var_declaration") == 0 || strcmp(kind, "var_spec") == 0) { - TSNode left = ts_node_child_by_field_name(node, "name", 4); - if (ts_node_is_null(left)) { - left = ts_node_child_by_field_name(node, "left", 4); - } - TSNode right = ts_node_child_by_field_name(node, "value", 5); - if (ts_node_is_null(right)) { - right = ts_node_child_by_field_name(node, "right", 5); - } - if (!ts_node_is_null(left) && !ts_node_is_null(right)) { - char *var_name = cbm_node_text(ctx->arena, left, ctx->source); - const char *type_name = - extract_constructor_type(ctx->arena, right, ctx->source, ctx->language); - if (var_name && var_name[0] && type_name && type_name[0]) { - CBMTypeAssign ta; - ta.var_name = var_name; - ta.type_name = type_name; - ta.enclosing_func_qn = cbm_enclosing_func_qn_cached(ctx, node); - cbm_typeassign_push(&ctx->result->type_assigns, ctx->arena, ta); - } - } + process_go_var_type_assign(ctx, node, func_qn); } - if (strcmp(kind, "variable_declarator") == 0) { - TSNode name_node = ts_node_child_by_field_name(node, "name", 4); - TSNode value_node = ts_node_child_by_field_name(node, "value", 5); - if (!ts_node_is_null(name_node) && !ts_node_is_null(value_node)) { - const char *nk = ts_node_type(name_node); - if (strcmp(nk, "identifier") == 0 || strcmp(nk, "simple_identifier") == 0) { - char *var_name = cbm_node_text(ctx->arena, name_node, ctx->source); - const char *type_name = - extract_constructor_type(ctx->arena, value_node, ctx->source, ctx->language); - if (var_name && var_name[0] && type_name && type_name[0]) { - CBMTypeAssign ta; - ta.var_name = var_name; - ta.type_name = type_name; - ta.enclosing_func_qn = cbm_enclosing_func_qn_cached(ctx, node); - cbm_typeassign_push(&ctx->result->type_assigns, ctx->arena, ta); - } - } - } + process_declarator_type_assign(ctx, node, func_qn); } - if (strcmp(kind, "let_declaration") == 0 && ctx->language == CBM_LANG_RUST) { - TSNode pat = ts_node_child_by_field_name(node, "pattern", 7); - TSNode val = ts_node_child_by_field_name(node, "value", 5); - if (!ts_node_is_null(pat) && !ts_node_is_null(val)) { - if (strcmp(ts_node_type(pat), "identifier") == 0) { - char *var_name = cbm_node_text(ctx->arena, pat, ctx->source); - const char *type_name = - extract_constructor_type(ctx->arena, val, ctx->source, ctx->language); - if (var_name && var_name[0] && type_name && type_name[0]) { - CBMTypeAssign ta; - ta.var_name = var_name; - ta.type_name = type_name; - ta.enclosing_func_qn = cbm_enclosing_func_qn_cached(ctx, node); - cbm_typeassign_push(&ctx->result->type_assigns, ctx->arena, ta); - } - } - } + process_rust_let_type_assign(ctx, node, func_qn); } +} + +// Walk AST for assignment patterns where RHS is a constructor call. +static void walk_type_assigns(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) { + process_type_assign_node(ctx, node, spec, cbm_enclosing_func_qn_cached(ctx, node)); - // Recurse uint32_t count = ts_node_child_count(node); for (uint32_t i = 0; i < count; i++) { walk_type_assigns(ctx, ts_node_child(node, i), spec); @@ -191,97 +183,7 @@ void cbm_extract_type_assigns(CBMExtractCtx *ctx) { // --- Unified handler --- -// NOLINTNEXTLINE(readability-function-cognitive-complexity) void handle_type_assigns(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state) { - const char *kind = ts_node_type(node); - - // Assignment: var = Constructor() - if (spec->assignment_node_types && cbm_kind_in_set(node, spec->assignment_node_types)) { - TSNode left = ts_node_child_by_field_name(node, "left", 4); - TSNode right = ts_node_child_by_field_name(node, "right", 5); - if (ts_node_is_null(right)) { - right = ts_node_child_by_field_name(node, "value", 5); - } - if (!ts_node_is_null(left) && !ts_node_is_null(right)) { - const char *lk = ts_node_type(left); - if (strcmp(lk, "identifier") == 0 || strcmp(lk, "simple_identifier") == 0) { - char *var_name = cbm_node_text(ctx->arena, left, ctx->source); - const char *type_name = - extract_constructor_type(ctx->arena, right, ctx->source, ctx->language); - if (var_name && var_name[0] && type_name && type_name[0]) { - CBMTypeAssign ta; - ta.var_name = var_name; - ta.type_name = type_name; - ta.enclosing_func_qn = state->enclosing_func_qn; - cbm_typeassign_push(&ctx->result->type_assigns, ctx->arena, ta); - } - } - } - } - - // Go: short_var_declaration, var_spec - if (strcmp(kind, "short_var_declaration") == 0 || strcmp(kind, "var_spec") == 0) { - TSNode left = ts_node_child_by_field_name(node, "name", 4); - if (ts_node_is_null(left)) { - left = ts_node_child_by_field_name(node, "left", 4); - } - TSNode right = ts_node_child_by_field_name(node, "value", 5); - if (ts_node_is_null(right)) { - right = ts_node_child_by_field_name(node, "right", 5); - } - if (!ts_node_is_null(left) && !ts_node_is_null(right)) { - char *var_name = cbm_node_text(ctx->arena, left, ctx->source); - const char *type_name = - extract_constructor_type(ctx->arena, right, ctx->source, ctx->language); - if (var_name && var_name[0] && type_name && type_name[0]) { - CBMTypeAssign ta; - ta.var_name = var_name; - ta.type_name = type_name; - ta.enclosing_func_qn = state->enclosing_func_qn; - cbm_typeassign_push(&ctx->result->type_assigns, ctx->arena, ta); - } - } - } - - // JS/TS: variable_declarator - if (strcmp(kind, "variable_declarator") == 0) { - TSNode name_node = ts_node_child_by_field_name(node, "name", 4); - TSNode value_node = ts_node_child_by_field_name(node, "value", 5); - if (!ts_node_is_null(name_node) && !ts_node_is_null(value_node)) { - const char *nk = ts_node_type(name_node); - if (strcmp(nk, "identifier") == 0 || strcmp(nk, "simple_identifier") == 0) { - char *var_name = cbm_node_text(ctx->arena, name_node, ctx->source); - const char *type_name = - extract_constructor_type(ctx->arena, value_node, ctx->source, ctx->language); - if (var_name && var_name[0] && type_name && type_name[0]) { - CBMTypeAssign ta; - ta.var_name = var_name; - ta.type_name = type_name; - ta.enclosing_func_qn = state->enclosing_func_qn; - cbm_typeassign_push(&ctx->result->type_assigns, ctx->arena, ta); - } - } - } - } - - // Rust: let_declaration - if (strcmp(kind, "let_declaration") == 0 && ctx->language == CBM_LANG_RUST) { - TSNode pat = ts_node_child_by_field_name(node, "pattern", 7); - TSNode val = ts_node_child_by_field_name(node, "value", 5); - if (!ts_node_is_null(pat) && !ts_node_is_null(val)) { - if (strcmp(ts_node_type(pat), "identifier") == 0) { - char *var_name = cbm_node_text(ctx->arena, pat, ctx->source); - const char *type_name = - extract_constructor_type(ctx->arena, val, ctx->source, ctx->language); - if (var_name && var_name[0] && type_name && type_name[0]) { - CBMTypeAssign ta; - ta.var_name = var_name; - ta.type_name = type_name; - ta.enclosing_func_qn = state->enclosing_func_qn; - cbm_typeassign_push(&ctx->result->type_assigns, ctx->arena, ta); - } - } - } - } + process_type_assign_node(ctx, node, spec, state->enclosing_func_qn); } diff --git a/internal/cbm/extract_type_refs.c b/internal/cbm/extract_type_refs.c index 8d89bbec7..36fda1cc1 100644 --- a/internal/cbm/extract_type_refs.c +++ b/internal/cbm/extract_type_refs.c @@ -141,9 +141,66 @@ static void extract_return_type_refs(CBMExtractCtx *ctx, TSNode func_node, const } } -// Walk function body for type references (casts, type assertions, local var types, generics). -// NOLINTNEXTLINE(readability-function-cognitive-complexity) -static void walk_body_type_refs(CBMExtractCtx *ctx, TSNode node, const char *func_qn) { +// Extract type ref from a node whose "type" field contains the type. +static void extract_type_field_ref(CBMExtractCtx *ctx, TSNode node, const char *func_qn) { + TSNode type_node = ts_node_child_by_field_name(node, "type", 4); + if (!ts_node_is_null(type_node)) { + add_type_ref(ctx, extract_type_text(ctx->arena, type_node, ctx->source), func_qn); + } +} + +// Extract type refs from TS/JS type_arguments or type_annotation children. +static void extract_ts_body_type_refs(CBMExtractCtx *ctx, TSNode node, const char *kind, + const char *func_qn) { + if (strcmp(kind, "as_expression") == 0 || strcmp(kind, "satisfies_expression") == 0) { + extract_type_field_ref(ctx, node, func_qn); + } else if (strcmp(kind, "type_arguments") == 0) { + uint32_t nc = ts_node_child_count(node); + for (uint32_t i = 0; i < nc; i++) { + TSNode child = ts_node_child(node, i); + if (strcmp(ts_node_type(child), "type_identifier") == 0 || + strcmp(ts_node_type(child), "identifier") == 0) { + add_type_ref(ctx, cbm_node_text(ctx->arena, child, ctx->source), func_qn); + } + } + } else if (strcmp(kind, "variable_declarator") == 0) { + uint32_t nc = ts_node_child_count(node); + for (uint32_t i = 0; i < nc; i++) { + TSNode child = ts_node_child(node, i); + if (strcmp(ts_node_type(child), "type_annotation") == 0) { + if (ts_node_child_count(child) > 0) { + TSNode inner = ts_node_child(child, ts_node_child_count(child) - 1); + add_type_ref(ctx, extract_type_text(ctx->arena, inner, ctx->source), func_qn); + } + } + } + } +} + +// Extract type refs from Java generic_type children. +static void extract_java_body_type_refs(CBMExtractCtx *ctx, TSNode node, const char *kind, + const char *func_qn) { + if (strcmp(kind, "local_variable_declaration") == 0 || strcmp(kind, "cast_expression") == 0) { + extract_type_field_ref(ctx, node, func_qn); + } else if (strcmp(kind, "generic_type") == 0) { + uint32_t nc = ts_node_child_count(node); + for (uint32_t i = 0; i < nc; i++) { + TSNode child = ts_node_child(node, i); + if (strcmp(ts_node_type(child), "type_arguments") == 0) { + uint32_t nta = ts_node_child_count(child); + for (uint32_t j = 0; j < nta; j++) { + TSNode ta = ts_node_child(child, j); + if (strcmp(ts_node_type(ta), "type_identifier") == 0) { + add_type_ref(ctx, cbm_node_text(ctx->arena, ta, ctx->source), func_qn); + } + } + } + } + } +} + +// Process a single node for body-level type references. +static void process_body_type_ref(CBMExtractCtx *ctx, TSNode node, const char *func_qn) { const char *kind = ts_node_type(node); switch (ctx->language) { @@ -151,66 +208,15 @@ static void walk_body_type_refs(CBMExtractCtx *ctx, TSNode node, const char *fun if (strcmp(kind, "var_spec") == 0 || strcmp(kind, "type_assertion") == 0 || strcmp(kind, "type_conversion_expression") == 0 || strcmp(kind, "composite_literal") == 0) { - TSNode type_node = ts_node_child_by_field_name(node, "type", 4); - if (!ts_node_is_null(type_node)) { - add_type_ref(ctx, extract_type_text(ctx->arena, type_node, ctx->source), func_qn); - } + extract_type_field_ref(ctx, node, func_qn); } break; case CBM_LANG_TYPESCRIPT: case CBM_LANG_TSX: - if (strcmp(kind, "as_expression") == 0 || strcmp(kind, "satisfies_expression") == 0) { - TSNode type_node = ts_node_child_by_field_name(node, "type", 4); - if (!ts_node_is_null(type_node)) { - add_type_ref(ctx, extract_type_text(ctx->arena, type_node, ctx->source), func_qn); - } - } else if (strcmp(kind, "type_arguments") == 0) { - uint32_t nc = ts_node_child_count(node); - for (uint32_t i = 0; i < nc; i++) { - TSNode child = ts_node_child(node, i); - if (strcmp(ts_node_type(child), "type_identifier") == 0 || - strcmp(ts_node_type(child), "identifier") == 0) { - add_type_ref(ctx, cbm_node_text(ctx->arena, child, ctx->source), func_qn); - } - } - } else if (strcmp(kind, "variable_declarator") == 0) { - // Check for type_annotation child - uint32_t nc = ts_node_child_count(node); - for (uint32_t i = 0; i < nc; i++) { - TSNode child = ts_node_child(node, i); - if (strcmp(ts_node_type(child), "type_annotation") == 0) { - if (ts_node_child_count(child) > 0) { - TSNode inner = ts_node_child(child, ts_node_child_count(child) - 1); - add_type_ref(ctx, extract_type_text(ctx->arena, inner, ctx->source), - func_qn); - } - } - } - } + extract_ts_body_type_refs(ctx, node, kind, func_qn); break; case CBM_LANG_JAVA: - if (strcmp(kind, "local_variable_declaration") == 0 || - strcmp(kind, "cast_expression") == 0) { - TSNode type_node = ts_node_child_by_field_name(node, "type", 4); - if (!ts_node_is_null(type_node)) { - add_type_ref(ctx, extract_type_text(ctx->arena, type_node, ctx->source), func_qn); - } - } else if (strcmp(kind, "generic_type") == 0) { - // Extract type arguments - uint32_t nc = ts_node_child_count(node); - for (uint32_t i = 0; i < nc; i++) { - TSNode child = ts_node_child(node, i); - if (strcmp(ts_node_type(child), "type_arguments") == 0) { - uint32_t nta = ts_node_child_count(child); - for (uint32_t j = 0; j < nta; j++) { - TSNode ta = ts_node_child(child, j); - if (strcmp(ts_node_type(ta), "type_identifier") == 0) { - add_type_ref(ctx, cbm_node_text(ctx->arena, ta, ctx->source), func_qn); - } - } - } - } - } + extract_java_body_type_refs(ctx, node, kind, func_qn); break; case CBM_LANG_PYTHON: if (strcmp(kind, "assignment") == 0) { @@ -222,17 +228,18 @@ static void walk_body_type_refs(CBMExtractCtx *ctx, TSNode node, const char *fun break; case CBM_LANG_RUST: if (strcmp(kind, "let_declaration") == 0 || strcmp(kind, "type_cast_expression") == 0) { - TSNode type_node = ts_node_child_by_field_name(node, "type", 4); - if (!ts_node_is_null(type_node)) { - add_type_ref(ctx, extract_type_text(ctx->arena, type_node, ctx->source), func_qn); - } + extract_type_field_ref(ctx, node, func_qn); } break; default: break; } +} + +// Walk function body for type references (casts, type assertions, local var types, generics). +static void walk_body_type_refs(CBMExtractCtx *ctx, TSNode node, const char *func_qn) { + process_body_type_ref(ctx, node, func_qn); - // Recurse uint32_t count = ts_node_child_count(node); for (uint32_t i = 0; i < count; i++) { walk_body_type_refs(ctx, ts_node_child(node, i), func_qn); @@ -297,123 +304,40 @@ void cbm_extract_type_refs(CBMExtractCtx *ctx) { // The cursor visits both, so this single handler replaces the old // walk_type_refs + walk_body_type_refs split. -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Extract signature type refs from a function node. +static void extract_signature_type_refs(CBMExtractCtx *ctx, TSNode node, WalkState *state) { + TSNode name_node = ts_node_child_by_field_name(node, "name", 4); + if (ts_node_is_null(name_node)) { + return; + } + char *func_name = cbm_node_text(ctx->arena, name_node, ctx->source); + if (!func_name || !func_name[0]) { + return; + } + + const char *func_qn; + if (state->enclosing_class_qn) { + func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", state->enclosing_class_qn, func_name); + } else { + func_qn = cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, func_name); + } + + TSNode params = ts_node_child_by_field_name(node, "parameters", 10); + if (!ts_node_is_null(params)) { + extract_param_type_refs(ctx, params, func_qn); + } + extract_return_type_refs(ctx, node, func_qn); +} + void handle_type_refs(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state) { if (!spec->function_node_types || !spec->function_node_types[0]) { return; } - // Function signature: extract param and return type refs if (cbm_kind_in_set(node, spec->function_node_types)) { - TSNode name_node = ts_node_child_by_field_name(node, "name", 4); - if (ts_node_is_null(name_node)) { - return; - } - char *func_name = cbm_node_text(ctx->arena, name_node, ctx->source); - if (!func_name || !func_name[0]) { - return; - } - - const char *func_qn; - if (state->enclosing_class_qn) { - func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", state->enclosing_class_qn, func_name); - } else { - func_qn = cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, func_name); - } - - TSNode params = ts_node_child_by_field_name(node, "parameters", 10); - if (!ts_node_is_null(params)) { - extract_param_type_refs(ctx, params, func_qn); - } - extract_return_type_refs(ctx, node, func_qn); + extract_signature_type_refs(ctx, node, state); return; } - // Body type refs: use enclosing_func_qn from state - const char *kind = ts_node_type(node); - const char *func_qn = state->enclosing_func_qn; - - switch (ctx->language) { - case CBM_LANG_GO: - if (strcmp(kind, "var_spec") == 0 || strcmp(kind, "type_assertion") == 0 || - strcmp(kind, "type_conversion_expression") == 0 || - strcmp(kind, "composite_literal") == 0) { - TSNode type_node = ts_node_child_by_field_name(node, "type", 4); - if (!ts_node_is_null(type_node)) { - add_type_ref(ctx, extract_type_text(ctx->arena, type_node, ctx->source), func_qn); - } - } - break; - case CBM_LANG_TYPESCRIPT: - case CBM_LANG_TSX: - if (strcmp(kind, "as_expression") == 0 || strcmp(kind, "satisfies_expression") == 0) { - TSNode type_node = ts_node_child_by_field_name(node, "type", 4); - if (!ts_node_is_null(type_node)) { - add_type_ref(ctx, extract_type_text(ctx->arena, type_node, ctx->source), func_qn); - } - } else if (strcmp(kind, "type_arguments") == 0) { - uint32_t nc = ts_node_child_count(node); - for (uint32_t i = 0; i < nc; i++) { - TSNode child = ts_node_child(node, i); - if (strcmp(ts_node_type(child), "type_identifier") == 0 || - strcmp(ts_node_type(child), "identifier") == 0) { - add_type_ref(ctx, cbm_node_text(ctx->arena, child, ctx->source), func_qn); - } - } - } else if (strcmp(kind, "variable_declarator") == 0) { - uint32_t nc = ts_node_child_count(node); - for (uint32_t i = 0; i < nc; i++) { - TSNode child = ts_node_child(node, i); - if (strcmp(ts_node_type(child), "type_annotation") == 0) { - if (ts_node_child_count(child) > 0) { - TSNode inner = ts_node_child(child, ts_node_child_count(child) - 1); - add_type_ref(ctx, extract_type_text(ctx->arena, inner, ctx->source), - func_qn); - } - } - } - } - break; - case CBM_LANG_JAVA: - if (strcmp(kind, "local_variable_declaration") == 0 || - strcmp(kind, "cast_expression") == 0) { - TSNode type_node = ts_node_child_by_field_name(node, "type", 4); - if (!ts_node_is_null(type_node)) { - add_type_ref(ctx, extract_type_text(ctx->arena, type_node, ctx->source), func_qn); - } - } else if (strcmp(kind, "generic_type") == 0) { - uint32_t nc = ts_node_child_count(node); - for (uint32_t i = 0; i < nc; i++) { - TSNode child = ts_node_child(node, i); - if (strcmp(ts_node_type(child), "type_arguments") == 0) { - uint32_t nta = ts_node_child_count(child); - for (uint32_t j = 0; j < nta; j++) { - TSNode ta = ts_node_child(child, j); - if (strcmp(ts_node_type(ta), "type_identifier") == 0) { - add_type_ref(ctx, cbm_node_text(ctx->arena, ta, ctx->source), func_qn); - } - } - } - } - } - break; - case CBM_LANG_PYTHON: - if (strcmp(kind, "assignment") == 0) { - TSNode type_node = ts_node_child_by_field_name(node, "type", 4); - if (!ts_node_is_null(type_node)) { - add_type_ref(ctx, cbm_node_text(ctx->arena, type_node, ctx->source), func_qn); - } - } - break; - case CBM_LANG_RUST: - if (strcmp(kind, "let_declaration") == 0 || strcmp(kind, "type_cast_expression") == 0) { - TSNode type_node = ts_node_child_by_field_name(node, "type", 4); - if (!ts_node_is_null(type_node)) { - add_type_ref(ctx, extract_type_text(ctx->arena, type_node, ctx->source), func_qn); - } - } - break; - default: - break; - } + process_body_type_ref(ctx, node, state->enclosing_func_qn); } diff --git a/internal/cbm/extract_unified.c b/internal/cbm/extract_unified.c index 69c6f070c..c46de5c49 100644 --- a/internal/cbm/extract_unified.c +++ b/internal/cbm/extract_unified.c @@ -53,42 +53,49 @@ static void recompute_state(WalkState *state, const char *module_qn) { } } -// Compute function QN for scope tracking (mirrors cbm_enclosing_func_qn logic). -// NOLINTNEXTLINE(readability-function-cognitive-complexity) -static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, - WalkState *state) { - (void)spec; - // Wolfram: set_delayed_top/set_top/set_delayed/set — LHS is apply(user_symbol("f"), ...) - if (ctx->language == CBM_LANG_WOLFRAM) { - const char *nk = ts_node_type(node); - if (strcmp(nk, "set_delayed_top") == 0 || strcmp(nk, "set_top") == 0 || - strcmp(nk, "set_delayed") == 0 || strcmp(nk, "set") == 0) { - if (ts_node_named_child_count(node) > 0) { - TSNode lhs = ts_node_named_child(node, 0); - if (strcmp(ts_node_type(lhs), "apply") == 0 && ts_node_named_child_count(lhs) > 0) { - TSNode head = ts_node_named_child(lhs, 0); - if (strcmp(ts_node_type(head), "user_symbol") == 0) { - char *name = cbm_node_text(ctx->arena, head, ctx->source); - if (name && name[0]) { - return cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, name); - } - } +// Try to resolve Wolfram function QN from set_delayed_top/set_top/set_delayed/set LHS. +static const char *compute_wolfram_func_qn(CBMExtractCtx *ctx, TSNode node) { + const char *nk = ts_node_type(node); + if (strcmp(nk, "set_delayed_top") != 0 && strcmp(nk, "set_top") != 0 && + strcmp(nk, "set_delayed") != 0 && strcmp(nk, "set") != 0) { + return NULL; // not a Wolfram set node — signal caller to continue + } + if (ts_node_named_child_count(node) > 0) { + TSNode lhs = ts_node_named_child(node, 0); + if (strcmp(ts_node_type(lhs), "apply") == 0 && ts_node_named_child_count(lhs) > 0) { + TSNode head = ts_node_named_child(lhs, 0); + if (strcmp(ts_node_type(head), "user_symbol") == 0) { + char *name = cbm_node_text(ctx->arena, head, ctx->source); + if (name && name[0]) { + return cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, name); } } - return NULL; } } + return NULL; +} +// Resolve the name node for a function, handling arrow functions. +static TSNode resolve_func_name_node(TSNode node) { TSNode name_node = ts_node_child_by_field_name(node, "name", 4); - - // Arrow function: name from parent variable_declarator if (ts_node_is_null(name_node) && strcmp(ts_node_type(node), "arrow_function") == 0) { TSNode parent = ts_node_parent(node); if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "variable_declarator") == 0) { name_node = ts_node_child_by_field_name(parent, "name", 4); } } + return name_node; +} +// Compute function QN for scope tracking (mirrors cbm_enclosing_func_qn logic). +static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, + WalkState *state) { + (void)spec; + if (ctx->language == CBM_LANG_WOLFRAM) { + return compute_wolfram_func_qn(ctx, node); + } + + TSNode name_node = resolve_func_name_node(node); if (ts_node_is_null(name_node)) { return NULL; } @@ -252,18 +259,50 @@ static void handle_string_refs(CBMExtractCtx *ctx, TSNode node, const WalkState /* Recursively walk YAML block_mapping_pair nodes, building dotted key paths. * Emits string_refs with key_path for leaf values that are URLs or config values. * Example: body.operational_info.post_url → "https://..." */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Classify and emit a YAML leaf value as a string_ref with key_path. +static void emit_yaml_leaf_value(CBMExtractCtx *ctx, TSNode val, const char *path) { + char *val_text = cbm_node_text(ctx->arena, val, ctx->source); + if (!val_text || !val_text[0]) { + return; + } + + int vlen = (int)strlen(val_text); + const char *content = val_text; + if (vlen >= 2 && (val_text[0] == '"' || val_text[0] == '\'')) { + content = val_text + 1; + vlen -= 2; + if (vlen <= 0) { + return; + } + } + + int kind_val = cbm_classify_string(content, vlen); + if (kind_val < 0) { + return; + } + + char *stored = cbm_arena_strndup(ctx->arena, content, (size_t)vlen); + if (!stored) { + return; + } + + CBMStringRef ref = { + .value = stored, + .enclosing_func_qn = ctx->module_qn, + .key_path = path, + .kind = (CBMStringRefKind)kind_val, + }; + cbm_stringref_push(&ctx->result->string_refs, ctx->arena, ref); +} + static void walk_yaml_mapping(CBMExtractCtx *ctx, TSNode node, const char *prefix) { uint32_t nc = ts_node_named_child_count(node); for (uint32_t i = 0; i < nc; i++) { TSNode child = ts_node_named_child(node, i); - const char *ck = ts_node_type(child); - - if (strcmp(ck, "block_mapping_pair") != 0) { + if (strcmp(ts_node_type(child), "block_mapping_pair") != 0) { continue; } - /* Get key */ TSNode key = ts_node_child_by_field_name(child, "key", 3); if (ts_node_is_null(key)) { continue; @@ -273,20 +312,16 @@ static void walk_yaml_mapping(CBMExtractCtx *ctx, TSNode node, const char *prefi continue; } - /* Build dotted path */ const char *path = prefix ? cbm_arena_sprintf(ctx->arena, "%s.%s", prefix, key_text) : key_text; - /* Get value node */ TSNode val = ts_node_child_by_field_name(child, "value", 5); if (ts_node_is_null(val)) { continue; } const char *vk = ts_node_type(val); - /* If value is a nested mapping (block_node → block_mapping), recurse */ if (strcmp(vk, "block_node") == 0 || strcmp(vk, "block_mapping") == 0) { - /* Walk children for nested block_mapping */ uint32_t vnc = ts_node_named_child_count(val); for (uint32_t vi = 0; vi < vnc; vi++) { TSNode vc = ts_node_named_child(val, vi); @@ -299,40 +334,7 @@ static void walk_yaml_mapping(CBMExtractCtx *ctx, TSNode node, const char *prefi continue; } - /* Leaf value: extract and classify */ - char *val_text = cbm_node_text(ctx->arena, val, ctx->source); - if (!val_text || !val_text[0]) { - continue; - } - - /* Strip quotes */ - int vlen = (int)strlen(val_text); - const char *content = val_text; - if (vlen >= 2 && (val_text[0] == '"' || val_text[0] == '\'')) { - content = val_text + 1; - vlen -= 2; - if (vlen <= 0) { - continue; - } - } - - int kind_val = cbm_classify_string(content, vlen); - if (kind_val < 0) { - continue; - } - - char *stored = cbm_arena_strndup(ctx->arena, content, (size_t)vlen); - if (!stored) { - continue; - } - - CBMStringRef ref = { - .value = stored, - .enclosing_func_qn = ctx->module_qn, - .key_path = path, - .kind = (CBMStringRefKind)kind_val, - }; - cbm_stringref_push(&ctx->result->string_refs, ctx->arena, ref); + emit_yaml_leaf_value(ctx, val, path); } } @@ -388,7 +390,68 @@ static const char *infer_broker(const char *file_path, const char *source_key) { /* Scan a YAML mapping for source+target key pairs. * Collects all key-value pairs at this level and one level deep (for nested config:). */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Strip quotes from a YAML scalar value. +static char *strip_yaml_quotes(CBMArena *a, char *v) { + if (!v || !v[0]) { + return v; + } + int vlen = (int)strlen(v); + if (vlen >= 2 && (v[0] == '"' || v[0] == '\'')) { + return cbm_arena_strndup(a, v + 1, (size_t)(vlen - 2)); + } + return v; +} + +// Scan a nested YAML block_mapping for target keys (push_endpoint, uri, etc.). +static void scan_nested_mapping_targets(CBMExtractCtx *ctx, TSNode val, const char **targets, + int *n_targets) { + uint32_t vnc = ts_node_named_child_count(val); + for (uint32_t vi = 0; vi < vnc; vi++) { + TSNode vc = ts_node_named_child(val, vi); + if (strcmp(ts_node_type(vc), "block_mapping") != 0) { + continue; + } + uint32_t mnc = ts_node_named_child_count(vc); + for (uint32_t mi = 0; mi < mnc; mi++) { + TSNode mp = ts_node_named_child(vc, mi); + if (strcmp(ts_node_type(mp), "block_mapping_pair") != 0) { + continue; + } + TSNode mk = ts_node_child_by_field_name(mp, "key", 3); + TSNode mv = ts_node_child_by_field_name(mp, "value", 5); + if (ts_node_is_null(mk) || ts_node_is_null(mv)) { + continue; + } + char *mktext = cbm_node_text(ctx->arena, mk, ctx->source); + if (mktext && is_target_key(mktext) && *n_targets < 8) { + char *mvtext = + strip_yaml_quotes(ctx->arena, cbm_node_text(ctx->arena, mv, ctx->source)); + if (mvtext && strstr(mvtext, "://")) { + targets[(*n_targets)++] = mvtext; + } + } + } + } +} + +// Emit infra bindings for each source × target pair combination. +static void emit_infra_bindings(CBMExtractCtx *ctx, const char **sources, const char **source_keys, + int n_sources, const char **targets, int n_targets) { + for (int si = 0; si < n_sources; si++) { + for (int ti = 0; ti < n_targets; ti++) { + if (!sources[si] || !targets[ti]) { + continue; + } + CBMInfraBinding ib = { + .source_name = sources[si], + .target_url = targets[ti], + .broker = infer_broker(ctx->rel_path, source_keys[si]), + }; + cbm_infrabinding_push(&ctx->result->infra_bindings, ctx->arena, ib); + } + } +} + static void scan_mapping_for_bindings(CBMExtractCtx *ctx, TSNode mapping) { const char *sources[8] = {NULL}; const char *source_keys[8] = {NULL}; @@ -412,78 +475,23 @@ static void scan_mapping_for_bindings(CBMExtractCtx *ctx, TSNode mapping) { continue; } - /* Check if this is a source or target key with a scalar value */ const char *vtype = ts_node_type(val); if (strcmp(vtype, "block_node") != 0 && strcmp(vtype, "block_mapping") != 0) { - char *v = cbm_node_text(ctx->arena, val, ctx->source); - if (v && v[0]) { - /* Strip quotes */ - int vlen = (int)strlen(v); - if (vlen >= 2 && (v[0] == '"' || v[0] == '\'')) { - v = cbm_arena_strndup(ctx->arena, v + 1, (size_t)(vlen - 2)); - } - if (is_source_key(k) && n_sources < 8) { - sources[n_sources] = v; - source_keys[n_sources] = k; - n_sources++; - } - if (is_target_key(k) && n_targets < 8 && v && strstr(v, "://")) { - targets[n_targets++] = v; - } + char *v = strip_yaml_quotes(ctx->arena, cbm_node_text(ctx->arena, val, ctx->source)); + if (is_source_key(k) && n_sources < 8) { + sources[n_sources] = v; + source_keys[n_sources] = k; + n_sources++; + } + if (is_target_key(k) && n_targets < 8 && v && strstr(v, "://")) { + targets[n_targets++] = v; } } else { - /* Nested mapping (e.g., config: {push_endpoint: URL}) — scan one level */ - uint32_t vnc = ts_node_named_child_count(val); - for (uint32_t vi = 0; vi < vnc; vi++) { - TSNode vc = ts_node_named_child(val, vi); - const char *vck = ts_node_type(vc); - if (strcmp(vck, "block_mapping") == 0) { - /* Scan nested mapping for target keys */ - uint32_t mnc = ts_node_named_child_count(vc); - for (uint32_t mi = 0; mi < mnc; mi++) { - TSNode mp = ts_node_named_child(vc, mi); - if (strcmp(ts_node_type(mp), "block_mapping_pair") != 0) { - continue; - } - TSNode mk = ts_node_child_by_field_name(mp, "key", 3); - TSNode mv = ts_node_child_by_field_name(mp, "value", 5); - if (ts_node_is_null(mk) || ts_node_is_null(mv)) { - continue; - } - char *mktext = cbm_node_text(ctx->arena, mk, ctx->source); - if (mktext && is_target_key(mktext) && n_targets < 8) { - char *mvtext = cbm_node_text(ctx->arena, mv, ctx->source); - if (mvtext && mvtext[0]) { - int mvlen = (int)strlen(mvtext); - if (mvlen >= 2 && (mvtext[0] == '"' || mvtext[0] == '\'')) { - mvtext = cbm_arena_strndup(ctx->arena, mvtext + 1, - (size_t)(mvlen - 2)); - } - if (mvtext && strstr(mvtext, "://")) { - targets[n_targets++] = mvtext; - } - } - } - } - } - } + scan_nested_mapping_targets(ctx, val, targets, &n_targets); } } - /* Emit bindings for each source × target pair */ - for (int si = 0; si < n_sources; si++) { - for (int ti = 0; ti < n_targets; ti++) { - if (!sources[si] || !targets[ti]) { - continue; - } - CBMInfraBinding ib = { - .source_name = sources[si], - .target_url = targets[ti], - .broker = infer_broker(ctx->rel_path, source_keys[si]), - }; - cbm_infrabinding_push(&ctx->result->infra_bindings, ctx->arena, ib); - } - } + emit_infra_bindings(ctx, sources, source_keys, n_sources, targets, n_targets); } /* Walk a YAML block_sequence looking for list items with infra bindings */ @@ -507,7 +515,43 @@ static void scan_yaml_for_infra_bindings(CBMExtractCtx *ctx, TSNode node) { * where one is a source key (topic, queue_name) and another is a * target key (uri, push_endpoint). Handles nested blocks like * push_config { push_endpoint = "..." }. */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Extract a string value from an HCL attribute value node +// (quoted_template/template_literal/string_lit). +static char *extract_hcl_string_val(CBMArena *a, TSNode val_node, const char *source) { + const char *vk = ts_node_type(val_node); + if (strcmp(vk, "quoted_template") != 0 && strcmp(vk, "template_literal") != 0 && + strcmp(vk, "string_lit") != 0) { + return NULL; + } + char *val = cbm_node_text(a, val_node, source); + return strip_yaml_quotes(a, val); +} + +// Scan a nested HCL block for target keys (push_endpoint, uri, etc.). +static void scan_hcl_nested_block_targets(CBMExtractCtx *ctx, TSNode block, const char **targets, + int *n_targets) { + uint32_t bnc = ts_node_named_child_count(block); + for (uint32_t bi = 0; bi < bnc; bi++) { + TSNode bchild = ts_node_named_child(block, bi); + if (strcmp(ts_node_type(bchild), "attribute") != 0) { + continue; + } + TSNode bkey = ts_node_named_child(bchild, 0); + TSNode bval = ts_node_named_child(bchild, 1); + if (ts_node_is_null(bkey) || ts_node_is_null(bval)) { + continue; + } + char *bk = cbm_node_text(ctx->arena, bkey, ctx->source); + if (!bk || !is_target_key(bk)) { + continue; + } + char *bv = extract_hcl_string_val(ctx->arena, bval, ctx->source); + if (bv && strstr(bv, "://") && *n_targets < 8) { + targets[(*n_targets)++] = bv; + } + } +} + static void scan_hcl_block_for_bindings(CBMExtractCtx *ctx, TSNode block) { const char *sources[8] = {NULL}; const char *source_keys[8] = {NULL}; @@ -515,14 +559,12 @@ static void scan_hcl_block_for_bindings(CBMExtractCtx *ctx, TSNode block) { const char *targets[8] = {NULL}; int n_targets = 0; - /* Scan attributes at this level and one level deep (nested blocks) */ uint32_t nc = ts_node_named_child_count(block); for (uint32_t i = 0; i < nc; i++) { TSNode child = ts_node_named_child(block, i); const char *ck = ts_node_type(child); if (strcmp(ck, "attribute") == 0) { - /* HCL attribute: key = value */ TSNode key_node = ts_node_named_child(child, 0); TSNode val_node = ts_node_named_child(child, 1); if (ts_node_is_null(key_node) || ts_node_is_null(val_node)) { @@ -533,19 +575,7 @@ static void scan_hcl_block_for_bindings(CBMExtractCtx *ctx, TSNode block) { continue; } - /* Only extract literal string values (quoted_template or template_literal) */ - const char *vk = ts_node_type(val_node); - char *val = NULL; - if (strcmp(vk, "quoted_template") == 0 || strcmp(vk, "template_literal") == 0 || - strcmp(vk, "string_lit") == 0) { - val = cbm_node_text(ctx->arena, val_node, ctx->source); - if (val) { - int vlen = (int)strlen(val); - if (vlen >= 2 && (val[0] == '"' || val[0] == '\'')) { - val = cbm_arena_strndup(ctx->arena, val + 1, (size_t)(vlen - 2)); - } - } - } + char *val = extract_hcl_string_val(ctx->arena, val_node, ctx->source); if (!val || !val[0]) { continue; } @@ -559,55 +589,11 @@ static void scan_hcl_block_for_bindings(CBMExtractCtx *ctx, TSNode block) { targets[n_targets++] = val; } } else if (strcmp(ck, "block") == 0) { - /* Nested block (e.g., push_config { push_endpoint = "..." }) - * Scan its attributes for target keys */ - uint32_t bnc = ts_node_named_child_count(child); - for (uint32_t bi = 0; bi < bnc; bi++) { - TSNode bchild = ts_node_named_child(child, bi); - if (strcmp(ts_node_type(bchild), "attribute") != 0) { - continue; - } - TSNode bkey = ts_node_named_child(bchild, 0); - TSNode bval = ts_node_named_child(bchild, 1); - if (ts_node_is_null(bkey) || ts_node_is_null(bval)) { - continue; - } - char *bk = cbm_node_text(ctx->arena, bkey, ctx->source); - if (!bk || !is_target_key(bk)) { - continue; - } - const char *bvk = ts_node_type(bval); - if (strcmp(bvk, "quoted_template") == 0 || strcmp(bvk, "template_literal") == 0 || - strcmp(bvk, "string_lit") == 0) { - char *bv = cbm_node_text(ctx->arena, bval, ctx->source); - if (bv) { - int bvlen = (int)strlen(bv); - if (bvlen >= 2 && (bv[0] == '"' || bv[0] == '\'')) { - bv = cbm_arena_strndup(ctx->arena, bv + 1, (size_t)(bvlen - 2)); - } - if (bv && strstr(bv, "://") && n_targets < 8) { - targets[n_targets++] = bv; - } - } - } - } + scan_hcl_nested_block_targets(ctx, child, targets, &n_targets); } } - /* Emit bindings for each source × target pair */ - for (int si = 0; si < n_sources; si++) { - for (int ti = 0; ti < n_targets; ti++) { - if (!sources[si] || !targets[ti]) { - continue; - } - CBMInfraBinding ib = { - .source_name = sources[si], - .target_url = targets[ti], - .broker = infer_broker(ctx->rel_path, source_keys[si]), - }; - cbm_infrabinding_push(&ctx->result->infra_bindings, ctx->arena, ib); - } - } + emit_infra_bindings(ctx, sources, source_keys, n_sources, targets, n_targets); } /* Handle YAML files: walk top-level block_mapping recursively */ @@ -634,7 +620,54 @@ static void handle_yaml_nested(CBMExtractCtx *ctx, TSNode node) { // --- Main unified cursor walk --- -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Scan infra bindings for YAML/JSON/HCL languages. +static void scan_infra_bindings(CBMExtractCtx *ctx, TSNode node) { + if (ctx->language == CBM_LANG_YAML || ctx->language == CBM_LANG_JSON) { + const char *nk = ts_node_type(node); + if (strcmp(nk, "block_sequence") == 0 || strcmp(nk, "block_mapping") == 0 || + strcmp(nk, "array") == 0 || strcmp(nk, "document") == 0) { + scan_yaml_for_infra_bindings(ctx, node); + } + } else if (ctx->language == CBM_LANG_HCL) { + if (strcmp(ts_node_type(node), "block") == 0) { + scan_hcl_block_for_bindings(ctx, node); + } + } +} + +// Push scope markers for function, class, call, and import boundary nodes. +static void push_boundary_scopes(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, + WalkState *state, uint32_t depth) { + if (spec->function_node_types && cbm_kind_in_set(node, spec->function_node_types)) { + const char *fqn = compute_func_qn(ctx, node, spec, state); + if (fqn) { + push_scope(state, SCOPE_FUNC, depth, fqn); + } + } else if (spec->class_node_types && cbm_kind_in_set(node, spec->class_node_types)) { + const char *cqn = compute_class_qn(ctx, node); + if (cqn) { + push_scope(state, SCOPE_CLASS, depth, cqn); + } + } else if (ctx->language == CBM_LANG_RUST && strcmp(ts_node_type(node), "impl_item") == 0) { + TSNode type_node = ts_node_child_by_field_name(node, "type", 4); + if (!ts_node_is_null(type_node)) { + char *type_name = cbm_node_text(ctx->arena, type_node, ctx->source); + if (type_name && type_name[0]) { + const char *tqn = + cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, type_name); + push_scope(state, SCOPE_CLASS, depth, tqn); + } + } + } + + if (spec->call_node_types && cbm_kind_in_set(node, spec->call_node_types)) { + push_scope(state, SCOPE_CALL, depth, NULL); + } + if (spec->import_node_types && cbm_kind_in_set(node, spec->import_node_types)) { + push_scope(state, SCOPE_IMPORT, depth, NULL); + } +} + void cbm_extract_unified(CBMExtractCtx *ctx) { const CBMLangSpec *spec = cbm_lang_spec(ctx->language); if (!spec) { @@ -650,13 +683,9 @@ void cbm_extract_unified(CBMExtractCtx *ctx) { for (;;) { TSNode node = ts_tree_cursor_current_node(&cursor); - // 1. Pop scopes we've ascended out of pop_expired_scopes(&state, depth); - - // 2. Recompute state from remaining scopes recompute_state(&state, ctx->module_qn); - // 3. Dispatch to all handlers handle_string_constants(ctx, node, &state); handle_calls(ctx, node, spec, &state); handle_usages(ctx, node, spec, &state); @@ -667,56 +696,10 @@ void cbm_extract_unified(CBMExtractCtx *ctx) { handle_type_assigns(ctx, node, spec, &state); handle_string_refs(ctx, node, &state); handle_yaml_nested(ctx, node); + scan_infra_bindings(ctx, node); - /* Scan YAML/JSON for infra bindings (topic→URL pairs) */ - if (ctx->language == CBM_LANG_YAML || ctx->language == CBM_LANG_JSON) { - const char *nk = ts_node_type(node); - if (strcmp(nk, "block_sequence") == 0 || strcmp(nk, "block_mapping") == 0 || - strcmp(nk, "array") == 0 || strcmp(nk, "document") == 0) { - scan_yaml_for_infra_bindings(ctx, node); - } - } + push_boundary_scopes(ctx, node, spec, &state, depth); - /* Scan HCL for infra bindings (resource blocks with topic+endpoint) */ - if (ctx->language == CBM_LANG_HCL) { - const char *nk = ts_node_type(node); - if (strcmp(nk, "block") == 0) { - scan_hcl_block_for_bindings(ctx, node); - } - } - - // 4. Push scope markers for boundary nodes - if (spec->function_node_types && cbm_kind_in_set(node, spec->function_node_types)) { - const char *fqn = compute_func_qn(ctx, node, spec, &state); - if (fqn) { - push_scope(&state, SCOPE_FUNC, depth, fqn); - } - } else if (spec->class_node_types && cbm_kind_in_set(node, spec->class_node_types)) { - const char *cqn = compute_class_qn(ctx, node); - if (cqn) { - push_scope(&state, SCOPE_CLASS, depth, cqn); - } - } else if (ctx->language == CBM_LANG_RUST && strcmp(ts_node_type(node), "impl_item") == 0) { - // Rust impl block acts as class scope for methods - TSNode type_node = ts_node_child_by_field_name(node, "type", 4); - if (!ts_node_is_null(type_node)) { - char *type_name = cbm_node_text(ctx->arena, type_node, ctx->source); - if (type_name && type_name[0]) { - const char *tqn = - cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, type_name); - push_scope(&state, SCOPE_CLASS, depth, tqn); - } - } - } - - if (spec->call_node_types && cbm_kind_in_set(node, spec->call_node_types)) { - push_scope(&state, SCOPE_CALL, depth, NULL); - } - if (spec->import_node_types && cbm_kind_in_set(node, spec->import_node_types)) { - push_scope(&state, SCOPE_IMPORT, depth, NULL); - } - - // 5. Advance cursor: DFS order if (ts_tree_cursor_goto_first_child(&cursor)) { depth++; continue; @@ -724,7 +707,6 @@ void cbm_extract_unified(CBMExtractCtx *ctx) { if (ts_tree_cursor_goto_next_sibling(&cursor)) { continue; } - // Ascend until we find a sibling bool found = false; while (ts_tree_cursor_goto_parent(&cursor)) { depth--; diff --git a/internal/cbm/extract_usages.c b/internal/cbm/extract_usages.c index fb21c411c..047a0aee1 100644 --- a/internal/cbm/extract_usages.c +++ b/internal/cbm/extract_usages.c @@ -70,9 +70,40 @@ static bool is_reference_node(TSNode node, CBMLanguage lang) { } } +// Check if a reference node is a definition name (the "name" field of its parent). +static bool is_definition_name(TSNode node) { + TSNode parent = ts_node_parent(node); + if (ts_node_is_null(parent)) { + return false; + } + TSNode name_field = ts_node_child_by_field_name(parent, "name", 4); + return !ts_node_is_null(name_field) && + ts_node_start_byte(name_field) == ts_node_start_byte(node) && + ts_node_end_byte(name_field) == ts_node_end_byte(node); +} + +// Try to emit a usage for a reference node. Returns early if the node should be skipped. +static void try_emit_usage(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) { + if (!is_reference_node(node, ctx->language)) { + return; + } + if (is_inside_call(node, spec) || is_inside_import(node, spec)) { + return; + } + if (is_definition_name(node)) { + return; + } + char *name = cbm_node_text(ctx->arena, node, ctx->source); + if (name && name[0] && !cbm_is_keyword(name, ctx->language)) { + CBMUsage usage; + usage.ref_name = name; + usage.enclosing_func_qn = cbm_enclosing_func_qn_cached(ctx, node); + cbm_usages_push(&ctx->result->usages, ctx->arena, usage); + } +} + // Iterative usage walker — explicit stack #define USAGES_STACK_CAP 4096 -// NOLINTNEXTLINE(readability-function-cognitive-complexity) static void walk_usages(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec) { TSNode stack[USAGES_STACK_CAP]; int top = 0; @@ -80,34 +111,7 @@ static void walk_usages(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec while (top > 0) { TSNode node = stack[--top]; - - if (is_reference_node(node, ctx->language)) { - bool skip = false; - if (is_inside_call(node, spec) || is_inside_import(node, spec)) { - skip = true; - } - if (!skip) { - TSNode parent = ts_node_parent(node); - if (!ts_node_is_null(parent)) { - TSNode name_field = ts_node_child_by_field_name(parent, "name", 4); - if (!ts_node_is_null(name_field) && - ts_node_start_byte(name_field) == ts_node_start_byte(node) && - ts_node_end_byte(name_field) == ts_node_end_byte(node)) { - skip = true; - } - } - } - if (!skip) { - char *name = cbm_node_text(ctx->arena, node, ctx->source); - if (name && name[0] && !cbm_is_keyword(name, ctx->language)) { - CBMUsage usage; - usage.ref_name = name; - usage.enclosing_func_qn = cbm_enclosing_func_qn_cached(ctx, node); - cbm_usages_push(&ctx->result->usages, ctx->arena, usage); - } - } - } - + try_emit_usage(ctx, node, spec); uint32_t count = ts_node_child_count(node); for (int i = (int)count - 1; i >= 0 && top < USAGES_STACK_CAP; i--) { stack[top++] = ts_node_child(node, (uint32_t)i); diff --git a/internal/cbm/helpers.c b/internal/cbm/helpers.c index 9af5f2e94..6598efba3 100644 --- a/internal/cbm/helpers.c +++ b/internal/cbm/helpers.c @@ -545,7 +545,97 @@ static const char *module_parents_matlab[] = {"source_file", NULL}; static const char *module_parents_form[] = {"source_file", NULL}; static const char *module_parents_magma[] = {"source_file", NULL}; -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Check if parent node kind matches direct-or-grandparent for scripting languages. +// Returns true if pk matches root_kind, or pk matches wrapper_kind and grandparent is root_kind. +static bool check_script_module_level(TSNode parent, const char *pk, const char *root_kind, + const char *wrapper_kind) { + if (strcmp(pk, root_kind) == 0) { + return true; + } + if (wrapper_kind && strcmp(pk, wrapper_kind) == 0) { + TSNode gp = ts_node_parent(parent); + return !ts_node_is_null(gp) && strcmp(ts_node_type(gp), root_kind) == 0; + } + return false; +} + +// Get the module-level parent type list for table-driven languages. +static const char **get_module_parents(CBMLanguage lang) { + switch (lang) { + case CBM_LANG_GO: + return module_parents_go; + case CBM_LANG_RUST: + return module_parents_rust; + case CBM_LANG_JAVA: + return module_parents_java; + case CBM_LANG_KOTLIN: + return module_parents_kotlin; + case CBM_LANG_SCALA: + return module_parents_scala; + case CBM_LANG_CSHARP: + return module_parents_csharp; + case CBM_LANG_PHP: + return module_parents_php; + case CBM_LANG_RUBY: + return module_parents_ruby; + case CBM_LANG_C: + case CBM_LANG_CPP: + case CBM_LANG_OBJC: + return module_parents_c; + case CBM_LANG_ZIG: + return module_parents_zig; + case CBM_LANG_BASH: + return module_parents_bash; + case CBM_LANG_ERLANG: + return module_parents_erlang; + case CBM_LANG_HASKELL: + return module_parents_haskell; + case CBM_LANG_OCAML: + return module_parents_ocaml; + case CBM_LANG_ELIXIR: + return module_parents_elixir; + case CBM_LANG_HTML: + return module_parents_html; + case CBM_LANG_CSS: + case CBM_LANG_SCSS: + return module_parents_css; + case CBM_LANG_SQL: + return module_parents_sql; + case CBM_LANG_TOML: + return module_parents_toml; + case CBM_LANG_HCL: + return module_parents_hcl; + case CBM_LANG_JSON: + case CBM_LANG_INI: + case CBM_LANG_XML: + case CBM_LANG_MARKDOWN: + return module_parents_config; + case CBM_LANG_SWIFT: + return module_parents_zig; + case CBM_LANG_DART: + return module_parents_php; + case CBM_LANG_PERL: + case CBM_LANG_GROOVY: + return module_parents_zig; + case CBM_LANG_R: + return module_parents_php; + case CBM_LANG_MAKEFILE: + return module_parents_makefile; + case CBM_LANG_COMMONLISP: + return module_parents_commonlisp; + case CBM_LANG_MATLAB: + return module_parents_matlab; + case CBM_LANG_LEAN: + return module_parents_zig; + case CBM_LANG_FORM: + return module_parents_form; + case CBM_LANG_MAGMA: + return module_parents_magma; + default: + return NULL; + } +} + bool cbm_is_module_level(TSNode node, CBMLanguage lang) { TSNode parent = ts_node_parent(node); if (ts_node_is_null(parent)) { @@ -553,155 +643,23 @@ bool cbm_is_module_level(TSNode node, CBMLanguage lang) { } const char *pk = ts_node_type(parent); - // Python: module or expression_statement -> module + // Languages with wrapper-pattern (expression_statement/export_statement/assignment_statement) if (lang == CBM_LANG_PYTHON) { - if (strcmp(pk, "module") == 0) { - return true; - } - if (strcmp(pk, "expression_statement") == 0) { - TSNode gp = ts_node_parent(parent); - return !ts_node_is_null(gp) && strcmp(ts_node_type(gp), "module") == 0; - } - return false; + return check_script_module_level(parent, pk, "module", "expression_statement"); } - - // JS/TS: program, or export_statement -> program if (lang == CBM_LANG_JAVASCRIPT || lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX) { - if (strcmp(pk, "program") == 0) { - return true; - } - if (strcmp(pk, "export_statement") == 0) { - TSNode gp = ts_node_parent(parent); - return !ts_node_is_null(gp) && strcmp(ts_node_type(gp), "program") == 0; - } - return false; + return check_script_module_level(parent, pk, "program", "export_statement"); } - - // Lua: chunk if (lang == CBM_LANG_LUA) { - if (strcmp(pk, "chunk") == 0) { - return true; - } - // assignment_statement -> chunk - if (strcmp(pk, "assignment_statement") == 0) { - TSNode gp = ts_node_parent(parent); - return !ts_node_is_null(gp) && strcmp(ts_node_type(gp), "chunk") == 0; - } - return false; + return check_script_module_level(parent, pk, "chunk", "assignment_statement"); } - - // YAML: document or stream if (lang == CBM_LANG_YAML) { return strcmp(pk, "document") == 0 || strcmp(pk, "stream") == 0 || strcmp(pk, "block_mapping") == 0; } // Table lookup for the rest - const char **parents = NULL; - switch (lang) { - case CBM_LANG_GO: - parents = module_parents_go; - break; - case CBM_LANG_RUST: - parents = module_parents_rust; - break; - case CBM_LANG_JAVA: - parents = module_parents_java; - break; - case CBM_LANG_KOTLIN: - parents = module_parents_kotlin; - break; - case CBM_LANG_SCALA: - parents = module_parents_scala; - break; - case CBM_LANG_CSHARP: - parents = module_parents_csharp; - break; - case CBM_LANG_PHP: - parents = module_parents_php; - break; - case CBM_LANG_RUBY: - parents = module_parents_ruby; - break; - case CBM_LANG_C: - case CBM_LANG_CPP: - case CBM_LANG_OBJC: - parents = module_parents_c; - break; - case CBM_LANG_ZIG: - parents = module_parents_zig; - break; - case CBM_LANG_BASH: - parents = module_parents_bash; - break; - case CBM_LANG_ERLANG: - parents = module_parents_erlang; - break; - case CBM_LANG_HASKELL: - parents = module_parents_haskell; - break; - case CBM_LANG_OCAML: - parents = module_parents_ocaml; - break; - case CBM_LANG_ELIXIR: - parents = module_parents_elixir; - break; - case CBM_LANG_HTML: - parents = module_parents_html; - break; - case CBM_LANG_CSS: - case CBM_LANG_SCSS: - parents = module_parents_css; - break; - case CBM_LANG_SQL: - parents = module_parents_sql; - break; - case CBM_LANG_TOML: - parents = module_parents_toml; - break; - case CBM_LANG_HCL: - parents = module_parents_hcl; - break; - case CBM_LANG_JSON: - case CBM_LANG_INI: - case CBM_LANG_XML: - case CBM_LANG_MARKDOWN: - parents = module_parents_config; - break; - case CBM_LANG_SWIFT: - parents = module_parents_zig; - break; // source_file - case CBM_LANG_DART: - parents = module_parents_php; - break; // program - case CBM_LANG_PERL: - case CBM_LANG_GROOVY: - parents = module_parents_zig; - break; // source_file - case CBM_LANG_R: - parents = module_parents_php; - break; // program - case CBM_LANG_MAKEFILE: - parents = module_parents_makefile; - break; - case CBM_LANG_COMMONLISP: - parents = module_parents_commonlisp; - break; - case CBM_LANG_MATLAB: - parents = module_parents_matlab; - break; - case CBM_LANG_LEAN: - parents = module_parents_zig; - break; // source_file - case CBM_LANG_FORM: - parents = module_parents_form; - break; - case CBM_LANG_MAGMA: - parents = module_parents_magma; - break; - default: - return false; - } + const char **parents = get_module_parents(lang); if (parents) { for (const char **p = parents; *p; p++) { if (strcmp(pk, *p) == 0) { @@ -728,16 +686,47 @@ static size_t strip_ext_len(const char *s, size_t len) { return len; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Check if a path part should be skipped (Python __init__, JS/TS index). +static bool should_skip_fqn_part(const char *part, size_t part_len, bool is_last, bool has_name) { + if (!is_last || !has_name) { + return false; + } + if (part_len == 8 && memcmp(part, "__init__", 8) == 0) { + return true; + } + if (part_len == 5 && memcmp(part, "index", 5) == 0) { + return true; + } + return false; +} + +// Append dotted path segments from rel_path (extension-stripped) to output buffer. +static char *append_path_segments(char *out, const char *rel_path, size_t plen, bool has_name) { + const char *start = rel_path; + const char *end_ptr = rel_path + plen; + while (start < end_ptr) { + const char *slash = (const char *)memchr(start, '/', end_ptr - start); + const char *part_end = slash ? slash : end_ptr; + size_t part_len = (size_t)(part_end - start); + + if (part_len > 0) { + bool is_last = (part_end == end_ptr); + if (!should_skip_fqn_part(start, part_len, is_last, has_name)) { + *out++ = '.'; + memcpy(out, start, part_len); + out += part_len; + } + } + start = part_end + 1; + } + return out; +} + char *cbm_fqn_compute(CBMArena *a, const char *project, const char *rel_path, const char *name) { - // Build: project.path1.path2.filename.name - // where rel_path = "path1/path2/filename.ext" - // Strip extension, replace / with ., drop __init__ and index size_t proj_len = strlen(project); size_t path_len = strlen(rel_path); size_t name_len = name ? strlen(name) : 0; - // Worst case: project + . + path (with / -> .) + . + name + NUL size_t max_len = proj_len + 1 + path_len + 1 + name_len + 1; char *buf = (char *)cbm_arena_alloc(a, max_len); if (!buf) { @@ -748,43 +737,8 @@ char *cbm_fqn_compute(CBMArena *a, const char *project, const char *rel_path, co memcpy(out, project, proj_len); out += proj_len; - // Process path: strip extension, split by /, replace with dots - // Skip trailing extension size_t plen = strip_ext_len(rel_path, path_len); - - // Split by '/' and append each part as .part - const char *start = rel_path; - const char *end_ptr = rel_path + plen; - while (start < end_ptr) { - const char *slash = (const char *)memchr(start, '/', end_ptr - start); - const char *part_end = slash ? slash : end_ptr; - size_t part_len = (size_t)(part_end - start); - - if (part_len > 0) { - /* Handle __init__ (Python) and index (JS/TS): - * Strip ONLY when a name suffix is provided, so that symbols - * inside __init__.py get clean package QNs (proj.pkg.MyClass). - * When no name is given (fqn_module for the file itself), - * keep __init__/index to avoid QN collision with the Folder - * node for the same directory. */ - bool is_last = (part_end == end_ptr); - bool skip = false; - if (is_last && name && name_len > 0) { - if (part_len == 8 && memcmp(start, "__init__", 8) == 0) { - skip = true; - } - if (part_len == 5 && memcmp(start, "index", 5) == 0) { - skip = true; - } - } - if (!skip) { - *out++ = '.'; - memcpy(out, start, part_len); - out += part_len; - } - } - start = part_end + 1; - } + out = append_path_segments(out, rel_path, plen, name && name_len > 0); if (name && name_len > 0) { *out++ = '.'; @@ -834,58 +788,54 @@ char *cbm_fqn_folder(CBMArena *a, const char *project, const char *rel_dir) { /* ── String literal classifier ──────────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Check if a slash-prefixed string looks like a filesystem path. +static bool is_filesystem_path(const char *s, int len) { + if (len <= 4) { + return false; + } + return strncmp(s, "/usr/", 5) == 0 || strncmp(s, "/bin/", 5) == 0 || + strncmp(s, "/etc/", 5) == 0 || strncmp(s, "/var/", 5) == 0 || + strncmp(s, "/tmp/", 5) == 0 || strncmp(s, "/opt/", 5) == 0 || + strncmp(s, "/home/", 6) == 0 || strncmp(s, "/dev/", 5) == 0 || + strncmp(s, "/sys/", 5) == 0 || strncmp(s, "/proc/", 6) == 0; +} + +// Check if a slash-prefixed string looks like a REST API path. +static bool is_rest_path(const char *s, int len) { + if (is_filesystem_path(s, len)) { + return false; + } + if (len > 1 && s[len - 1] == '/') { + return false; /* regex pattern */ + } + if (s[1] == '^') { + return false; /* regex */ + } + if (len == 1 || (len == 2 && s[1] == '/')) { + return false; /* bare / or // */ + } + if (s[1] == '.') { + return false; /* relative path */ + } + for (int i = 1; i < len && i < 20; i++) { + char c = s[i]; + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) { + return true; + } + } + return false; +} + static bool is_url_like(const char *s, int len) { if (len < 3) { return false; } - - /* Full URL: contains :// */ if (strstr(s, "://")) { return true; } - - /* REST path: starts with / + alphanumeric */ if (s[0] == '/') { - /* Reject file system paths */ - if (len > 4 && (strncmp(s, "/usr/", 5) == 0 || strncmp(s, "/bin/", 5) == 0 || - strncmp(s, "/etc/", 5) == 0 || strncmp(s, "/var/", 5) == 0 || - strncmp(s, "/tmp/", 5) == 0 || strncmp(s, "/opt/", 5) == 0 || - strncmp(s, "/home/", 6) == 0 || strncmp(s, "/dev/", 5) == 0 || - strncmp(s, "/sys/", 5) == 0 || strncmp(s, "/proc/", 6) == 0)) { - return false; - } - /* Reject regex patterns */ - if (len > 1 && s[len - 1] == '/') { - return false; - } - if (s[1] == '^') { - return false; - } - - /* Reject single / or // */ - if (len == 1) { - return false; - } - if (len == 2 && s[1] == '/') { - return false; - } - - /* Reject relative paths ./ ../ */ - if (s[1] == '.') { - return false; - } - - /* Must have at least one path segment with alphanumeric */ - for (int i = 1; i < len && i < 20; i++) { - char c = s[i]; - if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) { - return true; - } - } - return false; + return is_rest_path(s, len); } - return false; } diff --git a/internal/cbm/sqlite_writer.c b/internal/cbm/sqlite_writer.c index 876befeed..07fb5110a 100644 --- a/internal/cbm/sqlite_writer.c +++ b/internal/cbm/sqlite_writer.c @@ -392,7 +392,8 @@ static void pb_flush_leaf(PageBuilder *pb) { // Record this leaf for interior page building if (pb->leaf_count >= pb->leaf_cap) { - pb->leaf_cap = pb->leaf_cap == 0 ? 256 : pb->leaf_cap * 2; + int old_cap = pb->leaf_cap; + pb->leaf_cap = old_cap == 0 ? 256 : old_cap * 2; void *tmp = realloc(pb->leaves, (size_t)pb->leaf_cap * sizeof(PageRef)); if (!tmp) { free(pb->leaves); @@ -400,6 +401,8 @@ static void pb_flush_leaf(PageBuilder *pb) { return; } pb->leaves = (PageRef *)tmp; + /* Zero-init new slots */ + memset(&pb->leaves[old_cap], 0, ((size_t)pb->leaf_cap - (size_t)old_cap) * sizeof(PageRef)); } pb->leaves[pb->leaf_count].page_num = page_num; // max_key is set by caller before flush @@ -447,7 +450,107 @@ static void pb_add_cell(PageBuilder *pb, const uint8_t *cell, int cell_len) { // - Lookup: X ≤ K0 → cell[0].left_child, K0 < X ≤ K1 → cell[1].left_child, etc. // - Table keys: varint(rowid) // - Index keys: varint(payload_len) + payload (full index record) -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +// Build an interior cell for a child PageRef. Returns cell length. +// For table B-trees: child_page(4) + varint(rowid). +// For index B-trees: child_page(4) + separator_cell. +// cell_buf must be at least 20 bytes for table cells. +// For index cells, returns malloc'd data via *out_heap (caller frees). +static int build_interior_cell(const PageRef *child, bool is_index, uint8_t *cell_buf, + uint8_t **out_heap) { + *out_heap = NULL; + if (!is_index) { + put_u32(cell_buf, child->page_num); + return 4 + put_varint(cell_buf + 4, child->max_key); + } + int clen = 4 + child->sep_cell_len; + uint8_t *data = (uint8_t *)malloc(clen); + put_u32(data, child->page_num); + memcpy(data + 4, child->sep_cell, child->sep_cell_len); + *out_heap = data; + return clen; +} + +// Write a completed interior page to disk and record it as a parent. +// Returns updated parent_count, or -1 on allocation failure. +static int write_interior_page(PageBuilder *pb, uint8_t *page, int cell_count, int content_offset, + uint32_t right_child_page, const PageRef *children, + int right_child_idx, bool is_index, PageRef **parents, + int parent_count, int *parent_cap) { + uint32_t pnum = pb->next_page++; + page[0] = is_index ? 0x02 : 0x05; + put_u16(page + 1, 0); + put_u16(page + 3, (uint16_t)cell_count); + put_u16(page + 5, (uint16_t)content_offset); + page[7] = 0; + put_u32(page + 8, right_child_page); + + fseek(pb->fp, (long)(pnum - 1) * PAGE_SIZE, SEEK_SET); + fwrite(page, 1, PAGE_SIZE, pb->fp); + + if (parent_count >= *parent_cap) { + int old_pcap = *parent_cap; + *parent_cap = old_pcap == 0 ? 64 : old_pcap * 2; + PageRef *tmp = (PageRef *)realloc(*parents, *parent_cap * sizeof(PageRef)); + if (!tmp) { + free(*parents); + *parents = NULL; + return -1; + } + *parents = tmp; + memset(&(*parents)[old_pcap], 0, + ((size_t)*parent_cap - (size_t)old_pcap) * sizeof(PageRef)); + } + (*parents)[parent_count].page_num = pnum; + (*parents)[parent_count].max_key = children[right_child_idx].max_key; + if (is_index && children[right_child_idx].sep_cell) { + int slen = children[right_child_idx].sep_cell_len; + (*parents)[parent_count].sep_cell = (uint8_t *)malloc(slen); + memcpy((*parents)[parent_count].sep_cell, children[right_child_idx].sep_cell, slen); + (*parents)[parent_count].sep_cell_len = slen; + } else { + (*parents)[parent_count].sep_cell = NULL; + (*parents)[parent_count].sep_cell_len = 0; + } + return parent_count + 1; +} + +// Free a PageRef array (sep_cell allocations), unless it's the original leaves. +static void free_children(PageRef *children, int child_count, const PageRef *leaves) { + if (children != leaves) { + for (int j = 0; j < child_count; j++) { + free(children[j].sep_cell); + } + free(children); + } +} + +// Fill an interior page with cells from children[*idx..child_count-2]. +// Updates cell_count, content_offset, ptr_offset, and *idx. +static void fill_interior_page(uint8_t *page, const PageRef *children, int child_count, + bool is_index, int *idx, int *cell_count, int *content_offset, + int *ptr_offset) { + while (*idx < child_count - 1) { + uint8_t tbuf[20]; + uint8_t *heap_cell = NULL; + int clen = build_interior_cell(&children[*idx], is_index, tbuf, &heap_cell); + uint8_t *cell_data = heap_cell ? heap_cell : tbuf; + + int available = *content_offset - *ptr_offset - 2; + if (clen > available && *cell_count > 0) { + free(heap_cell); + break; + } + + *content_offset -= clen; + memcpy(page + *content_offset, cell_data, clen); + put_u16(page + *ptr_offset, (uint16_t)*content_offset); + *ptr_offset += 2; + (*cell_count)++; + free(heap_cell); + (*idx)++; + } +} + static uint32_t pb_build_interior(PageBuilder *pb, bool is_index) { if (!pb->leaves) { return 0; @@ -466,144 +569,41 @@ static uint32_t pb_build_interior(PageBuilder *pb, bool is_index) { int i = 0; while (i < child_count) { - // Start a new interior page uint8_t page[PAGE_SIZE]; memset(page, 0, PAGE_SIZE); int cell_count = 0; int content_offset = PAGE_SIZE; - // Interior header: - // flag(1)+freeblock(2)+cell_count(2)+content_start(2)+frag(1)+right_child(4) = 12 int ptr_offset = 12; - // Add cells for children[i..] until page fills or children exhausted. - // Each child gets a cell EXCEPT the last one (which becomes right_child). - while (i < child_count - 1) { - // Build cell: child_page(4) + key for children[i] - uint8_t *cell_data = NULL; - int clen; - uint8_t tbuf[20]; - bool heap_cell = false; + fill_interior_page(page, children, child_count, is_index, &i, &cell_count, + &content_offset, &ptr_offset); - if (!is_index) { - put_u32(tbuf, children[i].page_num); - clen = 4 + put_varint(tbuf + 4, children[i].max_key); - cell_data = tbuf; - } else { - clen = 4 + children[i].sep_cell_len; - cell_data = (uint8_t *)malloc(clen); - heap_cell = true; - put_u32(cell_data, children[i].page_num); - memcpy(cell_data + 4, children[i].sep_cell, children[i].sep_cell_len); - } - - int available = content_offset - ptr_offset - 2; - if (clen > available && cell_count > 0) { - if (heap_cell) { - free(cell_data); - } - break; // page full, flush with what we have - } - - content_offset -= clen; - memcpy(page + content_offset, cell_data, clen); - put_u16(page + ptr_offset, (uint16_t)content_offset); - ptr_offset += 2; - cell_count++; - if (heap_cell) { - free(cell_data); - } - i++; + int right_child_idx = (i < child_count - 1) ? i : child_count - 1; + uint32_t right_child_page = 0; + if (right_child_idx >= 0 && right_child_idx < child_count) { + right_child_page = children[right_child_idx].page_num; } - - // Determine right-child: if we consumed all children, it's the last one. - // If we stopped early due to page full, right-child is children[i-1] - // (the last child we added a cell for)... No: cells were added for - // children[page_start..i-1], so right-child = children[i]. - // But if cell_count == 0 and we didn't add anything, we have a problem. - // That can happen only if the first cell doesn't fit — shouldn't happen - // for typical cell sizes on a 4096 page. - - uint32_t right_child_page; - int right_child_idx; if (i < child_count - 1) { - // Page full before reaching the end. Cells cover children[page_start..i-1]. - // Right-child = children[i] (first child NOT given a cell on this page). - // But wait, we need right-child to be the last child whose keys are > last cell's - // key. Actually: cells[page_start..i-1] have keys K_{page_start}..K_{i-1}. - // Right-child should contain keys > K_{i-1}, which is children[i]. - // But children[i] hasn't been processed yet — it will be on the next interior page. - // The right-child for THIS page should be children[i], and on the NEXT page, - // children[i] will get a cell too. But that would mean children[i] is both - // the right-child of this page AND has a cell on the next page. - // That's wrong — each child should appear exactly once. - // - // Correct approach: cells for children[page_start..i-1], right-child = children[i]. - // Then next page starts at i+1. - right_child_page = children[i].page_num; - right_child_idx = i; - i++; // skip the right-child for the next page's iteration + i++; } else { - // Reached the end normally. Last child = right-child. - right_child_page = children[child_count - 1].page_num; - right_child_idx = child_count - 1; - i = child_count; // exit outer loop + i = child_count; } - // Write the interior page - uint32_t pnum = pb->next_page++; - page[0] = is_index ? 0x02 : 0x05; - put_u16(page + 1, 0); - put_u16(page + 3, (uint16_t)cell_count); - put_u16(page + 5, (uint16_t)content_offset); - page[7] = 0; - put_u32(page + 8, right_child_page); - - fseek(pb->fp, (long)(pnum - 1) * PAGE_SIZE, SEEK_SET); - fwrite(page, 1, PAGE_SIZE, pb->fp); - - // Record this interior page as a parent for the next level - if (parent_count >= parent_cap) { - parent_cap = parent_cap == 0 ? 64 : parent_cap * 2; - PageRef *tmp = (PageRef *)realloc(parents, parent_cap * sizeof(PageRef)); - if (!tmp) { - free(parents); - parents = NULL; - break; - } - parents = tmp; + parent_count = write_interior_page(pb, page, cell_count, content_offset, + right_child_page, children, right_child_idx, + is_index, &parents, parent_count, &parent_cap); + if (parent_count < 0) { + break; } - parents[parent_count].page_num = pnum; - parents[parent_count].max_key = children[right_child_idx].max_key; - // For multi-level index trees, copy the separator from the rightmost child - if (is_index && children[right_child_idx].sep_cell) { - int slen = children[right_child_idx].sep_cell_len; - parents[parent_count].sep_cell = (uint8_t *)malloc(slen); - memcpy(parents[parent_count].sep_cell, children[right_child_idx].sep_cell, slen); - parents[parent_count].sep_cell_len = slen; - } else { - parents[parent_count].sep_cell = NULL; - parents[parent_count].sep_cell_len = 0; - } - parent_count++; } - if (children != pb->leaves) { - for (int j = 0; j < child_count; j++) { - free(children[j].sep_cell); - } - free(children); - } + free_children(children, child_count, pb->leaves); children = parents; child_count = parent_count; } uint32_t root = children ? children[0].page_num : 0; - if (children != pb->leaves) { - for (int j = 0; j < child_count; j++) { - free(children[j].sep_cell); - } - free(children); - } + free_children(children, child_count, pb->leaves); return root; } @@ -818,18 +818,21 @@ static uint8_t *build_index_entry_unique_2int_text_rowid(int64_t v1, int64_t v2, // --- Write a table B-tree from records --- -// Helper: ensure leaf_cap, flush current leaf page with given max_key -static void pb_ensure_leaf_cap(PageBuilder *pb) { - if (pb->leaf_count >= pb->leaf_cap) { - pb->leaf_cap = pb->leaf_cap == 0 ? 256 : pb->leaf_cap * 2; - void *tmp = realloc(pb->leaves, (size_t)pb->leaf_cap * sizeof(PageRef)); - if (!tmp) { - free(pb->leaves); - pb->leaves = NULL; - return; - } - pb->leaves = (PageRef *)tmp; +// Ensure leaves array has capacity for one more entry. +// Returns false on allocation failure. +static bool pb_ensure_leaf_cap(PageBuilder *pb) { + if (pb->leaf_count < pb->leaf_cap) { + return true; } + pb->leaf_cap = pb->leaf_cap == 0 ? 256 : pb->leaf_cap * 2; + void *tmp = realloc(pb->leaves, (size_t)pb->leaf_cap * sizeof(PageRef)); + if (!tmp) { + free(pb->leaves); + pb->leaves = NULL; + return false; + } + pb->leaves = (PageRef *)tmp; + return true; } // Add a table cell to the PageBuilder, flushing leaf pages as needed. @@ -842,8 +845,7 @@ static void pb_add_table_cell_with_flush(PageBuilder *pb, int64_t rowid, const u } if (!pb_cell_fits(pb, cell_len) && pb->cell_count > 0) { - pb_ensure_leaf_cap(pb); - if (!pb->leaves) { + if (!pb_ensure_leaf_cap(pb)) { free(cell); return; } @@ -919,76 +921,64 @@ static uint32_t write_table_btree(FILE *fp, uint32_t *next_page, const uint8_t * return pb_finalize_table(&pb, next_page, rowids[count - 1]); } +// Promote the last cell from current page to separator, un-add it, and flush. +static bool pb_promote_and_flush(PageBuilder *pb, uint8_t **cells, int *cell_lens, int prev_idx) { + if (!pb_ensure_leaf_cap(pb)) { + return false; + } + pb->leaves[pb->leaf_count].max_key = 0; + pb->leaves[pb->leaf_count].sep_cell = (uint8_t *)malloc(cell_lens[prev_idx]); + memcpy(pb->leaves[pb->leaf_count].sep_cell, cells[prev_idx], cell_lens[prev_idx]); + pb->leaves[pb->leaf_count].sep_cell_len = cell_lens[prev_idx]; + + // Un-add the last cell + pb->cell_count--; + pb->content_offset += cell_lens[prev_idx]; + pb->ptr_offset -= 2; + + pb_flush_leaf(pb); + return true; +} + +// Write an empty index leaf page. +static uint32_t write_empty_index_leaf(FILE *fp, uint32_t *next_page) { + uint32_t pnum = (*next_page)++; + uint8_t page[PAGE_SIZE]; + memset(page, 0, PAGE_SIZE); + page[0] = 0x0A; + put_u16(page + 1, 0); + put_u16(page + 3, 0); + put_u16(page + 5, (uint16_t)PAGE_SIZE); + page[7] = 0; + fseek(fp, (long)(pnum - 1) * PAGE_SIZE, SEEK_SET); + fwrite(page, 1, PAGE_SIZE, fp); + return pnum; +} + // Write leaf pages for an index, returns root page. -// NOLINTNEXTLINE(readability-function-cognitive-complexity) static uint32_t write_index_btree(FILE *fp, uint32_t *next_page, uint8_t **cells, int *cell_lens, int count) { if (count == 0) { - uint32_t pnum = (*next_page)++; - uint8_t page[PAGE_SIZE]; - memset(page, 0, PAGE_SIZE); - page[0] = 0x0A; // leaf index - put_u16(page + 1, 0); // no freeblocks - put_u16(page + 3, 0); // 0 cells - put_u16(page + 5, (uint16_t)PAGE_SIZE); // content at end of page - page[7] = 0; // 0 fragmented bytes - fseek(fp, (long)(pnum - 1) * PAGE_SIZE, SEEK_SET); - fwrite(page, 1, PAGE_SIZE, fp); - return pnum; + return write_empty_index_leaf(fp, next_page); } PageBuilder pb; pb_init(&pb, fp, *next_page, true); - // SQLite index B-trees are B-trees (NOT B+ trees): separator keys on interior - // pages are real entries that must NOT also appear on leaf pages. When a leaf - // overflows, the last entry is PROMOTED to the interior page and removed from - // the leaf. This ensures each entry exists exactly once in the tree. for (int i = 0; i < count; i++) { if (!pb_cell_fits(&pb, cell_lens[i]) && pb.cell_count > 0) { - if (pb.leaf_count >= pb.leaf_cap) { - pb.leaf_cap = pb.leaf_cap == 0 ? 256 : pb.leaf_cap * 2; - void *tmp = realloc(pb.leaves, (size_t)pb.leaf_cap * sizeof(PageRef)); - if (!tmp) { - free(pb.leaves); - pb.leaves = NULL; - return 0; - } - pb.leaves = (PageRef *)tmp; + if (!pb_promote_and_flush(&pb, cells, cell_lens, i - 1)) { + return 0; } - pb.leaves[pb.leaf_count].max_key = 0; // unused for index - - // Promote last cell from leaf to interior: remove it from the leaf page - // and store it as the separator for the interior page. - int last = i - 1; - pb.leaves[pb.leaf_count].sep_cell = (uint8_t *)malloc(cell_lens[last]); - memcpy(pb.leaves[pb.leaf_count].sep_cell, cells[last], cell_lens[last]); - pb.leaves[pb.leaf_count].sep_cell_len = cell_lens[last]; - - // "Un-add" the last cell from the current page - pb.cell_count--; - pb.content_offset += cell_lens[last]; - pb.ptr_offset -= 2; - - pb_flush_leaf(&pb); } pb_add_cell(&pb, cells[i], cell_lens[i]); } if (pb.cell_count > 0) { - if (pb.leaf_count >= pb.leaf_cap) { - pb.leaf_cap = pb.leaf_cap == 0 ? 256 : pb.leaf_cap * 2; - void *tmp = realloc(pb.leaves, (size_t)pb.leaf_cap * sizeof(PageRef)); - if (!tmp) { - free(pb.leaves); - pb.leaves = NULL; - return 0; - } - pb.leaves = (PageRef *)tmp; + if (!pb_ensure_leaf_cap(&pb)) { + return 0; } pb.leaves[pb.leaf_count].max_key = 0; - // Last leaf: no promotion needed — last cell stays on the leaf. - // Store last cell as separator in case multi-level interior pages need it. int last = count - 1; pb.leaves[pb.leaf_count].sep_cell = (uint8_t *)malloc(cell_lens[last]); memcpy(pb.leaves[pb.leaf_count].sep_cell, cells[last], cell_lens[last]); diff --git a/scripts/lint.sh b/scripts/lint.sh index a6c7aba75..6027d2b28 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -1,12 +1,12 @@ #!/bin/bash -# lint.sh — Run all CI linters (clang-format + cppcheck). +# lint.sh — Run all linters (clang-tidy + cppcheck + clang-format). # # Usage: -# scripts/lint.sh # Default tools +# scripts/lint.sh # All 3 linters # scripts/lint.sh CLANG_FORMAT=clang-format-20 # Override formatter +# scripts/lint.sh --ci # CI mode (skip clang-tidy) # -# This script is the SINGLE source of truth for CI linting. -# clang-tidy is intentionally excluded (platform-dependent, pre-commit only). +# This script is the SINGLE source of truth for linting. set -euo pipefail @@ -16,15 +16,26 @@ cd "$ROOT" # shellcheck source=env.sh source "$ROOT/scripts/env.sh" -# Forward overrides from args +# Check for --ci flag (skip clang-tidy for platforms without it) +CI_ONLY=false MAKE_ARGS=() for arg in "$@"; do - MAKE_ARGS+=("$arg") + if [ "$arg" = "--ci" ]; then + CI_ONLY=true + else + MAKE_ARGS+=("$arg") + fi done print_env "lint.sh" -# Run format and cppcheck in parallel -make -j2 -f Makefile.cbm lint-ci "${MAKE_ARGS[@]+"${MAKE_ARGS[@]}"}" +if $CI_ONLY; then + echo "=== CI mode: cppcheck + clang-format ===" + $ARCH_PREFIX make -j2 -f Makefile.cbm lint-ci "${MAKE_ARGS[@]+"${MAKE_ARGS[@]}"}" + echo "=== CI linters passed ===" +else + echo "=== Full lint: clang-tidy + cppcheck + clang-format ===" + $ARCH_PREFIX make -j3 -f Makefile.cbm lint "${MAKE_ARGS[@]+"${MAKE_ARGS[@]}"}" +fi echo "=== All linters passed ===" diff --git a/src/cli/cli.c b/src/cli/cli.c index ec59ea0b7..72739a8fd 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -161,63 +161,61 @@ const char *cbm_detect_shell_rc(const char *home_dir) { /* ── CLI binary detection ─────────────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Search for an executable named `name` in the PATH environment variable. + * Returns the full path in `out` (max out_sz) if found, else empty string. */ +static bool find_in_path(const char *name, char *out, size_t out_sz) { + const char *path_env = getenv("PATH"); + if (!path_env) { + return false; + } + char path_copy[4096]; + snprintf(path_copy, sizeof(path_copy), "%s", path_env); + char *saveptr; + char *dir = strtok_r(path_copy, ":", &saveptr); + while (dir) { + snprintf(out, out_sz, "%s/%s", dir, name); + struct stat st; + if (stat(out, &st) == 0 && (st.st_mode & S_IXUSR)) { + return true; + } + dir = strtok_r(NULL, ":", &saveptr); + } + return false; +} + +/* Check if a path exists and is executable. */ +static bool is_executable(const char *path) { + struct stat st; + return stat(path, &st) == 0 && (st.st_mode & S_IXUSR); +} + const char *cbm_find_cli(const char *name, const char *home_dir) { static char buf[512]; if (!name || !name[0]) { return ""; } - - /* Check PATH first */ - const char *path_env = getenv("PATH"); - if (path_env) { - char path_copy[4096]; - snprintf(path_copy, sizeof(path_copy), "%s", path_env); - char *saveptr; - char *dir = strtok_r(path_copy, ":", &saveptr); - while (dir) { - snprintf(buf, sizeof(buf), "%s/%s", dir, name); - struct stat st; - if (stat(buf, &st) == 0 && (st.st_mode & S_IXUSR)) { - return buf; - } - dir = strtok_r(NULL, ":", &saveptr); - } + if (find_in_path(name, buf, sizeof(buf))) { + return buf; } - - /* Check common install locations */ - if (home_dir && home_dir[0]) { - const char *candidates[] = { - "/usr/local/bin/%s", - NULL, /* filled dynamically */ - NULL, - NULL, - NULL, - }; - char paths[5][512]; - snprintf(paths[0], sizeof(paths[0]), "/usr/local/bin/%s", name); - snprintf(paths[1], sizeof(paths[1]), "%s/.npm/bin/%s", home_dir, name); - snprintf(paths[2], sizeof(paths[2]), "%s/.local/bin/%s", home_dir, name); - snprintf(paths[3], sizeof(paths[3]), "%s/.cargo/bin/%s", home_dir, name); + if (!home_dir || !home_dir[0]) { + return ""; + } + char paths[5][512]; + snprintf(paths[0], sizeof(paths[0]), "/usr/local/bin/%s", name); + snprintf(paths[1], sizeof(paths[1]), "%s/.npm/bin/%s", home_dir, name); + snprintf(paths[2], sizeof(paths[2]), "%s/.local/bin/%s", home_dir, name); + snprintf(paths[3], sizeof(paths[3]), "%s/.cargo/bin/%s", home_dir, name); #ifdef __APPLE__ - snprintf(paths[4], sizeof(paths[4]), "/opt/homebrew/bin/%s", name); + snprintf(paths[4], sizeof(paths[4]), "/opt/homebrew/bin/%s", name); #else - paths[4][0] = '\0'; + paths[4][0] = '\0'; #endif - (void)candidates; - - for (int i = 0; i < 5; i++) { - if (!paths[i][0]) { - continue; - } - struct stat st; - if (stat(paths[i], &st) == 0 && (st.st_mode & S_IXUSR)) { - snprintf(buf, sizeof(buf), "%s", paths[i]); - return buf; - } + for (int i = 0; i < 5; i++) { + if (paths[i][0] && is_executable(paths[i])) { + snprintf(buf, sizeof(buf), "%s", paths[i]); + return buf; } } - return ""; } @@ -866,7 +864,11 @@ int cbm_remove_zed_mcp(const char *config_path) { /* ── Agent detection ──────────────────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +static bool dir_exists(const char *path) { + struct stat st; + return stat(path, &st) == 0 && S_ISDIR(st.st_mode); +} + cbm_detected_agents_t cbm_detect_agents(const char *home_dir) { cbm_detected_agents_t agents; memset(&agents, 0, sizeof(agents)); @@ -875,76 +877,45 @@ cbm_detected_agents_t cbm_detect_agents(const char *home_dir) { } char path[1024]; - struct stat st; - /* Claude Code: ~/.claude/ */ snprintf(path, sizeof(path), "%s/.claude", home_dir); - if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { - agents.claude_code = true; - } + agents.claude_code = dir_exists(path); - /* Codex CLI: ~/.codex/ */ snprintf(path, sizeof(path), "%s/.codex", home_dir); - if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { - agents.codex = true; - } + agents.codex = dir_exists(path); - /* Gemini CLI: ~/.gemini/ */ snprintf(path, sizeof(path), "%s/.gemini", home_dir); - if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { - agents.gemini = true; - } + agents.gemini = dir_exists(path); - /* Zed: platform-specific config dir */ #ifdef __APPLE__ snprintf(path, sizeof(path), "%s/Library/Application Support/Zed", home_dir); #else snprintf(path, sizeof(path), "%s/.config/zed", home_dir); #endif - if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { - agents.zed = true; - } + agents.zed = dir_exists(path); - /* OpenCode: binary on PATH */ - const char *oc = cbm_find_cli("opencode", home_dir); - if (oc[0]) { - agents.opencode = true; - } + agents.opencode = cbm_find_cli("opencode", home_dir)[0] != '\0'; - /* Antigravity: ~/.gemini/antigravity/ */ snprintf(path, sizeof(path), "%s/.gemini/antigravity", home_dir); - if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { + if (dir_exists(path)) { agents.antigravity = true; - agents.gemini = true; /* parent dir implies gemini */ + agents.gemini = true; } - /* Aider: binary on PATH */ - const char *ai = cbm_find_cli("aider", home_dir); - if (ai[0]) { - agents.aider = true; - } + agents.aider = cbm_find_cli("aider", home_dir)[0] != '\0'; - /* KiloCode: globalStorage dir */ snprintf(path, sizeof(path), "%s/.config/Code/User/globalStorage/kilocode.kilo-code", home_dir); - if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { - agents.kilocode = true; - } + agents.kilocode = dir_exists(path); - /* VS Code: User config dir */ #ifdef __APPLE__ snprintf(path, sizeof(path), "%s/Library/Application Support/Code/User", home_dir); #else snprintf(path, sizeof(path), "%s/.config/Code/User", home_dir); #endif - if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { - agents.vscode = true; - } + agents.vscode = dir_exists(path); - /* OpenClaw: ~/.openclaw/ dir */ snprintf(path, sizeof(path), "%s/.openclaw", home_dir); - if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { - agents.openclaw = true; - } + agents.openclaw = dir_exists(path); return agents; } @@ -1691,14 +1662,9 @@ int cbm_ensure_path(const char *bin_dir, const char *rc_file, bool dry_run) { /* ── Tar.gz extraction ────────────────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) -unsigned char *cbm_extract_binary_from_targz(const unsigned char *data, int data_len, - int *out_len) { - if (!data || data_len <= 0 || !out_len) { - return NULL; - } - - /* Decompress gzip */ +/* Decompress gzip data into a malloc'd buffer. Returns NULL on failure. + * *out_total receives the decompressed size. Caller must free the result. */ +static unsigned char *gzip_decompress(const unsigned char *data, int data_len, size_t *out_total) { z_stream strm = {0}; strm.next_in = (unsigned char *)(uintptr_t)data; strm.avail_in = (unsigned int)data_len; @@ -1707,7 +1673,6 @@ unsigned char *cbm_extract_binary_from_targz(const unsigned char *data, int data return NULL; } - /* Allocate decompression buffer (up to 500MB) */ size_t buf_cap = (size_t)data_len * 10; if (buf_cap < 4096) { buf_cap = 4096; @@ -1752,58 +1717,82 @@ unsigned char *cbm_extract_binary_from_targz(const unsigned char *data, int data free(decompressed); return NULL; } + *out_total = total; + return decompressed; +} + +/* Check if a tar block is all zeros (end of archive). */ +static bool is_tar_end_of_archive(const unsigned char *hdr) { + for (int i = 0; i < 512; i++) { + if (hdr[i] != 0) { + return false; + } + } + return true; +} + +/* Try to extract the target binary from a tar entry. Returns malloc'd data or NULL. */ +static unsigned char *tar_try_extract_binary(const unsigned char *hdr, char typeflag, + const char *name, const unsigned char *archive, + size_t data_pos, long file_size, size_t total, + int *out_len) { + (void)hdr; + if (typeflag != '0' && typeflag != '\0') { + return NULL; + } + const char *basename = strrchr(name, '/'); + basename = basename ? basename + 1 : name; + if (strncmp(basename, TAR_BINARY_NAME, TAR_BINARY_NAME_LEN) != 0) { + return NULL; + } + if (data_pos + (size_t)file_size > total) { + return NULL; + } + unsigned char *result = malloc((size_t)file_size); + if (!result) { + return NULL; + } + memcpy(result, archive + data_pos, (size_t)file_size); + *out_len = (int)file_size; + return result; +} + +unsigned char *cbm_extract_binary_from_targz(const unsigned char *data, int data_len, + int *out_len) { + if (!data || data_len <= 0 || !out_len) { + return NULL; + } + + size_t total = 0; + unsigned char *decompressed = gzip_decompress(data, data_len, &total); + if (!decompressed) { + return NULL; + } /* Parse tar: find entry starting with "codebase-memory-mcp" */ size_t pos = 0; while (pos + 512 <= total) { - /* Tar header is 512 bytes */ const unsigned char *hdr = decompressed + pos; - /* Check for end-of-archive (two zero blocks) */ - bool all_zero = true; - for (int i = 0; i < 512 && all_zero; i++) { - if (hdr[i] != 0) { - all_zero = false; - } - } - if (all_zero) { + if (is_tar_end_of_archive(hdr)) { break; } - /* Extract filename (bytes 0-99) */ char name[TAR_NAME_LEN] = {0}; memcpy(name, hdr, TAR_NAME_LEN - 1); - - /* Extract size from octal field */ char size_str[TAR_SIZE_LEN] = {0}; memcpy(size_str, hdr + TAR_SIZE_OFFSET, TAR_SIZE_LEN - 1); long file_size = strtol(size_str, NULL, 8); - - /* Extract type flag (byte 156) */ char typeflag = (char)hdr[TAR_TYPE_OFFSET]; + pos += 512; - pos += 512; /* skip header */ - - /* Check if this is a regular file with our binary name */ - if (typeflag == '0' || typeflag == '\0') { - /* Get basename */ - const char *basename = strrchr(name, '/'); - basename = basename ? basename + 1 : name; - - if (strncmp(basename, TAR_BINARY_NAME, TAR_BINARY_NAME_LEN) == 0) { - if (pos + (size_t)file_size <= total) { - unsigned char *result = malloc((size_t)file_size); - if (result) { - memcpy(result, decompressed + pos, (size_t)file_size); - *out_len = (int)file_size; - free(decompressed); - return result; - } - } - } + unsigned char *found = tar_try_extract_binary(hdr, typeflag, name, decompressed, pos, + file_size, total, out_len); + if (found) { + free(decompressed); + return found; } - /* Skip to next 512-byte boundary */ size_t blocks = ((size_t)file_size + TAR_BLOCK_MASK) / TAR_BLOCK_SIZE; pos += blocks * 512; } @@ -1814,30 +1803,74 @@ unsigned char *cbm_extract_binary_from_targz(const unsigned char *data, int data /* ── Zip extraction (in-memory, replaces external unzip) ──────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Zip local file header constants */ +enum { + ZIP_SIG_0 = 0x50, + ZIP_SIG_1 = 0x4B, + ZIP_SIG_2 = 0x03, + ZIP_SIG_3 = 0x04, + ZIP_HDR_SZ = 30, + ZIP_OFF_METHOD = 8, + ZIP_OFF_COMP = 18, + ZIP_OFF_UNCOMP = 22, + ZIP_OFF_NAMELEN = 26, + ZIP_OFF_EXTRALEN = 28, + ZIP_STORED = 0, + ZIP_DEFLATE = 8 +}; +static const uint32_t ZIP_MAX_UNCOMP = 500U * 1024U * 1024U; + +/* Decompress a single zip entry (stored or deflated). Returns malloc'd buffer + * or NULL on failure. *out_len receives the decompressed size. */ +static unsigned char *zip_extract_entry(const unsigned char *file_data, uint16_t method, + uint32_t comp_size, uint32_t uncomp_size, int *out_len) { + if (method == ZIP_STORED) { + if (comp_size > ZIP_MAX_UNCOMP) { + return NULL; + } + unsigned char *out = malloc(comp_size); + if (!out) { + return NULL; + } + memcpy(out, file_data, comp_size); + *out_len = (int)comp_size; + return out; + } + if (method == ZIP_DEFLATE) { + if (uncomp_size > ZIP_MAX_UNCOMP) { + return NULL; + } + unsigned char *out = malloc(uncomp_size); + if (!out) { + return NULL; + } + z_stream strm = {0}; + strm.next_in = (unsigned char *)file_data; + strm.avail_in = comp_size; + strm.next_out = out; + strm.avail_out = uncomp_size; + if (inflateInit2(&strm, -MAX_WBITS) != Z_OK) { + free(out); + return NULL; + } + int ret = inflate(&strm, Z_FINISH); + inflateEnd(&strm); + if (ret != Z_STREAM_END) { + free(out); + return NULL; + } + *out_len = (int)strm.total_out; + return out; + } + return NULL; /* unknown method */ +} + unsigned char *cbm_extract_binary_from_zip(const unsigned char *data, int data_len, int *out_len) { if (!data || data_len <= 0 || !out_len) { return NULL; } *out_len = 0; - /* Zip local file header layout */ - enum { - ZIP_SIG_0 = 0x50, - ZIP_SIG_1 = 0x4B, - ZIP_SIG_2 = 0x03, - ZIP_SIG_3 = 0x04, - ZIP_HDR_SZ = 30, - ZIP_OFF_METHOD = 8, - ZIP_OFF_COMP = 18, - ZIP_OFF_UNCOMP = 22, - ZIP_OFF_NAMELEN = 26, - ZIP_OFF_EXTRALEN = 28, - ZIP_STORED = 0, - ZIP_DEFLATE = 8 - }; - static const uint32_t ZIP_MAX_UNCOMP = 500U * 1024U * 1024U; - int pos = 0; while (pos + ZIP_HDR_SZ <= data_len) { if (data[pos] != ZIP_SIG_0 || data[pos + 1] != ZIP_SIG_1 || data[pos + 2] != ZIP_SIG_2 || @@ -1861,78 +1894,31 @@ unsigned char *cbm_extract_binary_from_zip(const unsigned char *data, int data_l int header_end = pos + ZIP_HDR_SZ + name_len + extra_len; if (header_end + (int)comp_size > data_len) { - break; /* Truncated */ + break; } - /* Extract filename */ char fname[512] = {0}; int fn_copy = name_len < (int)sizeof(fname) - 1 ? name_len : (int)sizeof(fname) - 1; memcpy(fname, data + pos + 30, (size_t)fn_copy); fname[fn_copy] = '\0'; - /* Security: reject path traversal */ if (strstr(fname, "..")) { pos = header_end + (int)comp_size; continue; } - /* Find the binary — basename match */ const char *basename = strrchr(fname, '/'); basename = basename ? basename + 1 : fname; if (strcmp(basename, "codebase-memory-mcp") == 0 || strcmp(basename, "codebase-memory-mcp.exe") == 0) { - - const unsigned char *file_data = data + header_end; - - if (method == ZIP_STORED) { - if (comp_size > ZIP_MAX_UNCOMP) { - return NULL; - } - unsigned char *out = malloc(comp_size); - if (!out) { - return NULL; - } - memcpy(out, file_data, comp_size); - *out_len = (int)comp_size; - return out; - } - if (method == ZIP_DEFLATE) { - if (uncomp_size > ZIP_MAX_UNCOMP) { - return NULL; - } - unsigned char *out = malloc(uncomp_size); - if (!out) { - return NULL; - } - z_stream strm = {0}; - strm.next_in = (unsigned char *)file_data; - strm.avail_in = comp_size; - strm.next_out = out; - strm.avail_out = uncomp_size; - - /* -MAX_WBITS = raw deflate (no zlib/gzip header) */ - if (inflateInit2(&strm, -MAX_WBITS) != Z_OK) { - free(out); - return NULL; - } - int ret = inflate(&strm, Z_FINISH); - inflateEnd(&strm); - if (ret != Z_STREAM_END) { - free(out); - return NULL; - } - *out_len = (int)strm.total_out; - return out; - } - /* Unknown compression method */ - return NULL; + return zip_extract_entry(data + header_end, method, comp_size, uncomp_size, out_len); } pos = header_end + (int)comp_size; } - return NULL; /* binary not found */ + return NULL; } /* ── Index management ─────────────────────────────────────────── */ @@ -2448,242 +2434,226 @@ static const char *detect_arch(void) { /* ── Agent config install/refresh (shared by install + update) ── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) -static void cbm_install_agent_configs(const char *home, const char *binary_path, bool force, - bool dry_run) { - cbm_detected_agents_t agents = cbm_detect_agents(home); +/* Print detected agent names on a single line. */ +static void print_detected_agents(const cbm_detected_agents_t *a) { + struct { + bool flag; + const char *name; + } agents[] = { + {a->claude_code, "Claude-Code"}, + {a->codex, "Codex"}, + {a->gemini, "Gemini-CLI"}, + {a->zed, "Zed"}, + {a->opencode, "OpenCode"}, + {a->antigravity, "Antigravity"}, + {a->aider, "Aider"}, + {a->kilocode, "KiloCode"}, + {a->vscode, "VS-Code"}, + {a->openclaw, "OpenClaw"}, + }; printf("Detected agents:"); - if (agents.claude_code) { - printf(" Claude-Code"); + bool any = false; + for (int i = 0; i < (int)(sizeof(agents) / sizeof(agents[0])); i++) { + if (agents[i].flag) { + printf(" %s", agents[i].name); + any = true; + } } - if (agents.codex) { - printf(" Codex"); - } - if (agents.gemini) { - printf(" Gemini-CLI"); - } - if (agents.zed) { - printf(" Zed"); - } - if (agents.opencode) { - printf(" OpenCode"); - } - if (agents.antigravity) { - printf(" Antigravity"); - } - if (agents.aider) { - printf(" Aider"); - } - if (agents.kilocode) { - printf(" KiloCode"); - } - if (agents.vscode) { - printf(" VS-Code"); - } - if (agents.openclaw) { - printf(" OpenClaw"); - } - if (!agents.claude_code && !agents.codex && !agents.gemini && !agents.zed && !agents.opencode && - !agents.antigravity && !agents.aider && !agents.kilocode && !agents.vscode && - !agents.openclaw) { + if (!any) { printf(" (none)"); } printf("\n\n"); +} - /* Claude Code: skills + MCP + hooks */ - if (agents.claude_code) { - char skills_dir[1024]; - snprintf(skills_dir, sizeof(skills_dir), "%s/.claude/skills", home); - printf("Claude Code:\n"); +/* Install Claude Code-specific configs (skills, MCP, hooks). */ +static void install_claude_code_config(const char *home, const char *binary_path, bool force, + bool dry_run) { + char skills_dir[1024]; + snprintf(skills_dir, sizeof(skills_dir), "%s/.claude/skills", home); + printf("Claude Code:\n"); - int skill_count = cbm_install_skills(skills_dir, force, dry_run); - printf(" skills: %d installed\n", skill_count); + int skill_count = cbm_install_skills(skills_dir, force, dry_run); + printf(" skills: %d installed\n", skill_count); - if (cbm_remove_old_monolithic_skill(skills_dir, dry_run)) { - printf(" removed old monolithic skill\n"); - } - - char mcp_path[1024]; - snprintf(mcp_path, sizeof(mcp_path), "%s/.claude/.mcp.json", home); - if (!dry_run) { - cbm_install_editor_mcp(binary_path, mcp_path); - } - printf(" mcp: %s\n", mcp_path); - - char mcp_path2[1024]; - snprintf(mcp_path2, sizeof(mcp_path2), "%s/.claude.json", home); - if (!dry_run) { - cbm_install_editor_mcp(binary_path, mcp_path2); - } - printf(" mcp: %s\n", mcp_path2); - - char settings_path[1024]; - snprintf(settings_path, sizeof(settings_path), "%s/.claude/settings.json", home); - if (!dry_run) { - cbm_upsert_claude_hooks(settings_path); - cbm_install_hook_gate_script(home); - cbm_install_session_reminder_script(home); - cbm_upsert_session_hooks(settings_path); - } - printf(" hooks: PreToolUse (code discovery gate)\n"); - printf(" hooks: SessionStart (MCP usage reminder on startup/resume/clear/compact)\n"); + if (cbm_remove_old_monolithic_skill(skills_dir, dry_run)) { + printf(" removed old monolithic skill\n"); } - /* Codex CLI */ - if (agents.codex) { - printf("Codex CLI:\n"); - char config_path[1024]; - snprintf(config_path, sizeof(config_path), "%s/.codex/config.toml", home); - if (!dry_run) { - cbm_upsert_codex_mcp(binary_path, config_path); - } - printf(" mcp: %s\n", config_path); + char mcp_path[1024]; + snprintf(mcp_path, sizeof(mcp_path), "%s/.claude/.mcp.json", home); + if (!dry_run) { + cbm_install_editor_mcp(binary_path, mcp_path); + } + printf(" mcp: %s\n", mcp_path); - char instr_path[1024]; - snprintf(instr_path, sizeof(instr_path), "%s/.codex/AGENTS.md", home); + char mcp_path2[1024]; + snprintf(mcp_path2, sizeof(mcp_path2), "%s/.claude.json", home); + if (!dry_run) { + cbm_install_editor_mcp(binary_path, mcp_path2); + } + printf(" mcp: %s\n", mcp_path2); + + char settings_path[1024]; + snprintf(settings_path, sizeof(settings_path), "%s/.claude/settings.json", home); + if (!dry_run) { + cbm_upsert_claude_hooks(settings_path); + cbm_install_hook_gate_script(home); + cbm_install_session_reminder_script(home); + cbm_upsert_session_hooks(settings_path); + } + printf(" hooks: PreToolUse (code discovery gate)\n"); + printf(" hooks: SessionStart (MCP usage reminder on startup/resume/clear/compact)\n"); +} + +/* Install MCP config + optional instructions for a generic agent. */ +static void install_generic_agent_config(const char *label, const char *binary_path, + const char *config_path, const char *instr_path, + bool dry_run, + int (*install_mcp)(const char *, const char *)) { + printf("%s:\n", label); + if (!dry_run) { + install_mcp(binary_path, config_path); + } + printf(" mcp: %s\n", config_path); + if (instr_path) { if (!dry_run) { cbm_upsert_instructions(instr_path, agent_instructions_content); } printf(" instructions: %s\n", instr_path); } +} - /* Gemini CLI */ - if (agents.gemini) { - printf("Gemini CLI:\n"); - char config_path[1024]; - snprintf(config_path, sizeof(config_path), "%s/.gemini/settings.json", home); - if (!dry_run) { - cbm_install_editor_mcp(binary_path, config_path); - } - printf(" mcp: %s\n", config_path); - - char instr_path[1024]; - snprintf(instr_path, sizeof(instr_path), "%s/.gemini/GEMINI.md", home); - if (!dry_run) { - cbm_upsert_instructions(instr_path, agent_instructions_content); - } - printf(" instructions: %s\n", instr_path); - - if (!dry_run) { - cbm_upsert_gemini_hooks(config_path); - } - printf(" hooks: BeforeTool (grep/file search reminder)\n"); +/* Install MCP configs for CLI-based agents (Codex, Gemini, OpenCode, Antigravity, Aider). */ +/* Install Gemini CLI config with hooks. */ +static void install_gemini_config(const char *home, const char *binary_path, bool dry_run) { + char cp[1024]; + char ip[1024]; + snprintf(cp, sizeof(cp), "%s/.gemini/settings.json", home); + snprintf(ip, sizeof(ip), "%s/.gemini/GEMINI.md", home); + install_generic_agent_config("Gemini CLI", binary_path, cp, ip, dry_run, + cbm_install_editor_mcp); + if (!dry_run) { + cbm_upsert_gemini_hooks(cp); } + printf(" hooks: BeforeTool (grep/file search reminder)\n"); +} - /* Zed */ - if (agents.zed) { - printf("Zed:\n"); - char config_path[1024]; -#ifdef __APPLE__ - snprintf(config_path, sizeof(config_path), - "%s/Library/Application Support/Zed/settings.json", home); -#else - snprintf(config_path, sizeof(config_path), "%s/.config/zed/settings.json", home); -#endif - if (!dry_run) { - cbm_install_zed_mcp(binary_path, config_path); - } - printf(" mcp: %s\n", config_path); +static void install_cli_agent_configs(const cbm_detected_agents_t *agents, const char *home, + const char *binary_path, bool dry_run) { + if (agents->codex) { + char cp[1024]; + char ip[1024]; + snprintf(cp, sizeof(cp), "%s/.codex/config.toml", home); + snprintf(ip, sizeof(ip), "%s/.codex/AGENTS.md", home); + install_generic_agent_config("Codex CLI", binary_path, cp, ip, dry_run, + cbm_upsert_codex_mcp); } - - /* OpenCode */ - if (agents.opencode) { - printf("OpenCode:\n"); - char config_path[1024]; - snprintf(config_path, sizeof(config_path), "%s/.config/opencode/opencode.json", home); - if (!dry_run) { - cbm_upsert_opencode_mcp(binary_path, config_path); - } - printf(" mcp: %s\n", config_path); - - char instr_path[1024]; - snprintf(instr_path, sizeof(instr_path), "%s/.config/opencode/AGENTS.md", home); - if (!dry_run) { - cbm_upsert_instructions(instr_path, agent_instructions_content); - } - printf(" instructions: %s\n", instr_path); + if (agents->gemini) { + install_gemini_config(home, binary_path, dry_run); } - - /* Antigravity */ - if (agents.antigravity) { - printf("Antigravity:\n"); - char config_path[1024]; - snprintf(config_path, sizeof(config_path), "%s/.gemini/antigravity/mcp_config.json", home); - if (!dry_run) { - cbm_upsert_antigravity_mcp(binary_path, config_path); - } - printf(" mcp: %s\n", config_path); - - char instr_path[1024]; - snprintf(instr_path, sizeof(instr_path), "%s/.gemini/antigravity/AGENTS.md", home); - if (!dry_run) { - cbm_upsert_instructions(instr_path, agent_instructions_content); - } - printf(" instructions: %s\n", instr_path); + if (agents->opencode) { + char cp[1024]; + char ip[1024]; + snprintf(cp, sizeof(cp), "%s/.config/opencode/opencode.json", home); + snprintf(ip, sizeof(ip), "%s/.config/opencode/AGENTS.md", home); + install_generic_agent_config("OpenCode", binary_path, cp, ip, dry_run, + cbm_upsert_opencode_mcp); } - - /* Aider */ - if (agents.aider) { + if (agents->antigravity) { + char cp[1024]; + char ip[1024]; + snprintf(cp, sizeof(cp), "%s/.gemini/antigravity/mcp_config.json", home); + snprintf(ip, sizeof(ip), "%s/.gemini/antigravity/AGENTS.md", home); + install_generic_agent_config("Antigravity", binary_path, cp, ip, dry_run, + cbm_upsert_antigravity_mcp); + } + if (agents->aider) { + char ip[1024]; + snprintf(ip, sizeof(ip), "%s/CONVENTIONS.md", home); printf("Aider:\n"); - char instr_path[1024]; - snprintf(instr_path, sizeof(instr_path), "%s/CONVENTIONS.md", home); if (!dry_run) { - cbm_upsert_instructions(instr_path, agent_instructions_content); + cbm_upsert_instructions(ip, agent_instructions_content); } - printf(" instructions: %s\n", instr_path); + printf(" instructions: %s\n", ip); } +} - /* KiloCode */ - if (agents.kilocode) { - printf("KiloCode:\n"); - char config_path[1024]; - snprintf(config_path, sizeof(config_path), +/* Install MCP configs for editor-based agents (Zed, KiloCode, VS Code, OpenClaw). */ +static void install_editor_agent_configs(const cbm_detected_agents_t *agents, const char *home, + const char *binary_path, bool dry_run) { + if (agents->zed) { + char cp[1024]; +#ifdef __APPLE__ + snprintf(cp, sizeof(cp), "%s/Library/Application Support/Zed/settings.json", home); +#else + snprintf(cp, sizeof(cp), "%s/.config/zed/settings.json", home); +#endif + install_generic_agent_config("Zed", binary_path, cp, NULL, dry_run, cbm_install_zed_mcp); + } + if (agents->kilocode) { + char cp[1024]; + char ip[1024]; + snprintf(cp, sizeof(cp), "%s/.config/Code/User/globalStorage/kilocode.kilo-code/settings/mcp_settings.json", home); - if (!dry_run) { - cbm_install_editor_mcp(binary_path, config_path); - } - printf(" mcp: %s\n", config_path); - - char instr_path[1024]; - snprintf(instr_path, sizeof(instr_path), "%s/.kilocode/rules/codebase-memory-mcp.md", home); - if (!dry_run) { - cbm_upsert_instructions(instr_path, agent_instructions_content); - } - printf(" instructions: %s\n", instr_path); + snprintf(ip, sizeof(ip), "%s/.kilocode/rules/codebase-memory-mcp.md", home); + install_generic_agent_config("KiloCode", binary_path, cp, ip, dry_run, + cbm_install_editor_mcp); } - - /* VS Code */ - if (agents.vscode) { - printf("VS Code:\n"); - char config_path[1024]; + if (agents->vscode) { + char cp[1024]; #ifdef __APPLE__ - snprintf(config_path, sizeof(config_path), - "%s/Library/Application Support/Code/User/mcp.json", home); + snprintf(cp, sizeof(cp), "%s/Library/Application Support/Code/User/mcp.json", home); #else - snprintf(config_path, sizeof(config_path), "%s/.config/Code/User/mcp.json", home); + snprintf(cp, sizeof(cp), "%s/.config/Code/User/mcp.json", home); #endif - if (!dry_run) { - cbm_install_vscode_mcp(binary_path, config_path); - } - printf(" mcp: %s\n", config_path); + install_generic_agent_config("VS Code", binary_path, cp, NULL, dry_run, + cbm_install_vscode_mcp); } + if (agents->openclaw) { + char cp[1024]; + snprintf(cp, sizeof(cp), "%s/.openclaw/openclaw.json", home); + install_generic_agent_config("OpenClaw", binary_path, cp, NULL, dry_run, + cbm_install_editor_mcp); + } +} - /* OpenClaw */ - if (agents.openclaw) { - printf("OpenClaw:\n"); - char config_path[1024]; - snprintf(config_path, sizeof(config_path), "%s/.openclaw/openclaw.json", home); - if (!dry_run) { - cbm_install_editor_mcp(binary_path, config_path); - } - printf(" mcp: %s\n", config_path); +static void cbm_install_agent_configs(const char *home, const char *binary_path, bool force, + bool dry_run) { + cbm_detected_agents_t agents = cbm_detect_agents(home); + print_detected_agents(&agents); + + if (agents.claude_code) { + install_claude_code_config(home, binary_path, force, dry_run); } + install_cli_agent_configs(&agents, home, binary_path, dry_run); + install_editor_agent_configs(&agents, home, binary_path, dry_run); +} + +/* Count .db files in the cache directory. */ +static int count_db_indexes(const char *home) { + const char *cache_dir = get_cache_dir(home); + if (!cache_dir) { + return 0; + } + cbm_dir_t *d = cbm_opendir(cache_dir); + if (!d) { + return 0; + } + int count = 0; + cbm_dirent_t *ent; + while ((ent = cbm_readdir(d)) != NULL) { + size_t len = strlen(ent->name); + if (len > 3 && strcmp(ent->name + len - 3, ".db") == 0) { + count++; + } + } + cbm_closedir(d); + return count; } /* ── Subcommand: install ──────────────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) int cbm_cmd_install(int argc, char **argv) { parse_auto_answer(argc, argv); bool dry_run = false; @@ -2705,23 +2675,7 @@ int cbm_cmd_install(int argc, char **argv) { printf("codebase-memory-mcp install %s\n\n", CBM_VERSION); - /* Step 1: Check for existing indexes */ - int index_count = 0; - const char *cache_dir = get_cache_dir(home); - if (cache_dir) { - cbm_dir_t *d = cbm_opendir(cache_dir); - if (d) { - cbm_dirent_t *ent; - while ((ent = cbm_readdir(d)) != NULL) { - size_t len = strlen(ent->name); - if (len > 3 && strcmp(ent->name + len - 3, ".db") == 0) { - index_count++; - } - } - cbm_closedir(d); - } - } - + int index_count = count_db_indexes(home); if (index_count > 0) { printf("Found %d existing index(es) that must be rebuilt:\n", index_count); cbm_list_indexes(home); @@ -2790,7 +2744,139 @@ int cbm_cmd_install(int argc, char **argv) { /* ── Subcommand: uninstall ────────────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Remove Claude Code agent configs. */ +static void uninstall_claude_code(const char *home, bool dry_run) { + char skills_dir[1024]; + snprintf(skills_dir, sizeof(skills_dir), "%s/.claude/skills", home); + int removed = cbm_remove_skills(skills_dir, dry_run); + printf("Claude Code: removed %d skill(s)\n", removed); + + char mcp_path[1024]; + snprintf(mcp_path, sizeof(mcp_path), "%s/.claude/.mcp.json", home); + if (!dry_run) { + cbm_remove_editor_mcp(mcp_path); + } + printf(" removed MCP config entry\n"); + + char mcp_path2[1024]; + snprintf(mcp_path2, sizeof(mcp_path2), "%s/.claude.json", home); + if (!dry_run) { + cbm_remove_editor_mcp(mcp_path2); + } + + char settings_path[1024]; + snprintf(settings_path, sizeof(settings_path), "%s/.claude/settings.json", home); + if (!dry_run) { + cbm_remove_claude_hooks(settings_path); + cbm_remove_session_hooks(settings_path); + } + printf(" removed PreToolUse + SessionStart hooks\n"); +} + +/* Remove MCP + instructions for a generic agent. */ +static void uninstall_agent_mcp_instr(const char *name, const char *config_path, + const char *instr_path, bool dry_run, + int (*remove_fn)(const char *)) { + if (!dry_run) { + remove_fn(config_path); + } + printf("%s: removed MCP config entry\n", name); + if (instr_path) { + if (!dry_run) { + cbm_remove_instructions(instr_path); + } + printf(" removed instructions\n"); + } +} + +/* Remove CLI agent configs (Codex, Gemini, OpenCode, Antigravity, Aider). */ +/* Uninstall Gemini CLI config + hooks. */ +static void uninstall_gemini_config(const char *home, bool dry_run) { + char cp[1024]; + char ip[1024]; + snprintf(cp, sizeof(cp), "%s/.gemini/settings.json", home); + snprintf(ip, sizeof(ip), "%s/.gemini/GEMINI.md", home); + if (!dry_run) { + cbm_remove_editor_mcp(cp); + cbm_remove_gemini_hooks(cp); + cbm_remove_instructions(ip); + } + printf("Gemini CLI: removed MCP config + hooks + instructions\n"); +} + +static void uninstall_cli_agents(const cbm_detected_agents_t *agents, const char *home, + bool dry_run) { + if (agents->codex) { + char cp[1024]; + char ip[1024]; + snprintf(cp, sizeof(cp), "%s/.codex/config.toml", home); + snprintf(ip, sizeof(ip), "%s/.codex/AGENTS.md", home); + uninstall_agent_mcp_instr("Codex CLI", cp, ip, dry_run, cbm_remove_codex_mcp); + } + if (agents->gemini) { + uninstall_gemini_config(home, dry_run); + } + if (agents->opencode) { + char cp[1024]; + char ip[1024]; + snprintf(cp, sizeof(cp), "%s/.config/opencode/opencode.json", home); + snprintf(ip, sizeof(ip), "%s/.config/opencode/AGENTS.md", home); + uninstall_agent_mcp_instr("OpenCode", cp, ip, dry_run, cbm_remove_opencode_mcp); + } + if (agents->antigravity) { + char cp[1024]; + char ip[1024]; + snprintf(cp, sizeof(cp), "%s/.gemini/antigravity/mcp_config.json", home); + snprintf(ip, sizeof(ip), "%s/.gemini/antigravity/AGENTS.md", home); + uninstall_agent_mcp_instr("Antigravity", cp, ip, dry_run, cbm_remove_antigravity_mcp); + } + if (agents->aider) { + char ip[1024]; + snprintf(ip, sizeof(ip), "%s/CONVENTIONS.md", home); + if (!dry_run) { + cbm_remove_instructions(ip); + } + printf("Aider: removed instructions\n"); + } +} + +/* Remove editor agent configs (Zed, KiloCode, VS Code, OpenClaw). */ +static void uninstall_editor_agents(const cbm_detected_agents_t *agents, const char *home, + bool dry_run) { + if (agents->zed) { + char cp[1024]; +#ifdef __APPLE__ + snprintf(cp, sizeof(cp), "%s/Library/Application Support/Zed/settings.json", home); +#else + snprintf(cp, sizeof(cp), "%s/.config/zed/settings.json", home); +#endif + uninstall_agent_mcp_instr("Zed", cp, NULL, dry_run, cbm_remove_zed_mcp); + } + if (agents->kilocode) { + char cp[1024]; + char ip[1024]; + snprintf(cp, sizeof(cp), + "%s/.config/Code/User/globalStorage/kilocode.kilo-code/settings/mcp_settings.json", + home); + snprintf(ip, sizeof(ip), "%s/.kilocode/rules/codebase-memory-mcp.md", home); + uninstall_agent_mcp_instr("KiloCode", cp, ip, dry_run, cbm_remove_editor_mcp); + } + if (agents->vscode) { + char cp[1024]; +#ifdef __APPLE__ + snprintf(cp, sizeof(cp), "%s/Library/Application Support/Code/User/mcp.json", home); +#else + snprintf(cp, sizeof(cp), "%s/.config/Code/User/mcp.json", home); +#endif + uninstall_agent_mcp_instr("VS Code", cp, NULL, dry_run, cbm_remove_vscode_mcp); + } + if (agents->openclaw) { + char cp[1024]; + snprintf(cp, sizeof(cp), "%s/.openclaw/openclaw.json", home); + uninstall_agent_mcp_instr("OpenClaw", cp, NULL, dry_run, cbm_remove_editor_mcp); + } +} + int cbm_cmd_uninstall(int argc, char **argv) { parse_auto_answer(argc, argv); bool dry_run = false; @@ -2808,188 +2894,15 @@ int cbm_cmd_uninstall(int argc, char **argv) { printf("codebase-memory-mcp uninstall\n\n"); - /* Step 1: Detect agents and remove per-agent configs */ cbm_detected_agents_t agents = cbm_detect_agents(home); - if (agents.claude_code) { - char skills_dir[1024]; - snprintf(skills_dir, sizeof(skills_dir), "%s/.claude/skills", home); - int removed = cbm_remove_skills(skills_dir, dry_run); - printf("Claude Code: removed %d skill(s)\n", removed); - - char mcp_path[1024]; - snprintf(mcp_path, sizeof(mcp_path), "%s/.claude/.mcp.json", home); - if (!dry_run) { - cbm_remove_editor_mcp(mcp_path); - } - printf(" removed MCP config entry\n"); - - /* Also remove from new location (Claude Code >=2.1.80) */ - char mcp_path2[1024]; - snprintf(mcp_path2, sizeof(mcp_path2), "%s/.claude.json", home); - if (!dry_run) { - cbm_remove_editor_mcp(mcp_path2); - } - - char settings_path[1024]; - snprintf(settings_path, sizeof(settings_path), "%s/.claude/settings.json", home); - if (!dry_run) { - cbm_remove_claude_hooks(settings_path); - cbm_remove_session_hooks(settings_path); - } - printf(" removed PreToolUse + SessionStart hooks\n"); - } - - if (agents.codex) { - char config_path[1024]; - snprintf(config_path, sizeof(config_path), "%s/.codex/config.toml", home); - if (!dry_run) { - cbm_remove_codex_mcp(config_path); - } - printf("Codex CLI: removed MCP config entry\n"); - - char instr_path[1024]; - snprintf(instr_path, sizeof(instr_path), "%s/.codex/AGENTS.md", home); - if (!dry_run) { - cbm_remove_instructions(instr_path); - } - printf(" removed instructions\n"); - } - - if (agents.gemini) { - char config_path[1024]; - snprintf(config_path, sizeof(config_path), "%s/.gemini/settings.json", home); - if (!dry_run) { - cbm_remove_editor_mcp(config_path); - } - printf("Gemini CLI: removed MCP config entry\n"); - - if (!dry_run) { - cbm_remove_gemini_hooks(config_path); - } - printf(" removed BeforeTool hook\n"); - - char instr_path[1024]; - snprintf(instr_path, sizeof(instr_path), "%s/.gemini/GEMINI.md", home); - if (!dry_run) { - cbm_remove_instructions(instr_path); - } - printf(" removed instructions\n"); - } - - if (agents.zed) { - char config_path[1024]; -#ifdef __APPLE__ - snprintf(config_path, sizeof(config_path), - "%s/Library/Application Support/Zed/settings.json", home); -#else - snprintf(config_path, sizeof(config_path), "%s/.config/zed/settings.json", home); -#endif - if (!dry_run) { - cbm_remove_zed_mcp(config_path); - } - printf("Zed: removed MCP config entry\n"); - } - - if (agents.opencode) { - char config_path[1024]; - snprintf(config_path, sizeof(config_path), "%s/.config/opencode/opencode.json", home); - if (!dry_run) { - cbm_remove_opencode_mcp(config_path); - } - printf("OpenCode: removed MCP config entry\n"); - - char instr_path[1024]; - snprintf(instr_path, sizeof(instr_path), "%s/.config/opencode/AGENTS.md", home); - if (!dry_run) { - cbm_remove_instructions(instr_path); - } - printf(" removed instructions\n"); - } - - if (agents.antigravity) { - char config_path[1024]; - snprintf(config_path, sizeof(config_path), "%s/.gemini/antigravity/mcp_config.json", home); - if (!dry_run) { - cbm_remove_antigravity_mcp(config_path); - } - printf("Antigravity: removed MCP config entry\n"); - - char instr_path[1024]; - snprintf(instr_path, sizeof(instr_path), "%s/.gemini/antigravity/AGENTS.md", home); - if (!dry_run) { - cbm_remove_instructions(instr_path); - } - printf(" removed instructions\n"); - } - - if (agents.aider) { - char instr_path[1024]; - snprintf(instr_path, sizeof(instr_path), "%s/CONVENTIONS.md", home); - if (!dry_run) { - cbm_remove_instructions(instr_path); - } - printf("Aider: removed instructions\n"); - } - - if (agents.kilocode) { - char config_path[1024]; - snprintf(config_path, sizeof(config_path), - "%s/.config/Code/User/globalStorage/kilocode.kilo-code/settings/mcp_settings.json", - home); - if (!dry_run) { - cbm_remove_editor_mcp(config_path); - } - printf("KiloCode: removed MCP config entry\n"); - - char instr_path[1024]; - snprintf(instr_path, sizeof(instr_path), "%s/.kilocode/rules/codebase-memory-mcp.md", home); - if (!dry_run) { - cbm_remove_instructions(instr_path); - } - printf(" removed instructions\n"); - } - - if (agents.vscode) { - char config_path[1024]; -#ifdef __APPLE__ - snprintf(config_path, sizeof(config_path), - "%s/Library/Application Support/Code/User/mcp.json", home); -#else - snprintf(config_path, sizeof(config_path), "%s/.config/Code/User/mcp.json", home); -#endif - if (!dry_run) { - cbm_remove_vscode_mcp(config_path); - } - printf("VS Code: removed MCP config entry\n"); - } - - if (agents.openclaw) { - char config_path[1024]; - snprintf(config_path, sizeof(config_path), "%s/.openclaw/openclaw.json", home); - if (!dry_run) { - cbm_remove_editor_mcp(config_path); - } - printf("OpenClaw: removed MCP config entry\n"); + uninstall_claude_code(home, dry_run); } + uninstall_cli_agents(&agents, home, dry_run); + uninstall_editor_agents(&agents, home, dry_run); /* Step 2: Remove indexes */ - int index_count = 0; - const char *cache_dir = get_cache_dir(home); - if (cache_dir) { - cbm_dir_t *d = cbm_opendir(cache_dir); - if (d) { - cbm_dirent_t *ent; - while ((ent = cbm_readdir(d)) != NULL) { - size_t len = strlen(ent->name); - if (len > 3 && strcmp(ent->name + len - 3, ".db") == 0) { - index_count++; - } - } - cbm_closedir(d); - } - } - + int index_count = count_db_indexes(home); if (index_count > 0) { printf("\nFound %d index(es):\n", index_count); cbm_list_indexes(home); @@ -3025,7 +2938,147 @@ int cbm_cmd_uninstall(int argc, char **argv) { /* ── Subcommand: update ───────────────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity,readability-function-size) +/* Read archive from disk, extract binary (tar.gz or zip), write to bin_dest. + * Returns 0 on success, 1 on failure. Cleans up tmp_archive. */ +static int extract_and_install_binary(const char *tmp_archive, const char *ext, + const char *bin_dest) { + FILE *f = fopen(tmp_archive, "rb"); + if (!f) { + fprintf(stderr, "error: cannot open %s\n", tmp_archive); + return 1; + } + fseek(f, 0, SEEK_END); + long fsize = ftell(f); + fseek(f, 0, SEEK_SET); + + unsigned char *data = malloc((size_t)fsize); + if (!data) { + fclose(f); + cbm_unlink(tmp_archive); + return 1; + } + fread(data, 1, (size_t)fsize, f); + fclose(f); + + int bin_len = 0; + unsigned char *bin_data = NULL; + if (strcmp(ext, "tar.gz") == 0) { + bin_data = cbm_extract_binary_from_targz(data, (int)fsize, &bin_len); + } else { + bin_data = cbm_extract_binary_from_zip(data, (int)fsize, &bin_len); + } + free(data); + cbm_unlink(tmp_archive); + + if (!bin_data || bin_len <= 0) { + fprintf(stderr, "error: binary not found in archive\n"); + free(bin_data); + return 1; + } + + if (cbm_replace_binary(bin_dest, bin_data, bin_len, 0755) != 0) { + fprintf(stderr, "error: cannot write to %s\n", bin_dest); + free(bin_data); + return 1; + } + free(bin_data); + return 0; +} + +/* Build the download URL for the update command. */ +static void build_update_url(char *url, int url_sz, const char *os, const char *arch, + const char *ext, bool want_ui) { + const char *base_url = getenv("CBM_DOWNLOAD_URL"); + if (!base_url || !base_url[0]) { + base_url = "https://github.com/DeusData/codebase-memory-mcp/releases/latest/download"; + } + snprintf(url, url_sz, "%s/codebase-memory-mcp-%s%s-%s.%s", base_url, want_ui ? "ui-" : "", os, + arch, ext); +} + +/* Prompt to delete existing indexes. Returns 0 to continue, 1 to abort. */ +static int update_clear_indexes(const char *home, bool dry_run) { + int index_count = count_db_indexes(home); + if (index_count == 0) { + return 0; + } + printf("Found %d existing index(es) that must be rebuilt after update:\n", index_count); + cbm_list_indexes(home); + printf("\n"); + if (dry_run) { + printf("(dry-run — indexes would be deleted)\n\n"); + return 0; + } + if (!prompt_yn("Delete these indexes and continue with update?")) { + printf("Update cancelled.\n"); + return 1; + } + int removed = cbm_remove_indexes(home); + printf("Removed %d index(es).\n\n", removed); + return 0; +} + +/* Download, verify checksum, kill old instances, and install binary. Returns 0 on success. */ +static int download_verify_install(const char *url, const char *ext, const char *os, + const char *arch, bool want_ui, const char *bin_dest) { + char tmp_archive[256]; + snprintf(tmp_archive, sizeof(tmp_archive), "%s/cbm-update.%s", cbm_tmpdir(), ext); + + int rc = cbm_download_to_file(url, tmp_archive); + if (rc != 0) { + fprintf(stderr, "error: download failed (exit %d)\n", rc); + cbm_unlink(tmp_archive); + return 1; + } + + char archive_name[256]; + snprintf(archive_name, sizeof(archive_name), "codebase-memory-mcp-%s%s-%s.%s", + want_ui ? "ui-" : "", os, arch, ext); + int crc = verify_download_checksum(tmp_archive, archive_name); + if (crc == 1) { + cbm_unlink(tmp_archive); + return 1; + } + + int killed = cbm_kill_other_instances(); + if (killed > 0) { + printf("Stopped %d running MCP server instance(s).\n", killed); + } + + if (extract_and_install_binary(tmp_archive, ext, bin_dest) != 0) { + return 1; + } + return 0; +} + +/* Select update variant. Returns 0=standard, 1=ui, -1=error. */ +static int select_update_variant(int variant_flag) { + if (variant_flag == 1) { + return 0; + } + if (variant_flag == 2) { + return 1; + } +#ifndef _WIN32 + if (!isatty(fileno(stdin))) { + fprintf(stderr, "error: variant selection requires a terminal. " + "Use --standard or --ui flag.\n"); + return -1; + } +#endif + printf("Which binary variant do you want?\n"); + printf(" 1) standard — MCP server only\n"); + printf(" 2) ui — MCP server + embedded graph visualization\n"); + printf("Choose (1/2): "); + (void)fflush(stdout); + char choice[16]; + if (!fgets(choice, sizeof(choice), stdin)) { + fprintf(stderr, "error: failed to read input\n"); + return -1; + } + return (choice[0] == '2') ? 1 : 0; +} + int cbm_cmd_update(int argc, char **argv) { parse_auto_answer(argc, argv); @@ -3050,84 +3103,25 @@ int cbm_cmd_update(int argc, char **argv) { printf("codebase-memory-mcp update (current: %s)\n\n", CBM_VERSION); /* Step 1: Check for existing indexes */ - int index_count = 0; - const char *cache_dir = get_cache_dir(home); - if (cache_dir) { - cbm_dir_t *d = cbm_opendir(cache_dir); - if (d) { - cbm_dirent_t *ent; - while ((ent = cbm_readdir(d)) != NULL) { - size_t len = strlen(ent->name); - if (len > 3 && strcmp(ent->name + len - 3, ".db") == 0) { - index_count++; - } - } - cbm_closedir(d); - } + if (update_clear_indexes(home, dry_run) != 0) { + return 1; } - if (index_count > 0) { - printf("Found %d existing index(es) that must be rebuilt after update:\n", index_count); - cbm_list_indexes(home); - printf("\n"); - if (!dry_run) { - if (!prompt_yn("Delete these indexes and continue with update?")) { - printf("Update cancelled.\n"); - return 1; - } - int removed = cbm_remove_indexes(home); - printf("Removed %d index(es).\n\n", removed); - } else { - printf("(dry-run — indexes would be deleted)\n\n"); - } - } - - /* Step 2: Determine variant (--standard / --ui flags skip interactive prompt) */ - bool want_ui = false; - if (variant_flag == 1) { - want_ui = false; - } else if (variant_flag == 2) { - want_ui = true; - } else { -#ifndef _WIN32 - if (!isatty(fileno(stdin))) { - fprintf(stderr, "error: variant selection requires a terminal. " - "Use --standard or --ui flag.\n"); - return 1; - } -#endif - printf("Which binary variant do you want?\n"); - printf(" 1) standard — MCP server only\n"); - printf(" 2) ui — MCP server + embedded graph visualization\n"); - printf("Choose (1/2): "); - (void)fflush(stdout); - - char choice[16]; - if (!fgets(choice, sizeof(choice), stdin)) { - fprintf(stderr, "error: failed to read input\n"); - return 1; - } - want_ui = (choice[0] == '2'); + /* Step 2: Determine variant */ + int want_ui_rc = select_update_variant(variant_flag); + if (want_ui_rc < 0) { + return 1; } + bool want_ui = (want_ui_rc == 1); const char *variant = want_ui ? "ui-" : ""; const char *variant_label = want_ui ? "ui" : "standard"; - /* Step 3: Build download URL (CBM_DOWNLOAD_URL overrides for testing) */ const char *os = detect_os(); const char *arch = detect_arch(); const char *ext = strcmp(os, "windows") == 0 ? "zip" : "tar.gz"; - const char *base_url = getenv("CBM_DOWNLOAD_URL"); - if (!base_url || !base_url[0]) { - base_url = "https://github.com/DeusData/codebase-memory-mcp/releases/latest/download"; - } - char url[512]; - if (want_ui) { - snprintf(url, sizeof(url), "%s/codebase-memory-mcp-ui-%s-%s.%s", base_url, os, arch, ext); - } else { - snprintf(url, sizeof(url), "%s/codebase-memory-mcp-%s-%s.%s", base_url, os, arch, ext); - } + build_update_url(url, sizeof(url), os, arch, ext, want_ui); if (dry_run) { printf("\nWould download %s binary for %s/%s ...\n", variant_label, os, arch); @@ -3146,134 +3140,20 @@ int cbm_cmd_update(int argc, char **argv) { return 0; } - /* Step 4: Download using curl */ - char tmp_archive[256]; - snprintf(tmp_archive, sizeof(tmp_archive), "%s/cbm-update.%s", cbm_tmpdir(), ext); - - int rc = cbm_download_to_file(url, tmp_archive); - if (rc != 0) { - fprintf(stderr, "error: download failed (exit %d)\n", rc); - cbm_unlink(tmp_archive); - return 1; - } - - /* Step 4b: Verify checksum */ - { - /* Build the expected archive filename (matches checksums.txt format) */ - char archive_name[256]; - if (want_ui) { - snprintf(archive_name, sizeof(archive_name), "codebase-memory-mcp-ui-%s-%s.%s", os, - arch, ext); - } else { - snprintf(archive_name, sizeof(archive_name), "codebase-memory-mcp-%s-%s.%s", os, arch, - ext); - } - int crc = verify_download_checksum(tmp_archive, archive_name); - if (crc == 1) { - /* Hard fail: checksum mismatch */ - cbm_unlink(tmp_archive); - return 1; - } - /* crc == -1: could not verify (warning only), crc == 0: verified OK */ - } - - /* Step 4c: Kill running MCP server instances before replacement */ - { - int killed = cbm_kill_other_instances(); - if (killed > 0) { - printf("Stopped %d running MCP server instance(s).\n", killed); - } - } - - /* Step 5: Extract binary */ + /* Step 4-5: Download, verify, and install binary */ char bin_dest[1024]; #ifdef _WIN32 snprintf(bin_dest, sizeof(bin_dest), "%s/.local/bin/codebase-memory-mcp.exe", home); #else snprintf(bin_dest, sizeof(bin_dest), "%s/.local/bin/codebase-memory-mcp", home); #endif - - /* Ensure install directory exists */ char bin_dir[1024]; snprintf(bin_dir, sizeof(bin_dir), "%s/.local/bin", home); cbm_mkdir_p(bin_dir, 0755); - if (strcmp(ext, "tar.gz") == 0) { - /* Read archive into memory and extract */ - FILE *f = fopen(tmp_archive, "rb"); - if (!f) { - fprintf(stderr, "error: cannot open %s\n", tmp_archive); - return 1; - } - fseek(f, 0, SEEK_END); - long fsize = ftell(f); - fseek(f, 0, SEEK_SET); - - unsigned char *data = malloc((size_t)fsize); - if (!data) { - fclose(f); - cbm_unlink(tmp_archive); - return 1; - } - fread(data, 1, (size_t)fsize, f); - fclose(f); - - int bin_len = 0; - unsigned char *bin_data = cbm_extract_binary_from_targz(data, (int)fsize, &bin_len); - free(data); - cbm_unlink(tmp_archive); - - if (!bin_data || bin_len <= 0) { - fprintf(stderr, "error: binary not found in archive\n"); - free(bin_data); - return 1; - } - - /* Replace binary: unlink first (handles read-only existing file), - * then create with 0755 permissions. Fixes #114. */ - if (cbm_replace_binary(bin_dest, bin_data, bin_len, 0755) != 0) { - fprintf(stderr, "error: cannot write to %s\n", bin_dest); - free(bin_data); - return 1; - } - free(bin_data); - } else { - /* Zip extraction: in-memory (no external unzip dependency) */ - FILE *f = fopen(tmp_archive, "rb"); - if (!f) { - fprintf(stderr, "error: cannot open %s\n", tmp_archive); - return 1; - } - fseek(f, 0, SEEK_END); - long fsize = ftell(f); - fseek(f, 0, SEEK_SET); - - unsigned char *data = malloc((size_t)fsize); - if (!data) { - fclose(f); - cbm_unlink(tmp_archive); - return 1; - } - fread(data, 1, (size_t)fsize, f); - fclose(f); - - int bin_len = 0; - unsigned char *bin_data = cbm_extract_binary_from_zip(data, (int)fsize, &bin_len); - free(data); - cbm_unlink(tmp_archive); - - if (!bin_data || bin_len <= 0) { - fprintf(stderr, "error: binary not found in zip archive\n"); - free(bin_data); - return 1; - } - - if (cbm_replace_binary(bin_dest, bin_data, bin_len, 0755) != 0) { - fprintf(stderr, "error: cannot write to %s\n", bin_dest); - free(bin_data); - return 1; - } - free(bin_data); + int rc = download_verify_install(url, ext, os, arch, want_ui, bin_dest); + if (rc != 0) { + return 1; } /* Step 5b: macOS ad-hoc signing (required for arm64, harmless for x86_64) */ diff --git a/src/cypher/cypher.c b/src/cypher/cypher.c index fbf44b078..9c60258dc 100644 --- a/src/cypher/cypher.c +++ b/src/cypher/cypher.c @@ -62,6 +62,42 @@ static void lex_push_n(cbm_lex_result_t *r, cbm_token_type_t type, const char *s (cbm_token_t){.type = type, .text = heap_strndup(start, len), .pos = pos}; } +/* Parse a string literal (with escape handling) into the token list. + * *pos points at the character after the opening quote; updated past closing quote. */ +static void lex_string_literal(const char *input, int len, int *pos, char quote, + cbm_lex_result_t *out) { + int start = *pos; + char buf[4096]; + int blen = 0; + while (*pos < len && input[*pos] != quote) { + if (input[*pos] == '\\' && *pos + 1 < len) { + (*pos)++; + switch (input[*pos]) { + case 'n': + buf[blen++] = '\n'; + break; + case 't': + buf[blen++] = '\t'; + break; + case '\\': + buf[blen++] = '\\'; + break; + default: + buf[blen++] = input[*pos]; + break; + } + } else { + buf[blen++] = input[*pos]; + } + (*pos)++; + } + buf[blen] = '\0'; + if (*pos < len) { + (*pos)++; /* skip closing quote */ + } + lex_push(out, TOK_STRING, buf, start - 1); +} + /* Keyword table (case-insensitive lookup) */ typedef struct { const char *name; @@ -154,7 +190,132 @@ static cbm_token_type_t keyword_lookup(const char *word) { return TOK_IDENT; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Try to match a two-character token at position i. Returns true and advances i if matched. */ +static bool lex_try_two_char(const char *input, int len, int *i, cbm_lex_result_t *out) { + static const struct { + char c1, c2; + cbm_token_type_t type; + const char *text; + } pairs[] = { + {'!', '=', TOK_NEQ, "!="}, {'<', '>', TOK_NEQ, "<>"}, {'=', '~', TOK_EQTILDE, "=~"}, + {'>', '=', TOK_GTE, ">="}, {'<', '=', TOK_LTE, "<="}, {'.', '.', TOK_DOTDOT, ".."}, + }; + char c = input[*i]; + if (*i + 1 >= len) { + return false; + } + char c2 = input[*i + 1]; + for (int p = 0; p < (int)(sizeof(pairs) / sizeof(pairs[0])); p++) { + if (c == pairs[p].c1 && c2 == pairs[p].c2) { + lex_push(out, pairs[p].type, pairs[p].text, *i); + *i += 2; + return true; + } + } + return false; +} + +/* Try to match a single-character token. Returns TOK_EOF if not matched. */ +static cbm_token_type_t lex_single_char(char c) { + switch (c) { + case '(': + return TOK_LPAREN; + case ')': + return TOK_RPAREN; + case '[': + return TOK_LBRACKET; + case ']': + return TOK_RBRACKET; + case '-': + return TOK_DASH; + case '>': + return TOK_GT; + case '<': + return TOK_LT; + case ':': + return TOK_COLON; + case '.': + return TOK_DOT; + case '{': + return TOK_LBRACE; + case '}': + return TOK_RBRACE; + case '*': + return TOK_STAR; + case ',': + return TOK_COMMA; + case '=': + return TOK_EQ; + case '|': + return TOK_PIPE; + default: + return TOK_EOF; + } +} + +/* Try to lex an identifier or keyword starting at position i. Returns true if matched. */ +static bool lex_try_ident(const char *input, int len, int *i, cbm_lex_result_t *out) { + char c = input[*i]; + if (!isalpha((unsigned char)c) && c != '_') { + return false; + } + int start = *i; + while (*i < len && (isalnum((unsigned char)input[*i]) || input[*i] == '_')) { + (*i)++; + } + char word[256]; + int wlen = *i - start; + if (wlen >= (int)sizeof(word)) { + wlen = (int)sizeof(word) - 1; + } + memcpy(word, input + start, wlen); + word[wlen] = '\0'; + cbm_token_type_t type = keyword_lookup(word); + lex_push_n(out, type, input + start, *i - start, start); + return true; +} + +/* Try to lex a number starting at position i. Returns true if matched. */ +static bool lex_try_number(const char *input, int len, int *i, cbm_lex_result_t *out) { + char c = input[*i]; + if (!isdigit((unsigned char)c) && + !(c == '.' && *i + 1 < len && isdigit((unsigned char)input[*i + 1]))) { + return false; + } + int start = *i; + while (*i < len && (isdigit((unsigned char)input[*i]) || + (input[*i] == '.' && *i + 1 < len && input[*i + 1] != '.'))) { + (*i)++; + } + lex_push_n(out, TOK_NUMBER, input + start, *i - start, start); + return true; +} + +/* Skip whitespace and comments. Returns true if something was skipped. */ +static bool lex_skip_whitespace_comments(const char *input, int len, int *i) { + if (isspace((unsigned char)input[*i])) { + (*i)++; + return true; + } + if (*i + 1 < len && input[*i] == '/' && input[*i + 1] == '/') { + while (*i < len && input[*i] != '\n') { + (*i)++; + } + return true; + } + if (*i + 1 < len && input[*i] == '/' && input[*i + 1] == '*') { + *i += 2; + while (*i + 1 < len && !(input[*i] == '*' && input[*i + 1] == '/')) { + (*i)++; + } + if (*i + 1 < len) { + *i += 2; + } + return true; + } + return false; +} + int cbm_lex(const char *input, cbm_lex_result_t *out) { memset(out, 0, sizeof(*out)); if (!input) { @@ -165,28 +326,7 @@ int cbm_lex(const char *input, cbm_lex_result_t *out) { int i = 0; while (i < len) { - /* Skip whitespace */ - if (isspace((unsigned char)input[i])) { - i++; - continue; - } - - /* Skip // line comments */ - if (i + 1 < len && input[i] == '/' && input[i + 1] == '/') { - while (i < len && input[i] != '\n') { - i++; - } - continue; - } - /* Skip block comments */ - if (i + 1 < len && input[i] == '/' && input[i + 1] == '*') { - i += 2; - while (i + 1 < len && !(input[i] == '*' && input[i + 1] == '/')) { - i++; - } - if (i + 1 < len) { - i += 2; - } + if (lex_skip_whitespace_comments(input, len, &i)) { continue; } @@ -196,153 +336,30 @@ int cbm_lex(const char *input, cbm_lex_result_t *out) { if (c == '"' || c == '\'') { char quote = c; i++; - int start = i; - /* Build string handling escapes */ - char buf[4096]; - int blen = 0; - while (i < len && input[i] != quote) { - if (input[i] == '\\' && i + 1 < len) { - i++; - switch (input[i]) { - case 'n': - buf[blen++] = '\n'; - break; - case 't': - buf[blen++] = '\t'; - break; - case '\\': - buf[blen++] = '\\'; - break; - default: - buf[blen++] = input[i]; - break; - } - } else { - buf[blen++] = input[i]; - } - i++; - } - buf[blen] = '\0'; - if (i < len) { - i++; /* skip closing quote */ - } - lex_push(out, TOK_STRING, buf, start - 1); + lex_string_literal(input, len, &i, quote, out); continue; } /* Numbers — stop before ".." (DOTDOT operator) */ - if (isdigit((unsigned char)c) || - (c == '.' && i + 1 < len && isdigit((unsigned char)input[i + 1]))) { - int start = i; - while (i < len && (isdigit((unsigned char)input[i]) || - (input[i] == '.' && i + 1 < len && input[i + 1] != '.'))) { - i++; - } - lex_push_n(out, TOK_NUMBER, input + start, i - start, start); + if (lex_try_number(input, len, &i, out)) { continue; } /* Identifiers / keywords */ - if (isalpha((unsigned char)c) || c == '_') { - int start = i; - while (i < len && (isalnum((unsigned char)input[i]) || input[i] == '_')) { - i++; - } - char word[256]; - int wlen = i - start; - if (wlen >= (int)sizeof(word)) { - wlen = (int)sizeof(word) - 1; - } - memcpy(word, input + start, wlen); - word[wlen] = '\0'; - cbm_token_type_t type = keyword_lookup(word); - lex_push_n(out, type, input + start, i - start, start); + if (lex_try_ident(input, len, &i, out)) { continue; } /* Two-character tokens */ - if (c == '!' && i + 1 < len && input[i + 1] == '=') { - lex_push(out, TOK_NEQ, "!=", i); - i += 2; - continue; - } - if (c == '<' && i + 1 < len && input[i + 1] == '>') { - lex_push(out, TOK_NEQ, "<>", i); - i += 2; - continue; - } - if (c == '=' && i + 1 < len && input[i + 1] == '~') { - lex_push(out, TOK_EQTILDE, "=~", i); - i += 2; - continue; - } - if (c == '>' && i + 1 < len && input[i + 1] == '=') { - lex_push(out, TOK_GTE, ">=", i); - i += 2; - continue; - } - if (c == '<' && i + 1 < len && input[i + 1] == '=') { - lex_push(out, TOK_LTE, "<=", i); - i += 2; - continue; - } - if (c == '.' && i + 1 < len && input[i + 1] == '.') { - lex_push(out, TOK_DOTDOT, "..", i); - i += 2; - continue; + { + bool found_two = lex_try_two_char(input, len, &i, out); + if (found_two) { + continue; + } } /* Single-character tokens */ - cbm_token_type_t stype = TOK_EOF; - switch (c) { - case '(': - stype = TOK_LPAREN; - break; - case ')': - stype = TOK_RPAREN; - break; - case '[': - stype = TOK_LBRACKET; - break; - case ']': - stype = TOK_RBRACKET; - break; - case '-': - stype = TOK_DASH; - break; - case '>': - stype = TOK_GT; - break; - case '<': - stype = TOK_LT; - break; - case ':': - stype = TOK_COLON; - break; - case '.': - stype = TOK_DOT; - break; - case '{': - stype = TOK_LBRACE; - break; - case '}': - stype = TOK_RBRACE; - break; - case '*': - stype = TOK_STAR; - break; - case ',': - stype = TOK_COMMA; - break; - case '=': - stype = TOK_EQ; - break; - case '|': - stype = TOK_PIPE; - break; - default: - break; - } + cbm_token_type_t stype = lex_single_char(c); if (stype != TOK_EOF) { char buf[2] = {c, '\0'}; @@ -499,8 +516,86 @@ static int parse_node(parser_t *p, cbm_node_pattern_t *out) { return 0; } +/* Parse *min..max hop range after the star token has been consumed */ +static void parse_hop_range(parser_t *p, int *min_hops, int *max_hops) { + if (check(p, TOK_NUMBER)) { + int val = (int)strtol(peek(p)->text, NULL, 10); + advance(p); + if (match(p, TOK_DOTDOT)) { + *min_hops = val; + *max_hops = check(p, TOK_NUMBER) ? (int)strtol(advance(p)->text, NULL, 10) : 0; + } else { + /* *N means 1..N */ + *min_hops = 1; + *max_hops = val; + } + } else if (match(p, TOK_DOTDOT)) { + *min_hops = 1; + *max_hops = check(p, TOK_NUMBER) ? (int)strtol(advance(p)->text, NULL, 10) : 0; + } else { + /* * alone = unbounded */ + *min_hops = 1; + *max_hops = 0; + } +} + +/* Parse relationship type list after ':' inside brackets. Returns -1 on error. */ +static int parse_rel_types(parser_t *p, cbm_rel_pattern_t *out) { + int cap = 4; + int n = 0; + const char **types = malloc(cap * sizeof(const char *)); + + const cbm_token_t *t = expect(p, TOK_IDENT); + if (!t) { + free(types); + return -1; + } + types[n++] = heap_strdup(t->text); + + while (match(p, TOK_PIPE)) { + t = expect(p, TOK_IDENT); + if (!t) { + for (int i = 0; i < n; i++) { + free((void *)types[i]); + } + free(types); + return -1; + } + if (n >= cap) { + cap *= 2; + types = safe_realloc(types, cap * sizeof(const char *)); + } + types[n++] = heap_strdup(t->text); + } + + out->types = types; + out->type_count = n; + return 0; +} + +/* Parse bracket content of a relationship: [var:TYPE*hops] */ +static int parse_rel_bracket(parser_t *p, cbm_rel_pattern_t *out) { + /* Optional variable */ + if (check(p, TOK_IDENT) && !check(p, TOK_COLON)) { + out->variable = heap_strdup(advance(p)->text); + } + /* Optional :Types */ + if (match(p, TOK_COLON)) { + if (parse_rel_types(p, out) < 0) { + return -1; + } + } + /* Optional *hop_range */ + if (match(p, TOK_STAR)) { + parse_hop_range(p, &out->min_hops, &out->max_hops); + } + if (!expect(p, TOK_RBRACKET)) { + return -1; + } + return 0; +} + /* Parse relationship: -[:TYPE|TYPE2*min..max]-> or <-[...]- */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) static int parse_rel(parser_t *p, cbm_rel_pattern_t *out) { memset(out, 0, sizeof(*out)); out->min_hops = 1; @@ -514,79 +609,7 @@ static int parse_rel(parser_t *p, cbm_rel_pattern_t *out) { /* Optional bracket content */ if (match(p, TOK_LBRACKET)) { - /* Optional variable */ - if (check(p, TOK_IDENT) && !check(p, TOK_COLON)) { - /* Peek ahead: variable if followed by : or ] or * */ - /* Actually, check if next-next is colon */ - out->variable = heap_strdup(advance(p)->text); - } - - /* Optional :Types */ - if (match(p, TOK_COLON)) { - int cap = 4; - int n = 0; - const char **types = malloc(cap * sizeof(const char *)); - - const cbm_token_t *t = expect(p, TOK_IDENT); - if (!t) { - free(types); - return -1; - } - types[n++] = heap_strdup(t->text); - - while (match(p, TOK_PIPE)) { - t = expect(p, TOK_IDENT); - if (!t) { - for (int i = 0; i < n; i++) { - free((void *)types[i]); - } - free(types); - return -1; - } - if (n >= cap) { - cap *= 2; - types = safe_realloc(types, cap * sizeof(const char *)); - } - types[n++] = heap_strdup(t->text); - } - - out->types = types; - out->type_count = n; - } - - /* Optional *hop_range */ - if (match(p, TOK_STAR)) { - /* Parse hop range */ - if (check(p, TOK_NUMBER)) { - int val = (int)strtol(peek(p)->text, NULL, 10); - advance(p); - if (match(p, TOK_DOTDOT)) { - out->min_hops = val; - if (check(p, TOK_NUMBER)) { - out->max_hops = (int)strtol(advance(p)->text, NULL, 10); - } else { - out->max_hops = 0; /* unbounded */ - } - } else { - /* *N means 1..N */ - out->min_hops = 1; - out->max_hops = val; - } - } else if (match(p, TOK_DOTDOT)) { - out->min_hops = 1; - if (check(p, TOK_NUMBER)) { - out->max_hops = (int)strtol(advance(p)->text, NULL, 10); - } else { - out->max_hops = 0; - } - } else { - /* * alone = unbounded */ - out->min_hops = 1; - out->max_hops = 0; - } - } - - if (!expect(p, TOK_RBRACKET)) { + if (parse_rel_bracket(p, out) < 0) { return -1; } } @@ -691,8 +714,80 @@ static const char *unsupported_clause_error(cbm_token_type_t type) { /* Forward declarations for recursive descent */ static cbm_expr_t *parse_or_expr(parser_t *p); +/* Parse IN [val, val, ...] list. Returns expr_leaf or NULL on error. */ +static cbm_expr_t *parse_in_list(parser_t *p, cbm_condition_t *c) { + advance(p); + c->op = heap_strdup("IN"); + if (!expect(p, TOK_LBRACKET)) { + free((void *)c->variable); + free((void *)c->property); + free((void *)c->op); + return NULL; + } + int vcap = 8; + int vn = 0; + const char **vals = malloc(vcap * sizeof(const char *)); + while (!check(p, TOK_RBRACKET) && !check(p, TOK_EOF)) { + if (vn > 0) { + match(p, TOK_COMMA); + } + if (check(p, TOK_STRING) || check(p, TOK_NUMBER)) { + if (vn >= vcap) { + vcap *= 2; + vals = safe_realloc(vals, vcap * sizeof(const char *)); + } + vals[vn++] = heap_strdup(advance(p)->text); + } else { + break; + } + } + expect(p, TOK_RBRACKET); + c->in_values = vals; + c->in_value_count = vn; + return expr_leaf(*c); +} + +/* Try to parse a comparison operator. Returns heap-allocated op string or NULL. */ +static char *parse_comparison_op(parser_t *p) { + if (match(p, TOK_EQ)) { + return heap_strdup("="); + } + if (match(p, TOK_NEQ)) { + return heap_strdup("<>"); + } + if (match(p, TOK_EQTILDE)) { + return heap_strdup("=~"); + } + if (match(p, TOK_GTE)) { + return heap_strdup(">="); + } + if (match(p, TOK_LTE)) { + return heap_strdup("<="); + } + if (match(p, TOK_GT)) { + return heap_strdup(">"); + } + if (match(p, TOK_LT)) { + return heap_strdup("<"); + } + if (check(p, TOK_CONTAINS)) { + advance(p); + return heap_strdup("CONTAINS"); + } + if (check(p, TOK_STARTS)) { + advance(p); + expect(p, TOK_WITH); + return heap_strdup("STARTS WITH"); + } + if (check(p, TOK_ENDS)) { + advance(p); + expect(p, TOK_WITH); + return heap_strdup("ENDS WITH"); + } + return NULL; +} + /* Parse a single condition: var.prop OP value | var.prop IS [NOT] NULL | var.prop IN [...] */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) static cbm_expr_t *parse_condition_expr(parser_t *p) { /* Check for NOT prefix at condition level (e.g. NOT n.name CONTAINS "x") */ bool negated = match(p, TOK_NOT); @@ -733,64 +828,12 @@ static cbm_expr_t *parse_condition_expr(parser_t *p) { /* IN [...] */ if (check(p, TOK_IN)) { - advance(p); - c.op = heap_strdup("IN"); - if (!expect(p, TOK_LBRACKET)) { - free((void *)c.variable); - free((void *)c.property); - free((void *)c.op); - return NULL; - } - int vcap = 8; - int vn = 0; - const char **vals = malloc(vcap * sizeof(const char *)); - while (!check(p, TOK_RBRACKET) && !check(p, TOK_EOF)) { - if (vn > 0) { - match(p, TOK_COMMA); - } - if (check(p, TOK_STRING) || check(p, TOK_NUMBER)) { - if (vn >= vcap) { - vcap *= 2; - vals = safe_realloc(vals, vcap * sizeof(const char *)); - } - vals[vn++] = heap_strdup(advance(p)->text); - } else { - break; - } - } - expect(p, TOK_RBRACKET); - c.in_values = vals; - c.in_value_count = vn; - return expr_leaf(c); + return parse_in_list(p, &c); } /* Standard operators */ - if (match(p, TOK_EQ)) { - c.op = heap_strdup("="); - } else if (match(p, TOK_NEQ)) { - c.op = heap_strdup("<>"); - } else if (match(p, TOK_EQTILDE)) { - c.op = heap_strdup("=~"); - } else if (match(p, TOK_GTE)) { - c.op = heap_strdup(">="); - } else if (match(p, TOK_LTE)) { - c.op = heap_strdup("<="); - } else if (match(p, TOK_GT)) { - c.op = heap_strdup(">"); - } else if (match(p, TOK_LT)) { - c.op = heap_strdup("<"); - } else if (check(p, TOK_CONTAINS)) { - advance(p); - c.op = heap_strdup("CONTAINS"); - } else if (check(p, TOK_STARTS)) { - advance(p); - expect(p, TOK_WITH); - c.op = heap_strdup("STARTS WITH"); - } else if (check(p, TOK_ENDS)) { - advance(p); - expect(p, TOK_WITH); - c.op = heap_strdup("ENDS WITH"); - } else { + c.op = parse_comparison_op(p); + if (!c.op) { snprintf(p->error, sizeof(p->error), "unexpected operator at pos %d", peek(p)->pos); free((void *)c.variable); free((void *)c.property); @@ -952,8 +995,34 @@ static const char *str_func_name(cbm_token_type_t t) { } } +/* Parse a value literal: string, number, ident[.prop], true, false. Returns heap-allocated. */ +static const char *parse_value_literal(parser_t *p) { + if (check(p, TOK_STRING) || check(p, TOK_NUMBER)) { + return heap_strdup(advance(p)->text); + } + if (check(p, TOK_IDENT)) { + char buf[256]; + const cbm_token_t *v = advance(p); + if (match(p, TOK_DOT)) { + const cbm_token_t *pr = expect(p, TOK_IDENT); + snprintf(buf, sizeof(buf), "%s.%s", v->text, pr ? pr->text : ""); + } else { + snprintf(buf, sizeof(buf), "%s", v->text); + } + return heap_strdup(buf); + } + if (check(p, TOK_TRUE)) { + advance(p); + return heap_strdup("true"); + } + if (check(p, TOK_FALSE)) { + advance(p); + return heap_strdup("false"); + } + return NULL; +} + /* Parse CASE WHEN ... THEN ... [ELSE ...] END */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) static cbm_case_expr_t *parse_case_expr(parser_t *p) { /* CASE already consumed */ cbm_case_expr_t *kase = calloc(1, sizeof(cbm_case_expr_t)); @@ -967,27 +1036,7 @@ static cbm_case_expr_t *parse_case_expr(parser_t *p) { expr_free(when); break; } - const char *then_val = NULL; - if (check(p, TOK_STRING) || check(p, TOK_NUMBER)) { - then_val = heap_strdup(advance(p)->text); - } else if (check(p, TOK_IDENT)) { - /* Could be var.prop */ - char buf[256]; - const cbm_token_t *v = advance(p); - if (match(p, TOK_DOT)) { - const cbm_token_t *pr = expect(p, TOK_IDENT); - snprintf(buf, sizeof(buf), "%s.%s", v->text, pr ? pr->text : ""); - } else { - snprintf(buf, sizeof(buf), "%s", v->text); - } - then_val = heap_strdup(buf); - } else if (check(p, TOK_TRUE)) { - advance(p); - then_val = heap_strdup("true"); - } else if (check(p, TOK_FALSE)) { - advance(p); - then_val = heap_strdup("false"); - } + const char *then_val = parse_value_literal(p); if (kase->branch_count >= bcap) { bcap *= 2; kase->branches = safe_realloc(kase->branches, bcap * sizeof(cbm_case_branch_t)); @@ -997,18 +1046,141 @@ static cbm_case_expr_t *parse_case_expr(parser_t *p) { } if (match(p, TOK_ELSE)) { - if (check(p, TOK_STRING) || check(p, TOK_NUMBER)) { - kase->else_val = heap_strdup(advance(p)->text); - } else if (check(p, TOK_IDENT)) { - kase->else_val = heap_strdup(advance(p)->text); - } + kase->else_val = parse_value_literal(p); } expect(p, TOK_END); return kase; } +/* Parse a single RETURN/WITH item (aggregate, string func, CASE, or plain var.prop). + * Returns 0 on success, -1 on error. */ +/* Parse var[.prop] into item->variable and item->property. Returns -1 on error. */ +static int parse_var_dot_prop(parser_t *p, cbm_return_item_t *item) { + const cbm_token_t *var = expect(p, TOK_IDENT); + if (!var) { + return -1; + } + item->variable = heap_strdup(var->text); + if (match(p, TOK_DOT)) { + const cbm_token_t *prop = expect(p, TOK_IDENT); + if (prop) { + item->property = heap_strdup(prop->text); + } + } + return 0; +} + +/* Parse aggregate function call: COUNT(var.prop) */ +static int parse_aggregate_item(parser_t *p, cbm_return_item_t *item) { + cbm_token_type_t ft = peek(p)->type; + advance(p); + expect(p, TOK_LPAREN); + if (match(p, TOK_STAR)) { + item->variable = heap_strdup("*"); + } else { + if (parse_var_dot_prop(p, item) < 0) { + return -1; + } + } + expect(p, TOK_RPAREN); + item->func = heap_strdup(agg_func_name(ft)); + return 0; +} + +/* Parse string function call: toLower(var.prop) */ +static int parse_string_func_item(parser_t *p, cbm_return_item_t *item) { + cbm_token_type_t ft = peek(p)->type; + advance(p); + expect(p, TOK_LPAREN); + if (parse_var_dot_prop(p, item) < 0) { + return -1; + } + expect(p, TOK_RPAREN); + item->func = heap_strdup(str_func_name(ft)); + return 0; +} + +static int parse_return_item(parser_t *p, cbm_return_item_t *item) { + memset(item, 0, sizeof(*item)); + int rc = 0; + if (check(p, TOK_CASE)) { + advance(p); + item->kase = parse_case_expr(p); + item->variable = heap_strdup("CASE"); + } else if (is_aggregate_tok(peek(p)->type)) { + rc = parse_aggregate_item(p, item); + } else if (is_string_func_tok(peek(p)->type)) { + rc = parse_string_func_item(p, item); + } else { + rc = parse_var_dot_prop(p, item); + } + if (rc < 0) { + return -1; + } + /* Optional AS alias */ + if (match(p, TOK_AS)) { + const cbm_token_t *alias = expect(p, TOK_IDENT); + if (alias) { + item->alias = heap_strdup(alias->text); + } + } + return 0; +} + +/* Parse ORDER BY field into r->order_by and r->order_dir */ +/* Parse aggregate function call for ORDER BY */ +static void parse_order_by_agg(parser_t *p, char *buf, size_t buf_sz) { + const char *fn = agg_func_name(peek(p)->type); + advance(p); + expect(p, TOK_LPAREN); + if (match(p, TOK_STAR)) { + snprintf(buf, buf_sz, "%s(*)", fn); + } else { + const cbm_token_t *var = expect(p, TOK_IDENT); + snprintf(buf, buf_sz, "%s(%s)", fn, var ? var->text : ""); + } + expect(p, TOK_RPAREN); +} + +/* Parse var[.prop] for ORDER BY */ +static void parse_order_by_var(parser_t *p, char *buf, size_t buf_sz) { + const cbm_token_t *var = expect(p, TOK_IDENT); + if (!var) { + return; + } + snprintf(buf, buf_sz, "%s", var->text); + if (match(p, TOK_DOT)) { + const cbm_token_t *prop = expect(p, TOK_IDENT); + if (prop) { + snprintf(buf, buf_sz, "%s.%s", var->text, prop->text); + } + } +} + +/* Parse ORDER BY expression into buf. Returns buf. */ +static char *parse_order_by_expr(parser_t *p, char *buf, size_t buf_sz) { + buf[0] = '\0'; + if (is_aggregate_tok(peek(p)->type)) { + parse_order_by_agg(p, buf, buf_sz); + } else { + parse_order_by_var(p, buf, buf_sz); + } + return buf; +} + +static void parse_order_by_clause(parser_t *p, cbm_return_clause_t *r) { + expect(p, TOK_BY); + char order_buf[256]; + parse_order_by_expr(p, order_buf, sizeof(order_buf)); + r->order_by = heap_strdup(order_buf); + if (match(p, TOK_ASC)) { + r->order_dir = heap_strdup("ASC"); + } else if (match(p, TOK_DESC)) { + r->order_dir = heap_strdup("DESC"); + } +} + /* Parse RETURN/WITH clause (shared logic) */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) static int parse_return_or_with(parser_t *p, cbm_return_clause_t **out, bool is_with) { cbm_token_type_t tok = (int)is_with ? TOK_WITH : TOK_RETURN; /* For WITH, we need to check it's standalone (not preceded by STARTS) */ @@ -1036,82 +1208,10 @@ static int parse_return_or_with(parser_t *p, cbm_return_clause_t **out, bool is_ } cbm_return_item_t item = {0}; - - /* CASE expression */ - if (check(p, TOK_CASE)) { - advance(p); - item.kase = parse_case_expr(p); - item.variable = heap_strdup("CASE"); - } - /* Aggregate: COUNT/SUM/AVG/MIN/MAX/COLLECT */ - else if (is_aggregate_tok(peek(p)->type)) { - cbm_token_type_t ft = peek(p)->type; - advance(p); - expect(p, TOK_LPAREN); - if (match(p, TOK_STAR)) { - item.variable = heap_strdup("*"); - } else { - const cbm_token_t *var = expect(p, TOK_IDENT); - if (!var) { - free(r->items); - free(r); - return -1; - } - item.variable = heap_strdup(var->text); - if (match(p, TOK_DOT)) { - const cbm_token_t *prop = expect(p, TOK_IDENT); - if (prop) { - item.property = heap_strdup(prop->text); - } - } - } - expect(p, TOK_RPAREN); - item.func = heap_strdup(agg_func_name(ft)); - } - /* String function: toLower/toUpper/toString */ - else if (is_string_func_tok(peek(p)->type)) { - cbm_token_type_t ft = peek(p)->type; - advance(p); - expect(p, TOK_LPAREN); - const cbm_token_t *var = expect(p, TOK_IDENT); - if (!var) { - free(r->items); - free(r); - return -1; - } - item.variable = heap_strdup(var->text); - if (match(p, TOK_DOT)) { - const cbm_token_t *prop = expect(p, TOK_IDENT); - if (prop) { - item.property = heap_strdup(prop->text); - } - } - expect(p, TOK_RPAREN); - item.func = heap_strdup(str_func_name(ft)); - } - /* Plain var.prop */ - else { - const cbm_token_t *var = expect(p, TOK_IDENT); - if (!var) { - free(r->items); - free(r); - return -1; - } - item.variable = heap_strdup(var->text); - if (match(p, TOK_DOT)) { - const cbm_token_t *prop = expect(p, TOK_IDENT); - if (prop) { - item.property = heap_strdup(prop->text); - } - } - } - - /* Optional AS alias */ - if (match(p, TOK_AS)) { - const cbm_token_t *alias = expect(p, TOK_IDENT); - if (alias) { - item.alias = heap_strdup(alias->text); - } + if (parse_return_item(p, &item) < 0) { + free(r->items); + free(r); + return -1; } if (r->count >= cap) { @@ -1125,39 +1225,7 @@ static int parse_return_or_with(parser_t *p, cbm_return_clause_t **out, bool is_ tail: /* Optional ORDER BY */ if (match(p, TOK_ORDER)) { - expect(p, TOK_BY); - char order_buf[256] = ""; - if (is_aggregate_tok(peek(p)->type)) { - const char *fn = agg_func_name(peek(p)->type); - advance(p); - expect(p, TOK_LPAREN); - if (match(p, TOK_STAR)) { - snprintf(order_buf, sizeof(order_buf), "%s(*)", fn); - } else { - const cbm_token_t *var = expect(p, TOK_IDENT); - snprintf(order_buf, sizeof(order_buf), "%s(%s)", fn, var ? var->text : ""); - } - expect(p, TOK_RPAREN); - } else { - const cbm_token_t *var = expect(p, TOK_IDENT); - if (var) { - snprintf(order_buf, sizeof(order_buf), "%s", var->text); - if (match(p, TOK_DOT)) { - const cbm_token_t *prop = expect(p, TOK_IDENT); - if (prop) { - char tmp[256]; - snprintf(tmp, sizeof(tmp), "%s.%s", var->text, prop->text); - snprintf(order_buf, sizeof(order_buf), "%s", tmp); - } - } - } - } - r->order_by = heap_strdup(order_buf); - if (match(p, TOK_ASC)) { - r->order_dir = heap_strdup("ASC"); - } else if (match(p, TOK_DESC)) { - r->order_dir = heap_strdup("DESC"); - } + parse_order_by_clause(p, r); } /* Optional SKIP */ @@ -1191,7 +1259,7 @@ static int parse_match_pattern(parser_t *p, cbm_pattern_t *pat) { int node_cap = 4; int rel_cap = 4; pat->nodes = malloc(node_cap * sizeof(cbm_node_pattern_t)); - pat->rels = malloc(rel_cap * sizeof(cbm_rel_pattern_t)); + pat->rels = calloc(rel_cap, sizeof(cbm_rel_pattern_t)); if (parse_node(p, &pat->nodes[0]) < 0) { return -1; @@ -1220,7 +1288,113 @@ static int parse_match_pattern(parser_t *p, cbm_pattern_t *pat) { return 0; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Parse UNWIND [...] AS var clause into query */ +static void parse_unwind_clause(parser_t *p, cbm_query_t *q) { + advance(p); + if (check(p, TOK_LBRACKET)) { + /* Literal list: [1, 2, 3] — collect as JSON array string */ + advance(p); + char buf[2048] = "["; + int blen = 1; + while (!check(p, TOK_RBRACKET) && !check(p, TOK_EOF)) { + if (blen > 1) { + buf[blen++] = ','; + } + if (check(p, TOK_STRING)) { + blen += snprintf(buf + blen, sizeof(buf) - blen, "\"%s\"", peek(p)->text); + advance(p); + } else if (check(p, TOK_NUMBER)) { + blen += snprintf(buf + blen, sizeof(buf) - blen, "%s", peek(p)->text); + advance(p); + } else { + advance(p); + } + match(p, TOK_COMMA); + } + expect(p, TOK_RBRACKET); + buf[blen++] = ']'; + buf[blen] = '\0'; + q->unwind_expr = heap_strdup(buf); + } else if (check(p, TOK_IDENT)) { + q->unwind_expr = heap_strdup(advance(p)->text); + } + expect(p, TOK_AS); + const cbm_token_t *alias = expect(p, TOK_IDENT); + if (alias) { + q->unwind_alias = heap_strdup(alias->text); + } +} + +/* Parse a chain of MATCH / OPTIONAL MATCH patterns into query. + * Returns -1 on error (fills p->error). */ +static int parse_match_chain(parser_t *p, cbm_query_t *q, int *pat_cap) { + while (check(p, TOK_MATCH) || check(p, TOK_OPTIONAL)) { + bool opt = false; + if (check(p, TOK_OPTIONAL)) { + advance(p); + opt = true; + } + if (!expect(p, TOK_MATCH)) { + break; + } + if (q->pattern_count >= *pat_cap) { + *pat_cap *= 2; + q->patterns = safe_realloc(q->patterns, *pat_cap * sizeof(cbm_pattern_t)); + q->pattern_optional = safe_realloc(q->pattern_optional, *pat_cap * sizeof(bool)); + } + if (parse_match_pattern(p, &q->patterns[q->pattern_count]) < 0) { + return -1; + } + q->pattern_optional[q->pattern_count] = opt; + q->pattern_count++; + } + return 0; +} + +/* Parse post-WHERE clauses: additional MATCH, WITH, RETURN, UNION */ +static int parse_post_where(parser_t *p, cbm_query_t *q, int *pat_cap) { + /* More MATCH / OPTIONAL MATCH after WHERE */ + if (parse_match_chain(p, q, pat_cap) < 0) { + return -1; + } + /* Check for unsupported keywords */ + const char *unsup = unsupported_clause_error(peek(p)->type); + if (unsup) { + snprintf(p->error, sizeof(p->error), "%s", unsup); + return -1; + } + /* Optional WITH clause (standalone, not STARTS WITH) */ + if (check(p, TOK_WITH) && (p->pos < 2 || p->tokens[p->pos - 1].type != TOK_STARTS)) { + if (parse_return_or_with(p, &q->with_clause, true) < 0) { + return -1; + } + if (parse_where(p, &q->post_with_where) < 0) { + return -1; + } + } + /* Optional RETURN */ + if (parse_return(p, &q->ret) < 0) { + return -1; + } + /* UNION [ALL] */ + if (check(p, TOK_UNION)) { + advance(p); + q->union_all = match(p, TOK_ALL); + cbm_parse_result_t sub = {0}; + if (cbm_parse(&p->tokens[p->pos], p->count - p->pos, &sub) < 0) { + if (sub.error) { + snprintf(p->error, sizeof(p->error), "%s", sub.error); + } + cbm_parse_free(&sub); + return -1; + } + q->union_next = sub.query; + sub.query = NULL; + cbm_parse_free(&sub); + } + return 0; +} + int cbm_parse(const cbm_token_t *tokens, int token_count, cbm_parse_result_t *out) { memset(out, 0, sizeof(*out)); parser_t p = {.tokens = tokens, .count = token_count, .pos = 0}; @@ -1232,47 +1406,12 @@ int cbm_parse(const cbm_token_t *tokens, int token_count, cbm_parse_result_t *ou return -1; } - /* UNWIND ... AS var (before MATCH) */ cbm_query_t *q = calloc(1, sizeof(cbm_query_t)); if (check(&p, TOK_UNWIND)) { - advance(&p); - /* Parse expression — could be a literal list or a variable */ - if (check(&p, TOK_LBRACKET)) { - /* Literal list: [1, 2, 3] — collect as JSON array string */ - advance(&p); - char buf[2048] = "["; - int blen = 1; - while (!check(&p, TOK_RBRACKET) && !check(&p, TOK_EOF)) { - if (blen > 1) { - buf[blen++] = ','; - } - if (check(&p, TOK_STRING)) { - blen += snprintf(buf + blen, sizeof(buf) - blen, "\"%s\"", peek(&p)->text); - advance(&p); - } else if (check(&p, TOK_NUMBER)) { - blen += snprintf(buf + blen, sizeof(buf) - blen, "%s", peek(&p)->text); - advance(&p); - } else { - advance(&p); - } - match(&p, TOK_COMMA); - } - expect(&p, TOK_RBRACKET); - buf[blen++] = ']'; - buf[blen] = '\0'; - q->unwind_expr = heap_strdup(buf); - } else if (check(&p, TOK_IDENT)) { - q->unwind_expr = heap_strdup(advance(&p)->text); - } - expect(&p, TOK_AS); - const cbm_token_t *alias = expect(&p, TOK_IDENT); - if (alias) { - q->unwind_alias = heap_strdup(alias->text); - } + parse_unwind_clause(&p, q); } - /* Expect MATCH or OPTIONAL MATCH */ bool first_optional = false; if (check(&p, TOK_OPTIONAL)) { advance(&p); @@ -1284,7 +1423,6 @@ int cbm_parse(const cbm_token_t *tokens, int token_count, cbm_parse_result_t *ou return -1; } - /* Parse first pattern */ int pat_cap = 4; q->patterns = malloc(pat_cap * sizeof(cbm_pattern_t)); q->pattern_optional = malloc(pat_cap * sizeof(bool)); @@ -1297,109 +1435,24 @@ int cbm_parse(const cbm_token_t *tokens, int token_count, cbm_parse_result_t *ou q->pattern_optional[0] = first_optional; q->pattern_count = 1; - /* Additional MATCH / OPTIONAL MATCH patterns */ - while (check(&p, TOK_MATCH) || check(&p, TOK_OPTIONAL)) { - bool opt = false; - if (check(&p, TOK_OPTIONAL)) { - advance(&p); - opt = true; - } - if (!expect(&p, TOK_MATCH)) { - break; - } - if (q->pattern_count >= pat_cap) { - pat_cap *= 2; - q->patterns = safe_realloc(q->patterns, pat_cap * sizeof(cbm_pattern_t)); - q->pattern_optional = safe_realloc(q->pattern_optional, pat_cap * sizeof(bool)); - } - if (parse_match_pattern(&p, &q->patterns[q->pattern_count]) < 0) { - out->error = heap_strdup(p.error[0] ? p.error : "failed to parse additional pattern"); - cbm_query_free(q); - return -1; - } - q->pattern_optional[q->pattern_count] = opt; - q->pattern_count++; + if (parse_match_chain(&p, q, &pat_cap) < 0) { + out->error = heap_strdup(p.error[0] ? p.error : "failed to parse additional pattern"); + cbm_query_free(q); + return -1; } - /* Optional WHERE */ if (parse_where(&p, &q->where) < 0) { out->error = heap_strdup(p.error[0] ? p.error : "failed to parse WHERE"); cbm_query_free(q); return -1; } - /* More MATCH / OPTIONAL MATCH after WHERE (e.g. MATCH ... WHERE ... OPTIONAL MATCH ...) */ - while (check(&p, TOK_MATCH) || check(&p, TOK_OPTIONAL)) { - bool opt2 = false; - if (check(&p, TOK_OPTIONAL)) { - advance(&p); - opt2 = true; - } - if (!expect(&p, TOK_MATCH)) { - break; - } - if (q->pattern_count >= pat_cap) { - pat_cap *= 2; - q->patterns = safe_realloc(q->patterns, pat_cap * sizeof(cbm_pattern_t)); - q->pattern_optional = safe_realloc(q->pattern_optional, pat_cap * sizeof(bool)); - } - if (parse_match_pattern(&p, &q->patterns[q->pattern_count]) < 0) { - out->error = heap_strdup(p.error[0] ? p.error : "failed to parse post-WHERE pattern"); - cbm_query_free(q); - return -1; - } - q->pattern_optional[q->pattern_count] = opt2; - q->pattern_count++; - } - - /* Check for unsupported keywords after WHERE */ - unsup = unsupported_clause_error(peek(&p)->type); - if (unsup) { - out->error = heap_strdup(unsup); + if (parse_post_where(&p, q, &pat_cap) < 0) { + out->error = heap_strdup(p.error[0] ? p.error : "failed to parse query"); cbm_query_free(q); return -1; } - /* Optional WITH clause (standalone, not STARTS WITH) */ - if (check(&p, TOK_WITH) && (p.pos < 2 || p.tokens[p.pos - 1].type != TOK_STARTS)) { - /* Make sure this isn't inside a WHERE STARTS WITH */ - if (parse_return_or_with(&p, &q->with_clause, true) < 0) { - out->error = heap_strdup(p.error[0] ? p.error : "failed to parse WITH"); - cbm_query_free(q); - return -1; - } - /* Optional post-WITH WHERE */ - if (parse_where(&p, &q->post_with_where) < 0) { - out->error = heap_strdup(p.error[0] ? p.error : "failed to parse post-WITH WHERE"); - cbm_query_free(q); - return -1; - } - } - - /* Optional RETURN */ - if (parse_return(&p, &q->ret) < 0) { - out->error = heap_strdup(p.error[0] ? p.error : "failed to parse RETURN"); - cbm_query_free(q); - return -1; - } - - /* UNION [ALL] → recursive parse of rest */ - if (check(&p, TOK_UNION)) { - advance(&p); - q->union_all = match(&p, TOK_ALL); - /* Parse remaining tokens as a new query */ - cbm_parse_result_t sub = {0}; - if (cbm_parse(&p.tokens[p.pos], p.count - p.pos, &sub) < 0) { - out->error = heap_strdup(sub.error ? sub.error : "failed to parse UNION query"); - cbm_parse_free(&sub); - cbm_query_free(q); - return -1; - } - q->union_next = sub.query; - sub.query = NULL; - cbm_parse_free(&sub); - } - out->query = q; return 0; } @@ -1676,6 +1729,9 @@ static void node_deep_copy(cbm_node_t *dst, const cbm_node_t *src) { } static void node_fields_free(cbm_node_t *n) { + if (!n) { + return; + } free((void *)n->project); free((void *)n->label); free((void *)n->name); @@ -1758,39 +1814,86 @@ static void binding_set(binding_t *b, const char *var, const cbm_node_t *node) { b->var_count++; } -/* Evaluate a WHERE condition against a binding */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) -static bool eval_condition(const cbm_condition_t *c, binding_t *b) { - const char *actual; - - /* Check if variable is an edge binding */ +/* Resolve the actual property value for a condition from a binding */ +static const char *resolve_condition_value(const cbm_condition_t *c, binding_t *b) { cbm_edge_t *e = binding_get_edge(b, c->variable); if (e) { - actual = edge_prop(e, c->property); - } else { - cbm_node_t *n = binding_get(b, c->variable); - if (!n) { - /* Unbound variable — optimistic pass-through for early WHERE */ - return true; + return edge_prop(e, c->property); + } + cbm_node_t *n = binding_get(b, c->variable); + if (!n) { + return NULL; /* unbound variable */ + } + if (c->property) { + return node_prop(n, c->property); + } + /* Bare alias (e.g. post-WITH virtual var) — use node name directly */ + return n->name ? n->name : ""; +} + +/* Evaluate a comparison operator between actual and expected strings. */ +static bool eval_comparison_op(const char *op, const char *actual, const char *expected) { + if (strcmp(op, "=") == 0) { + return strcmp(actual, expected) == 0; + } + if (strcmp(op, "<>") == 0) { + return strcmp(actual, expected) != 0; + } + if (strcmp(op, "=~") == 0) { + cbm_regex_t re; + if (cbm_regcomp(&re, expected, CBM_REG_EXTENDED | CBM_REG_NOSUB) != 0) { + return false; } - if (c->property) { - actual = node_prop(n, c->property); - } else { - /* Bare alias (e.g. post-WITH virtual var) — use node name directly */ - actual = n->name ? n->name : ""; + int rc = cbm_regexec(&re, actual, 0, NULL, 0); + cbm_regfree(&re); + return rc == 0; + } + if (strcmp(op, "CONTAINS") == 0) { + return strstr(actual, expected) != NULL; + } + if (strcmp(op, "STARTS WITH") == 0) { + return strncmp(actual, expected, strlen(expected)) == 0; + } + if (strcmp(op, "ENDS WITH") == 0) { + size_t alen = strlen(actual); + size_t elen = strlen(expected); + return alen >= elen && strcmp(actual + alen - elen, expected) == 0; + } + if (strcmp(op, ">") == 0 || strcmp(op, "<") == 0 || strcmp(op, ">=") == 0 || + strcmp(op, "<=") == 0) { + double a = strtod(actual, NULL); + double exp_val = strtod(expected, NULL); + if (op[0] == '>' && op[1] == '=') { + return a >= exp_val; } + if (op[0] == '<' && op[1] == '=') { + return a <= exp_val; + } + if (op[0] == '>') { + return a > exp_val; + } + return a < exp_val; + } + return false; +} + +/* Evaluate a WHERE condition against a binding */ +static bool eval_condition(const cbm_condition_t *c, binding_t *b) { + const char *actual = resolve_condition_value(c, b); + if (!actual) { + return true; } bool result; /* IS NULL / IS NOT NULL */ if (strcmp(c->op, "IS NULL") == 0) { - result = ((!actual || actual[0] == '\0') != 0); - return (int)(c->negated ? !result : result); + result = (!actual || actual[0] == '\0'); + return c->negated ? !result : result; } if (strcmp(c->op, "IS NOT NULL") == 0) { - result = ((actual && actual[0] != '\0') != 0); - return (int)(c->negated ? !result : result); + result = (actual && actual[0] != '\0'); + return c->negated ? !result : result; } /* IN [...] */ @@ -1802,49 +1905,11 @@ static bool eval_condition(const cbm_condition_t *c, binding_t *b) { break; } } - return (int)(c->negated ? !result : result); + return c->negated ? !result : result; } - const char *expected = c->value; - - if (strcmp(c->op, "=") == 0) { - result = strcmp(actual, expected) == 0; - } else if (strcmp(c->op, "<>") == 0) { - result = strcmp(actual, expected) != 0; - } else if (strcmp(c->op, "=~") == 0) { - cbm_regex_t re; - if (cbm_regcomp(&re, expected, CBM_REG_EXTENDED | CBM_REG_NOSUB) != 0) { - return false; - } - int rc = cbm_regexec(&re, actual, 0, NULL, 0); - cbm_regfree(&re); - result = (rc == 0); - } else if (strcmp(c->op, "CONTAINS") == 0) { - result = strstr(actual, expected) != NULL; - } else if (strcmp(c->op, "STARTS WITH") == 0) { - result = strncmp(actual, expected, strlen(expected)) == 0; - } else if (strcmp(c->op, "ENDS WITH") == 0) { - size_t alen = strlen(actual); - size_t elen = strlen(expected); - result = ((alen >= elen && strcmp(actual + alen - elen, expected) == 0) != 0); - } else if (strcmp(c->op, ">") == 0 || strcmp(c->op, "<") == 0 || strcmp(c->op, ">=") == 0 || - strcmp(c->op, "<=") == 0) { - double a = strtod(actual, NULL); - double exp_val = strtod(expected, NULL); - if (strcmp(c->op, ">") == 0) { - result = a > exp_val; - } else if (strcmp(c->op, "<") == 0) { - result = a < exp_val; - } else if (strcmp(c->op, ">=") == 0) { - result = a >= exp_val; - } else { - result = a <= exp_val; - } - } else { - result = false; - } - - return (int)(c->negated ? !result : result); + result = eval_comparison_op(c->op, actual, c->value); + return c->negated ? !result : result; } /* Recursive expression tree evaluator */ @@ -2058,7 +2123,124 @@ static void scan_pattern_nodes(cbm_store_t *store, const char *project, int max_ /* ── Expand one pattern's relationships on a set of bindings ──── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity,readability-function-size) +/* Process edges: look up target node, filter by label/props, add binding. + * `inbound` controls which end of the edge is the target id. */ +static void process_edges(cbm_store_t *store, cbm_edge_t *edges, int edge_count, bool inbound, + const cbm_node_pattern_t *target_node, binding_t *b, const char *to_var, + const char *rel_var, binding_t *new_bindings, int *new_count, int max_new, + int *match_count) { + for (int ei = 0; ei < edge_count && *new_count < max_new; ei++) { + int64_t tid = inbound ? edges[ei].source_id : edges[ei].target_id; + cbm_node_t found = {0}; + if (cbm_store_find_node_by_id(store, tid, &found) != CBM_STORE_OK) { + continue; + } + if (target_node->label && strcmp(found.label, target_node->label) != 0) { + node_fields_free(&found); + continue; + } + if (!check_inline_props(&found, target_node->props, target_node->prop_count)) { + node_fields_free(&found); + continue; + } + binding_t nb = {0}; + binding_copy(&nb, b); + binding_set(&nb, to_var, &found); + if (rel_var) { + binding_set_edge(&nb, rel_var, &edges[ei]); + } + node_fields_free(&found); + new_bindings[(*new_count)++] = nb; + (*match_count)++; + } +} + +/* Expand variable-length relationship via BFS */ +static void expand_var_length(cbm_store_t *store, cbm_rel_pattern_t *rel, + cbm_node_pattern_t *target_node, binding_t *b, cbm_node_t *src, + const char *to_var, binding_t *new_bindings, int *new_count, + int max_new, int *match_count) { + int max_depth = rel->max_hops > 0 ? rel->max_hops : 10; + cbm_traverse_result_t tr = {0}; + const char *dir = rel->direction ? rel->direction : "outbound"; + cbm_store_bfs(store, src->id, dir, rel->types, rel->type_count, max_depth, 100, &tr); + for (int v = 0; v < tr.visited_count && *new_count < max_new; v++) { + cbm_node_hop_t *hop = &tr.visited[v]; + if (hop->hop < rel->min_hops) { + continue; + } + if (target_node->label && strcmp(hop->node.label, target_node->label) != 0) { + continue; + } + if (!check_inline_props(&hop->node, target_node->props, target_node->prop_count)) { + continue; + } + binding_t nb = {0}; + binding_copy(&nb, b); + binding_set(&nb, to_var, &hop->node); + new_bindings[(*new_count)++] = nb; + (*match_count)++; + } + cbm_store_traverse_free(&tr); +} + +/* Expand fixed-length (1-hop) relationship edges */ +static void expand_fixed_length(cbm_store_t *store, cbm_rel_pattern_t *rel, + cbm_node_pattern_t *target_node, binding_t *b, cbm_node_t *src, + const char *to_var, binding_t *new_bindings, int *new_count, + int max_new, int *match_count) { + bool is_inbound = rel->direction && strcmp(rel->direction, "inbound") == 0; + bool is_any = rel->direction && strcmp(rel->direction, "any") == 0; + const char *rel_var = rel->variable; + + if (rel->type_count > 0) { + for (int ti = 0; ti < rel->type_count; ti++) { + cbm_edge_t *edges = NULL; + int edge_count = 0; + if (is_inbound) { + cbm_store_find_edges_by_target_type(store, src->id, rel->types[ti], &edges, + &edge_count); + } else { + cbm_store_find_edges_by_source_type(store, src->id, rel->types[ti], &edges, + &edge_count); + } + process_edges(store, edges, edge_count, is_inbound, target_node, b, to_var, rel_var, + new_bindings, new_count, max_new, match_count); + cbm_store_free_edges(edges, edge_count); + } + if (is_any) { + for (int ti = 0; ti < rel->type_count; ti++) { + cbm_edge_t *edges = NULL; + int edge_count = 0; + cbm_store_find_edges_by_target_type(store, src->id, rel->types[ti], &edges, + &edge_count); + process_edges(store, edges, edge_count, true, target_node, b, to_var, rel_var, + new_bindings, new_count, max_new, match_count); + cbm_store_free_edges(edges, edge_count); + } + } + } else { + cbm_edge_t *edges = NULL; + int edge_count = 0; + if (is_inbound) { + cbm_store_find_edges_by_target(store, src->id, &edges, &edge_count); + } else { + cbm_store_find_edges_by_source(store, src->id, &edges, &edge_count); + } + process_edges(store, edges, edge_count, is_inbound, target_node, b, to_var, rel_var, + new_bindings, new_count, max_new, match_count); + cbm_store_free_edges(edges, edge_count); + if (is_any) { + edges = NULL; + edge_count = 0; + cbm_store_find_edges_by_target(store, src->id, &edges, &edge_count); + process_edges(store, edges, edge_count, true, target_node, b, to_var, rel_var, + new_bindings, new_count, max_new, match_count); + cbm_store_free_edges(edges, edge_count); + } + } +} + static void expand_pattern_rels(cbm_store_t *store, cbm_pattern_t *pat, binding_t **bindings, int *bind_count, const int *bind_cap, const char **var_name, bool is_optional) { @@ -2081,105 +2263,13 @@ static void expand_pattern_rels(cbm_store_t *store, cbm_pattern_t *pat, binding_ int match_count = 0; + int max_new = *bind_cap * 10; if (is_variable_length) { - int max_depth = rel->max_hops > 0 ? rel->max_hops : 10; - cbm_traverse_result_t tr = {0}; - const char *dir = rel->direction ? rel->direction : "outbound"; - cbm_store_bfs(store, src->id, dir, rel->types, rel->type_count, max_depth, 100, - &tr); - for (int v = 0; v < tr.visited_count && new_count < *bind_cap * 10; v++) { - cbm_node_hop_t *hop = &tr.visited[v]; - if (hop->hop < rel->min_hops) { - continue; - } - if (target_node->label && strcmp(hop->node.label, target_node->label) != 0) { - continue; - } - if (!check_inline_props(&hop->node, target_node->props, - target_node->prop_count)) { - continue; - } - binding_t nb = {0}; - binding_copy(&nb, b); - binding_set(&nb, to_var, &hop->node); - new_bindings[new_count++] = nb; - match_count++; - } - cbm_store_traverse_free(&tr); + expand_var_length(store, rel, target_node, b, src, to_var, new_bindings, &new_count, + max_new, &match_count); } else { - bool is_inbound = rel->direction && strcmp(rel->direction, "inbound") == 0; - bool is_any = rel->direction && strcmp(rel->direction, "any") == 0; - const char *rel_var = rel->variable; - -#define PROCESS_EDGES(edges_arr, edge_count_val, get_target_id) \ - for (int ei = 0; ei < (edge_count_val) && new_count < *bind_cap * 10; ei++) { \ - int64_t tid = (get_target_id); \ - cbm_node_t found = {0}; \ - if (cbm_store_find_node_by_id(store, tid, &found) != CBM_STORE_OK) \ - continue; \ - if (target_node->label && strcmp(found.label, target_node->label) != 0) { \ - node_fields_free(&found); \ - continue; \ - } \ - if (!check_inline_props(&found, target_node->props, target_node->prop_count)) { \ - node_fields_free(&found); \ - continue; \ - } \ - binding_t nb = {0}; \ - binding_copy(&nb, b); \ - binding_set(&nb, to_var, &found); \ - if (rel_var) \ - binding_set_edge(&nb, rel_var, &(edges_arr)[ei]); \ - node_fields_free(&found); \ - new_bindings[new_count++] = nb; \ - match_count++; \ - } - - if (rel->type_count > 0) { - for (int ti = 0; ti < rel->type_count; ti++) { - cbm_edge_t *edges = NULL; - int edge_count = 0; - if (is_inbound) { - cbm_store_find_edges_by_target_type(store, src->id, rel->types[ti], - &edges, &edge_count); - } else { - cbm_store_find_edges_by_source_type(store, src->id, rel->types[ti], - &edges, &edge_count); - } - PROCESS_EDGES(edges, edge_count, - is_inbound ? edges[ei].source_id : edges[ei].target_id); - cbm_store_free_edges(edges, edge_count); - } - if (is_any) { - for (int ti = 0; ti < rel->type_count; ti++) { - cbm_edge_t *edges = NULL; - int edge_count = 0; - cbm_store_find_edges_by_target_type(store, src->id, rel->types[ti], - &edges, &edge_count); - PROCESS_EDGES(edges, edge_count, edges[ei].source_id); - cbm_store_free_edges(edges, edge_count); - } - } - } else { - cbm_edge_t *edges = NULL; - int edge_count = 0; - if (is_inbound) { - cbm_store_find_edges_by_target(store, src->id, &edges, &edge_count); - } else { - cbm_store_find_edges_by_source(store, src->id, &edges, &edge_count); - } - PROCESS_EDGES(edges, edge_count, - is_inbound ? edges[ei].source_id : edges[ei].target_id); - cbm_store_free_edges(edges, edge_count); - if (is_any) { - edges = NULL; - edge_count = 0; - cbm_store_find_edges_by_target(store, src->id, &edges, &edge_count); - PROCESS_EDGES(edges, edge_count, edges[ei].source_id); - cbm_store_free_edges(edges, edge_count); - } - } -#undef PROCESS_EDGES + expand_fixed_length(store, rel, target_node, b, src, to_var, new_bindings, + &new_count, max_new, &match_count); } /* OPTIONAL MATCH: keep binding with empty target if no matches */ @@ -2203,46 +2293,53 @@ static void expand_pattern_rels(cbm_store_t *store, cbm_pattern_t *pat, binding_ /* ── Result postprocessing helpers ─────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Find the column index for ORDER BY, checking both column names and aliases. + * Returns -1 if not found. */ +static int rb_find_order_column(const result_builder_t *rb, const cbm_return_clause_t *ret) { + for (int ci = 0; ci < rb->col_count; ci++) { + if (strcmp(rb->columns[ci], ret->order_by) == 0) { + return ci; + } + } + for (int ci = 0; ci < ret->count; ci++) { + if (ret->items[ci].alias && strcmp(ret->items[ci].alias, ret->order_by) == 0) { + return ci; + } + } + return -1; +} + +/* Check whether a column contains numeric data by examining the first non-empty value */ +static bool rb_is_numeric_column(const result_builder_t *rb, int col) { + for (int i = 0; i < rb->row_count; i++) { + const char *v = rb->rows[i][col]; + if (v && *v) { + const char *p2 = (*v == '-') ? v + 1 : v; + if (*p2 == '\0') { + return false; + } + for (; *p2; p2++) { + if (*p2 < '0' || *p2 > '9') { + return false; + } + } + return true; + } + } + return false; +} + static void rb_apply_order_by(result_builder_t *rb, const cbm_return_clause_t *ret) { if (!ret->order_by) { return; } - int order_col = -1; - for (int ci = 0; ci < rb->col_count; ci++) { - if (strcmp(rb->columns[ci], ret->order_by) == 0) { - order_col = ci; - break; - } - } - if (order_col < 0) { - for (int ci = 0; ci < ret->count; ci++) { - if (ret->items[ci].alias && strcmp(ret->items[ci].alias, ret->order_by) == 0) { - order_col = ci; - break; - } - } - } + int order_col = rb_find_order_column(rb, ret); if (order_col < 0) { return; } bool desc = ret->order_dir && strcmp(ret->order_dir, "DESC") == 0; - bool numeric = false; - for (int i = 0; i < rb->row_count; i++) { - const char *v = rb->rows[i][order_col]; - if (v && *v) { - const char *p2 = (*v == '-') ? v + 1 : v; - numeric = (*p2 != '\0'); - for (; *p2; p2++) { - if (*p2 < '0' || *p2 > '9') { - numeric = false; - break; - } - } - break; - } - } + bool numeric = rb_is_numeric_column(rb, order_col); for (int i = 0; i < rb->row_count - 1; i++) { for (int j = 0; j < rb->row_count - i - 1; j++) { int cmp; @@ -2355,9 +2452,271 @@ static const char *project_item(binding_t *b, cbm_return_item_t *item, char *fun return raw; } +/* Check if a function name is an aggregate */ +static bool is_aggregate_func(const char *func) { + return func && + (strcmp(func, "COUNT") == 0 || strcmp(func, "SUM") == 0 || strcmp(func, "AVG") == 0 || + strcmp(func, "MIN") == 0 || strcmp(func, "MAX") == 0 || strcmp(func, "COLLECT") == 0); +} + +/* Sort bindings by a virtual variable using bubble sort */ +static void sort_bindings(binding_t *vbindings, int count, const char *key, bool desc) { + for (int i = 0; i < count - 1; i++) { + for (int j = 0; j < count - i - 1; j++) { + const char *va = binding_get_virtual(&vbindings[j], key, NULL); + const char *vb2 = binding_get_virtual(&vbindings[j + 1], key, NULL); + char *ea = NULL; + char *eb = NULL; + double da = strtod(va, &ea); + double db = strtod(vb2, &eb); + int cmp = (ea != va && eb != vb2) ? ((da > db) - (da < db)) : strcmp(va, vb2); + if (desc ? cmp < 0 : cmp > 0) { + binding_t tmp = vbindings[j]; + vbindings[j] = vbindings[j + 1]; + vbindings[j + 1] = tmp; + } + } + } +} + +/* Apply skip and limit to a binding array, freeing discarded entries */ +static void bindings_skip_limit(binding_t *vbindings, int *count, int skip, int limit) { + if (skip > 0 && skip < *count) { + for (int i = 0; i < skip; i++) { + binding_free(&vbindings[i]); + } + memmove(vbindings, vbindings + skip, (*count - skip) * sizeof(binding_t)); + *count -= skip; + } else if (skip >= *count) { + for (int i = 0; i < *count; i++) { + binding_free(&vbindings[i]); + } + *count = 0; + } + if (limit > 0 && *count > limit) { + for (int i = limit; i < *count; i++) { + binding_free(&vbindings[i]); + } + *count = limit; + } +} + +/* Sort, skip, and limit binding array in-place */ +static void with_sort_skip_limit(const cbm_return_clause_t *wc, binding_t *vbindings, int *vcount) { + if (wc->order_by) { + bool wdesc = wc->order_dir && strcmp(wc->order_dir, "DESC") == 0; + sort_bindings(vbindings, *vcount, wc->order_by, wdesc); + } + bindings_skip_limit(vbindings, vcount, wc->skip, wc->limit); +} + +/* Resolve the alias or compute a default name for a WITH/RETURN item */ +static const char *resolve_item_alias(const cbm_return_item_t *item, char *name_buf, + size_t buf_sz) { + if (item->alias) { + return item->alias; + } + if (item->property) { + snprintf(name_buf, buf_sz, "%s.%s", item->variable, item->property); + } else { + snprintf(name_buf, buf_sz, "%s", item->variable); + } + return name_buf; +} + /* ── WITH clause: project bindings through aggregation or rename ── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity,readability-function-size) +/* WITH aggregation group entry */ +typedef struct { + char group_key[1024]; + const char **group_vals; + double *sums; + int *counts; + double *mins, *maxs; +} with_agg_t; + +/* Build a group key from non-aggregate WITH items */ +static int with_agg_build_key(cbm_return_clause_t *wc, binding_t *b, char *key, size_t key_sz) { + int kl = 0; + for (int ci = 0; ci < wc->count; ci++) { + if (wc->items[ci].func) { + continue; + } + const char *v = binding_get_virtual(b, wc->items[ci].variable, wc->items[ci].property); + kl += snprintf(key + kl, key_sz - (size_t)kl, "%s|", v); + if (kl >= (int)key_sz) { + kl = (int)key_sz - 1; + } + } + return kl; +} + +/* Find or create an aggregation group. Returns index. */ +static int with_agg_find_or_create(with_agg_t **aggs, int *agg_cnt, int *agg_cap, + cbm_return_clause_t *wc, binding_t *b, const char *key) { + for (int a = 0; a < *agg_cnt; a++) { + if (strcmp((*aggs)[a].group_key, key) == 0) { + return a; + } + } + if (*agg_cnt >= *agg_cap) { + *agg_cap *= 2; + *aggs = safe_realloc(*aggs, *agg_cap * sizeof(with_agg_t)); + } + int found = (*agg_cnt)++; + snprintf((*aggs)[found].group_key, sizeof((*aggs)[found].group_key), "%s", key); + (*aggs)[found].group_vals = malloc(wc->count * sizeof(const char *)); + (*aggs)[found].sums = calloc(wc->count, sizeof(double)); + (*aggs)[found].counts = calloc(wc->count, sizeof(int)); + (*aggs)[found].mins = calloc(wc->count, sizeof(double)); + (*aggs)[found].maxs = calloc(wc->count, sizeof(double)); + for (int ci = 0; ci < wc->count; ci++) { + (*aggs)[found].mins[ci] = 1e308; + (*aggs)[found].maxs[ci] = -1e308; + } + for (int ci = 0; ci < wc->count; ci++) { + if (wc->items[ci].func) { + (*aggs)[found].group_vals[ci] = heap_strdup("0"); + continue; + } + const char *v = binding_get_virtual(b, wc->items[ci].variable, wc->items[ci].property); + (*aggs)[found].group_vals[ci] = heap_strdup(v); + } + return found; +} + +/* Accumulate aggregation values for a binding */ +static void with_agg_accumulate(with_agg_t *agg, cbm_return_clause_t *wc, binding_t *b) { + for (int ci = 0; ci < wc->count; ci++) { + if (!wc->items[ci].func) { + continue; + } + agg->counts[ci]++; + const char *raw = binding_get_virtual(b, wc->items[ci].variable, wc->items[ci].property); + double dv = strtod(raw, NULL); + agg->sums[ci] += dv; + if (dv < agg->mins[ci]) { + agg->mins[ci] = dv; + } + if (dv > agg->maxs[ci]) { + agg->maxs[ci] = dv; + } + } +} + +/* Format a WITH aggregation value into buf */ +static void with_agg_format(const char *func, with_agg_t *agg, int ci, char *buf, size_t buf_sz) { + if (strcmp(func, "COUNT") == 0) { + snprintf(buf, buf_sz, "%d", agg->counts[ci]); + } else if (strcmp(func, "SUM") == 0) { + snprintf(buf, buf_sz, "%.10g", agg->sums[ci]); + } else if (strcmp(func, "AVG") == 0) { + snprintf(buf, buf_sz, "%.10g", agg->counts[ci] > 0 ? agg->sums[ci] / agg->counts[ci] : 0.0); + } else if (strcmp(func, "MIN") == 0) { + snprintf(buf, buf_sz, "%.10g", agg->mins[ci]); + } else if (strcmp(func, "MAX") == 0) { + snprintf(buf, buf_sz, "%.10g", agg->maxs[ci]); + } else { + snprintf(buf, buf_sz, "%d", agg->counts[ci]); + } +} + +/* Add a virtual variable binding for one WITH item */ +static void with_add_vbinding_var(binding_t *vb, const char *alias, const char *val) { + cbm_node_t vn = {.name = heap_strdup(val), .qualified_name = heap_strdup(alias)}; + if (vb->var_count < 16) { + vb->var_names[vb->var_count] = vn.qualified_name; + vb->var_nodes[vb->var_count] = vn; + vb->var_count++; + } +} + +/* Free with_agg_t array */ +static void with_agg_free(with_agg_t *aggs, int agg_cnt, int item_count) { + for (int a = 0; a < agg_cnt; a++) { + for (int ci = 0; ci < item_count; ci++) { + free((void *)aggs[a].group_vals[ci]); + } + free(aggs[a].group_vals); + free(aggs[a].sums); + free(aggs[a].counts); + free(aggs[a].mins); + free(aggs[a].maxs); + } + free(aggs); +} + +/* Execute WITH aggregation path */ +static void execute_with_aggregate(cbm_return_clause_t *wc, binding_t *bindings, int bind_count, + binding_t **vbindings, int *vcount) { + int agg_cap = 256; + with_agg_t *aggs = calloc(agg_cap, sizeof(with_agg_t)); + int agg_cnt = 0; + + for (int bi = 0; bi < bind_count; bi++) { + char key[1024] = ""; + with_agg_build_key(wc, &bindings[bi], key, sizeof(key)); + int found = with_agg_find_or_create(&aggs, &agg_cnt, &agg_cap, wc, &bindings[bi], key); + with_agg_accumulate(&aggs[found], wc, &bindings[bi]); + } + + *vbindings = safe_realloc(*vbindings, (agg_cnt + 1) * sizeof(binding_t)); + if (!*vbindings) { + with_agg_free(aggs, agg_cnt, wc->count); + return; + } + for (int a = 0; a < agg_cnt; a++) { + binding_t vb = {0}; + for (int ci = 0; ci < wc->count; ci++) { + char name_buf[256]; + const char *alias = resolve_item_alias(&wc->items[ci], name_buf, sizeof(name_buf)); + if (wc->items[ci].func) { + char vbuf[64]; + with_agg_format(wc->items[ci].func, &aggs[a], ci, vbuf, sizeof(vbuf)); + with_add_vbinding_var(&vb, alias, vbuf); + } else { + with_add_vbinding_var(&vb, alias, aggs[a].group_vals[ci]); + } + } + (*vbindings)[(*vcount)++] = vb; + } + with_agg_free(aggs, agg_cnt, wc->count); +} + +/* Execute WITH simple (non-aggregate) projection */ +static void execute_with_simple(cbm_return_clause_t *wc, binding_t *bindings, int bind_count, + binding_t *vbindings, int *vcount) { + for (int bi = 0; bi < bind_count; bi++) { + binding_t vb = {0}; + for (int ci = 0; ci < wc->count; ci++) { + char name_buf[256]; + const char *alias = resolve_item_alias(&wc->items[ci], name_buf, sizeof(name_buf)); + char func_buf[512]; + const char *val = + project_item(&bindings[bi], &wc->items[ci], func_buf, sizeof(func_buf)); + with_add_vbinding_var(&vb, alias, val); + } + vbindings[(*vcount)++] = vb; + } +} + +/* Apply post-WITH WHERE filter */ +static void filter_bindings_where(const cbm_where_clause_t *where, binding_t *vbindings, + int *vcount) { + int kept = 0; + for (int i = 0; i < *vcount; i++) { + if (eval_where(where, &vbindings[i])) { + if (kept != i) { + vbindings[kept] = vbindings[i]; + } + kept++; + } else { + binding_free(&vbindings[i]); + } + } + *vcount = kept; +} + static void execute_with_clause(cbm_query_t *q, binding_t **bindings_ptr, int *bind_count_ptr) { cbm_return_clause_t *wc = q->with_clause; if (!wc) { @@ -2369,252 +2728,29 @@ static void execute_with_clause(cbm_query_t *q, binding_t **bindings_ptr, int *b binding_t *vbindings = malloc((bind_count + 1) * sizeof(binding_t)); int vcount = 0; - /* Check if WITH has aggregation */ bool has_agg = false; for (int i = 0; i < wc->count; i++) { - if (wc->items[i].func && - (strcmp(wc->items[i].func, "COUNT") == 0 || strcmp(wc->items[i].func, "SUM") == 0 || - strcmp(wc->items[i].func, "AVG") == 0 || strcmp(wc->items[i].func, "MIN") == 0 || - strcmp(wc->items[i].func, "MAX") == 0 || strcmp(wc->items[i].func, "COLLECT") == 0)) { + if (is_aggregate_func(wc->items[i].func)) { has_agg = true; break; } } if (has_agg) { - typedef struct { - char group_key[1024]; - const char **group_vals; - double *sums; - int *counts; - double *mins, *maxs; - } with_agg_t; - int agg_cap = 256; - with_agg_t *aggs = calloc(agg_cap, sizeof(with_agg_t)); - int agg_cnt = 0; - - for (int bi = 0; bi < bind_count; bi++) { - char key[1024] = ""; - int kl = 0; - for (int ci = 0; ci < wc->count; ci++) { - if (wc->items[ci].func) { - continue; - } - const char *v = binding_get_virtual(&bindings[bi], wc->items[ci].variable, - wc->items[ci].property); - kl += snprintf(key + kl, sizeof(key) - (size_t)kl, "%s|", v); - if (kl >= (int)sizeof(key)) { - kl = (int)sizeof(key) - 1; - } - } - int found = -1; - for (int a = 0; a < agg_cnt; a++) { - if (strcmp(aggs[a].group_key, key) == 0) { - found = a; - break; - } - } - if (found < 0) { - if (agg_cnt >= agg_cap) { - agg_cap *= 2; - aggs = safe_realloc(aggs, agg_cap * sizeof(with_agg_t)); - } - found = agg_cnt++; - snprintf(aggs[found].group_key, sizeof(aggs[found].group_key), "%s", key); - aggs[found].group_vals = malloc(wc->count * sizeof(const char *)); - aggs[found].sums = calloc(wc->count, sizeof(double)); - aggs[found].counts = calloc(wc->count, sizeof(int)); - aggs[found].mins = malloc(wc->count * sizeof(double)); - aggs[found].maxs = malloc(wc->count * sizeof(double)); - for (int ci = 0; ci < wc->count; ci++) { - aggs[found].mins[ci] = 1e308; - aggs[found].maxs[ci] = -1e308; - } - for (int ci = 0; ci < wc->count; ci++) { - if (wc->items[ci].func) { - aggs[found].group_vals[ci] = heap_strdup("0"); - continue; - } - const char *v = binding_get_virtual(&bindings[bi], wc->items[ci].variable, - wc->items[ci].property); - aggs[found].group_vals[ci] = heap_strdup(v); - } - } - for (int ci = 0; ci < wc->count; ci++) { - if (!wc->items[ci].func) { - continue; - } - aggs[found].counts[ci]++; - const char *raw = binding_get_virtual(&bindings[bi], wc->items[ci].variable, - wc->items[ci].property); - double dv = strtod(raw, NULL); - aggs[found].sums[ci] += dv; - if (dv < aggs[found].mins[ci]) { - aggs[found].mins[ci] = dv; - } - if (dv > aggs[found].maxs[ci]) { - aggs[found].maxs[ci] = dv; - } - } - } - - vbindings = safe_realloc(vbindings, (agg_cnt + 1) * sizeof(binding_t)); - for (int a = 0; a < agg_cnt; a++) { - binding_t vb = {0}; - for (int ci = 0; ci < wc->count; ci++) { - const char *alias = wc->items[ci].alias; - char name_buf[256]; - if (!alias) { - if (wc->items[ci].property) { - snprintf(name_buf, sizeof(name_buf), "%s.%s", wc->items[ci].variable, - wc->items[ci].property); - } else { - snprintf(name_buf, sizeof(name_buf), "%s", wc->items[ci].variable); - } - alias = name_buf; - } - char vbuf[64]; - if (wc->items[ci].func) { - const char *f = wc->items[ci].func; - if (strcmp(f, "COUNT") == 0) { - snprintf(vbuf, sizeof(vbuf), "%d", aggs[a].counts[ci]); - } else if (strcmp(f, "SUM") == 0) { - snprintf(vbuf, sizeof(vbuf), "%.10g", aggs[a].sums[ci]); - } else if (strcmp(f, "AVG") == 0) { - snprintf(vbuf, sizeof(vbuf), "%.10g", - aggs[a].counts[ci] > 0 ? aggs[a].sums[ci] / aggs[a].counts[ci] - : 0); - } else if (strcmp(f, "MIN") == 0) { - snprintf(vbuf, sizeof(vbuf), "%.10g", aggs[a].mins[ci]); - } else if (strcmp(f, "MAX") == 0) { - snprintf(vbuf, sizeof(vbuf), "%.10g", aggs[a].maxs[ci]); - } else { - snprintf(vbuf, sizeof(vbuf), "%d", aggs[a].counts[ci]); - } - cbm_node_t vn = {.name = heap_strdup(vbuf), - .qualified_name = heap_strdup(alias)}; - if (vb.var_count < 16) { - vb.var_names[vb.var_count] = vn.qualified_name; - vb.var_nodes[vb.var_count] = vn; - vb.var_count++; - } - } else { - cbm_node_t vn = {.name = heap_strdup(aggs[a].group_vals[ci]), - .qualified_name = heap_strdup(alias)}; - if (vb.var_count < 16) { - vb.var_names[vb.var_count] = vn.qualified_name; - vb.var_nodes[vb.var_count] = vn; - vb.var_count++; - } - } - } - vbindings[vcount++] = vb; - } - for (int a = 0; a < agg_cnt; a++) { - for (int ci = 0; ci < wc->count; ci++) { - free((void *)aggs[a].group_vals[ci]); - } - free(aggs[a].group_vals); - free(aggs[a].sums); - free(aggs[a].counts); - free(aggs[a].mins); - free(aggs[a].maxs); - } - free(aggs); + execute_with_aggregate(wc, bindings, bind_count, &vbindings, &vcount); } else { - for (int bi = 0; bi < bind_count; bi++) { - binding_t vb = {0}; - for (int ci = 0; ci < wc->count; ci++) { - const char *alias = wc->items[ci].alias; - char name_buf[256]; - if (!alias) { - if (wc->items[ci].property) { - snprintf(name_buf, sizeof(name_buf), "%s.%s", wc->items[ci].variable, - wc->items[ci].property); - } else { - snprintf(name_buf, sizeof(name_buf), "%s", wc->items[ci].variable); - } - alias = name_buf; - } - char func_buf[512]; - const char *val = - project_item(&bindings[bi], &wc->items[ci], func_buf, sizeof(func_buf)); - cbm_node_t vn = {.name = heap_strdup(val), .qualified_name = heap_strdup(alias)}; - if (vb.var_count < 16) { - vb.var_names[vb.var_count] = vn.qualified_name; - vb.var_nodes[vb.var_count] = vn; - vb.var_count++; - } - } - vbindings[vcount++] = vb; - } + execute_with_simple(wc, bindings, bind_count, vbindings, &vcount); } - /* ORDER BY + SKIP + LIMIT */ - if (wc->order_by) { - const char *ob = wc->order_by; - bool wdesc = (wc->order_dir && strcmp(wc->order_dir, "DESC") == 0) != 0; - for (int i2 = 0; i2 < vcount - 1; i2++) { - for (int j2 = 0; j2 < vcount - i2 - 1; j2++) { - const char *va = binding_get_virtual(&vbindings[j2], ob, NULL); - const char *vb2 = binding_get_virtual(&vbindings[j2 + 1], ob, NULL); - int cmp2; - char *ea = NULL; - char *eb = NULL; - double da = strtod(va, &ea); - double db = strtod(vb2, &eb); - if (ea != va && eb != vb2) { - cmp2 = (da > db) - (da < db); - } else { - cmp2 = strcmp(va, vb2); - } - if ((int)wdesc ? cmp2 < 0 : cmp2 > 0) { - binding_t tmp2 = vbindings[j2]; - vbindings[j2] = vbindings[j2 + 1]; - vbindings[j2 + 1] = tmp2; - } - } - } - } - if (wc->skip > 0 && wc->skip < vcount) { - for (int i2 = 0; i2 < wc->skip; i2++) { - binding_free(&vbindings[i2]); - } - memmove(vbindings, vbindings + wc->skip, (vcount - wc->skip) * sizeof(binding_t)); - vcount -= wc->skip; - } else if (wc->skip >= vcount) { - for (int i2 = 0; i2 < vcount; i2++) { - binding_free(&vbindings[i2]); - } - vcount = 0; - } - if (wc->limit > 0 && vcount > wc->limit) { - for (int i2 = wc->limit; i2 < vcount; i2++) { - binding_free(&vbindings[i2]); - } - vcount = wc->limit; - } + with_sort_skip_limit(wc, vbindings, &vcount); - /* Replace bindings with virtual */ for (int bi = 0; bi < bind_count; bi++) { binding_free(&bindings[bi]); } free(bindings); - /* Post-WITH WHERE */ if (q->post_with_where) { - int kept = 0; - for (int i = 0; i < vcount; i++) { - if (eval_where(q->post_with_where, &vbindings[i])) { - if (kept != i) { - vbindings[kept] = vbindings[i]; - } - kept++; - } else { - binding_free(&vbindings[i]); - } - } - vcount = kept; + filter_bindings_where(q->post_with_where, vbindings, &vcount); } *bindings_ptr = vbindings; @@ -2623,35 +2759,498 @@ static void execute_with_clause(cbm_query_t *q, binding_t **bindings_ptr, int *b /* ── Execute a single query (no UNION recursion) ──────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity,readability-function-size) +/* Project RETURN * — all bound variable properties */ +/* Collect all variable names from query patterns */ +static int collect_pattern_vars(cbm_query_t *q, const char **vars, int max_vars) { + int vc = 0; + for (int pi = 0; pi < q->pattern_count; pi++) { + for (int ni = 0; ni < q->patterns[pi].node_count && vc < max_vars; ni++) { + if (q->patterns[pi].nodes[ni].variable) { + vars[vc++] = q->patterns[pi].nodes[ni].variable; + } + } + for (int ri = 0; ri < q->patterns[pi].rel_count && vc < max_vars; ri++) { + if (q->patterns[pi].rels[ri].variable) { + vars[vc++] = q->patterns[pi].rels[ri].variable; + } + } + } + return vc; +} + +/* Build star-projection columns: var.name, var.qualified_name, var.label, var.file_path */ +static void build_star_columns(result_builder_t *rb, const char **vars, int vc) { + int col_n = vc * 4; + const char *col_names[128]; + for (int v = 0; v < vc; v++) { + char buf[128]; + snprintf(buf, sizeof(buf), "%s.name", vars[v]); + col_names[(size_t)v * 4] = heap_strdup(buf); + snprintf(buf, sizeof(buf), "%s.qualified_name", vars[v]); + col_names[((size_t)v * 4) + 1] = heap_strdup(buf); + snprintf(buf, sizeof(buf), "%s.label", vars[v]); + col_names[((size_t)v * 4) + 2] = heap_strdup(buf); + snprintf(buf, sizeof(buf), "%s.file_path", vars[v]); + col_names[((size_t)v * 4) + 3] = heap_strdup(buf); + } + rb_set_columns(rb, col_names, col_n); + for (int i = 0; i < col_n; i++) { + free((void *)col_names[i]); + } +} + +/* Project one variable's 4 columns for RETURN * */ +static void project_star_var(binding_t *b, const char *var, const char **vals) { + cbm_edge_t *edge = binding_get_edge(b, var); + if (edge) { + vals[0] = edge_prop(edge, "type"); + vals[1] = ""; + vals[2] = ""; + vals[3] = ""; + return; + } + cbm_node_t *n = binding_get(b, var); + vals[0] = n && n->name ? n->name : ""; + vals[1] = n && n->qualified_name ? n->qualified_name : ""; + vals[2] = n && n->label ? n->label : ""; + vals[3] = n && n->file_path ? n->file_path : ""; +} + +/* Project one binding row for RETURN * */ +static void project_star_row(binding_t *b, const char **vars, int vc, const char **vals) { + for (int v = 0; v < vc; v++) { + project_star_var(b, vars[v], vals + ((size_t)v * 4)); + } +} + +static void execute_return_star(cbm_query_t *q, binding_t *bindings, int bind_count, int max_rows, + result_builder_t *rb) { + const char *vars[32]; + int vc = collect_pattern_vars(q, vars, 32); + build_star_columns(rb, vars, vc); + for (int bi = 0; bi < bind_count && rb->row_count < max_rows; bi++) { + const char *vals[128]; + project_star_row(&bindings[bi], vars, vc, vals); + rb_add_row(rb, vals); + } +} + +/* Format an aggregate value into buf based on function name */ +/* Format a COLLECT list as JSON array string */ +static void format_collect_list(char **items, int item_count, char *buf, size_t buf_sz) { + char cbuf[2048] = "["; + int bl = 1; + for (int i = 0; i < item_count; i++) { + if (i > 0) { + cbuf[bl++] = ','; + } + bl += snprintf(cbuf + bl, sizeof(cbuf) - (size_t)bl, "\"%s\"", items[i]); + if (bl >= (int)sizeof(cbuf)) { + bl = (int)sizeof(cbuf) - 1; + } + } + if (bl < (int)sizeof(cbuf) - 1) { + cbuf[bl++] = ']'; + } + cbuf[bl] = '\0'; + snprintf(buf, buf_sz, "%s", cbuf); +} + +static void format_agg_value(const char *func, int count, double sum, double min_val, + double max_val, char ***collect_lists, int *collect_counts, int ci, + char *buf, size_t buf_sz) { + if (strcmp(func, "COUNT") == 0) { + snprintf(buf, buf_sz, "%d", count); + } else if (strcmp(func, "SUM") == 0) { + snprintf(buf, buf_sz, "%.10g", sum); + } else if (strcmp(func, "AVG") == 0) { + snprintf(buf, buf_sz, "%.10g", count > 0 ? sum / count : 0.0); + } else if (strcmp(func, "MIN") == 0) { + snprintf(buf, buf_sz, "%.10g", min_val); + } else if (strcmp(func, "MAX") == 0) { + snprintf(buf, buf_sz, "%.10g", max_val); + } else if (strcmp(func, "COLLECT") == 0) { + format_collect_list(collect_lists[ci], collect_counts[ci], buf, buf_sz); + } else { + snprintf(buf, buf_sz, "%d", count); + } +} + +/* RETURN aggregation entry */ +typedef struct { + char group_key[1024]; + const char **group_vals; + double *sums; + int *counts; + double *mins, *maxs; + char ***collect_lists; + int *collect_counts; +} ret_agg_entry_t; + +/* Initialize a new RETURN aggregation group */ +static void ret_agg_init_group(ret_agg_entry_t *entry, const char *key, int item_count, + const char **vals) { + snprintf(entry->group_key, sizeof(entry->group_key), "%s", key); + entry->group_vals = malloc(item_count * sizeof(const char *)); + entry->sums = calloc(item_count, sizeof(double)); + entry->counts = calloc(item_count, sizeof(int)); + entry->mins = malloc(item_count * sizeof(double)); + entry->maxs = malloc(item_count * sizeof(double)); + entry->collect_lists = calloc(item_count, sizeof(char **)); + entry->collect_counts = calloc(item_count, sizeof(int)); + for (int ci = 0; ci < item_count; ci++) { + entry->mins[ci] = 1e308; + entry->maxs[ci] = -1e308; + entry->group_vals[ci] = heap_strdup(vals[ci]); + } +} + +/* Accumulate a binding into RETURN aggregation */ +static void ret_agg_accumulate(ret_agg_entry_t *entry, cbm_return_clause_t *ret, binding_t *b) { + for (int ci = 0; ci < ret->count; ci++) { + if (!ret->items[ci].func) { + continue; + } + entry->counts[ci]++; + const char *raw = binding_get_virtual(b, ret->items[ci].variable, ret->items[ci].property); + double dv = strtod(raw, NULL); + entry->sums[ci] += dv; + if (dv < entry->mins[ci]) { + entry->mins[ci] = dv; + } + if (dv > entry->maxs[ci]) { + entry->maxs[ci] = dv; + } + if (strcmp(ret->items[ci].func, "COLLECT") == 0) { + int idx = entry->collect_counts[ci]++; + entry->collect_lists[ci] = + safe_realloc(entry->collect_lists[ci], (idx + 1) * sizeof(char *)); + entry->collect_lists[ci][idx] = heap_strdup(raw); + } + } +} + +/* Free RETURN aggregation entries */ +static void ret_agg_free(ret_agg_entry_t *aggs, int agg_count, int item_count) { + for (int a = 0; a < agg_count; a++) { + for (int ci = 0; ci < item_count; ci++) { + free((void *)aggs[a].group_vals[ci]); + for (int j = 0; j < aggs[a].collect_counts[ci]; j++) { + free(aggs[a].collect_lists[ci][j]); + } + free(aggs[a].collect_lists[ci]); + } + free(aggs[a].group_vals); + free(aggs[a].sums); + free(aggs[a].counts); + free(aggs[a].mins); + free(aggs[a].maxs); + free(aggs[a].collect_lists); + free(aggs[a].collect_counts); + } + free(aggs); +} + +/* Execute RETURN with aggregation */ +/* Build group key and projected values for one binding */ +static void ret_agg_build_key(cbm_return_clause_t *ret, binding_t *b, char *key, size_t key_sz, + const char **vals) { + int klen = 0; + for (int ci = 0; ci < ret->count; ci++) { + if (ret->items[ci].func) { + vals[ci] = "0"; + continue; + } + char func_buf[512]; + vals[ci] = project_item(b, &ret->items[ci], func_buf, sizeof(func_buf)); + klen += snprintf(key + klen, key_sz - (size_t)klen, "%s|", vals[ci]); + if (klen >= (int)key_sz) { + klen = (int)key_sz - 1; + } + } +} + +/* Emit one aggregated row into the result builder */ +static void ret_agg_emit_row(cbm_return_clause_t *ret, ret_agg_entry_t *agg, result_builder_t *rb) { + const char *row[32]; + char bufs[32][64]; + for (int ci = 0; ci < ret->count; ci++) { + if (!ret->items[ci].func) { + row[ci] = agg->group_vals[ci]; + continue; + } + format_agg_value(ret->items[ci].func, agg->counts[ci], agg->sums[ci], agg->mins[ci], + agg->maxs[ci], agg->collect_lists, agg->collect_counts, ci, bufs[ci], + sizeof(bufs[ci])); + row[ci] = bufs[ci]; + } + rb_add_row(rb, row); +} + +static void execute_return_agg(cbm_return_clause_t *ret, binding_t *bindings, int bind_count, + result_builder_t *rb) { + int agg_cap = 256; + ret_agg_entry_t *aggs = calloc(agg_cap, sizeof(ret_agg_entry_t)); + int agg_count = 0; + + for (int bi = 0; bi < bind_count; bi++) { + char key[1024] = ""; + const char *vals[32]; + ret_agg_build_key(ret, &bindings[bi], key, sizeof(key), vals); + + int found = -1; + for (int a = 0; a < agg_count; a++) { + if (strcmp(aggs[a].group_key, key) == 0) { + found = a; + break; + } + } + if (found < 0) { + if (agg_count >= agg_cap) { + agg_cap *= 2; + aggs = safe_realloc(aggs, agg_cap * sizeof(ret_agg_entry_t)); + } + found = agg_count++; + ret_agg_init_group(&aggs[found], key, ret->count, vals); + } + ret_agg_accumulate(&aggs[found], ret, &bindings[bi]); + } + + for (int a = 0; a < agg_count; a++) { + ret_agg_emit_row(ret, &aggs[a], rb); + } + ret_agg_free(aggs, agg_count, ret->count); +} + +/* Build RETURN column names from items */ +static void build_return_columns(result_builder_t *rb, cbm_return_clause_t *ret) { + const char *col_names[32]; + for (int i = 0; i < ret->count && i < 32; i++) { + cbm_return_item_t *item = &ret->items[i]; + if (item->alias) { + col_names[i] = item->alias; + } else if (item->func) { + char buf[128]; + snprintf(buf, sizeof(buf), "%s(%s)", item->func, item->variable); + col_names[i] = heap_strdup(buf); + } else if (item->kase) { + col_names[i] = "CASE"; + } else if (item->property) { + char buf[128]; + snprintf(buf, sizeof(buf), "%s.%s", item->variable, item->property); + col_names[i] = heap_strdup(buf); + } else { + col_names[i] = item->variable; + } + } + rb_set_columns(rb, col_names, ret->count); + for (int i = 0; i < ret->count && i < 32; i++) { + cbm_return_item_t *item = &ret->items[i]; + if (!item->alias && (item->func || (!item->kase && item->property))) { + free((void *)col_names[i]); + } + } +} + +/* Execute simple (non-aggregate) RETURN projection */ +static void execute_return_simple(cbm_return_clause_t *ret, binding_t *bindings, int bind_count, + int max_rows, result_builder_t *rb) { + int proj_cap = max_rows; + if (ret->limit > 0 && !ret->order_by && ret->skip <= 0) { + proj_cap = ret->limit; + } + for (int bi = 0; bi < bind_count && rb->row_count < proj_cap; bi++) { + const char *vals[32]; + char func_bufs[32][512]; + for (int ci = 0; ci < ret->count; ci++) { + vals[ci] = + project_item(&bindings[bi], &ret->items[ci], func_bufs[ci], sizeof(func_bufs[ci])); + } + rb_add_row(rb, vals); + } +} + +/* Build default 3-column headers (name, qualified_name, label) per variable */ +static void build_default_columns(result_builder_t *rb, const char **vars, int vc) { + int col_n = vc * 3; + const char *col_names[48]; + for (int v = 0; v < vc; v++) { + char buf[128]; + snprintf(buf, sizeof(buf), "%s.name", vars[v]); + col_names[(size_t)v * 3] = heap_strdup(buf); + snprintf(buf, sizeof(buf), "%s.qualified_name", vars[v]); + col_names[((size_t)v * 3) + 1] = heap_strdup(buf); + snprintf(buf, sizeof(buf), "%s.label", vars[v]); + col_names[((size_t)v * 3) + 2] = heap_strdup(buf); + } + rb_set_columns(rb, col_names, col_n); + for (int i = 0; i < col_n; i++) { + free((void *)col_names[i]); + } +} + +/* Default projection when no RETURN clause */ +static void execute_default_projection(cbm_pattern_t *pat0, binding_t *bindings, int bind_count, + int max_rows, result_builder_t *rb) { + const char *vars[16]; + int vc = 0; + for (int ni = 0; ni < pat0->node_count && vc < 16; ni++) { + if (pat0->nodes[ni].variable) { + vars[vc++] = pat0->nodes[ni].variable; + } + } + build_default_columns(rb, vars, vc); + for (int bi = 0; bi < bind_count && rb->row_count < max_rows; bi++) { + const char *vals[48]; + for (int v = 0; v < vc; v++) { + cbm_node_t *n = binding_get(&bindings[bi], vars[v]); + vals[(size_t)v * 3] = n && n->name ? n->name : ""; + vals[((size_t)v * 3) + 1] = n && n->qualified_name ? n->qualified_name : ""; + vals[((size_t)v * 3) + 2] = n && n->label ? n->label : ""; + } + rb_add_row(rb, vals); + } +} + +/* Cross-join node-only pattern into existing bindings */ +static void cross_join_nodes(binding_t **bindings, int *bind_count, cbm_node_t *extra_nodes, + int extra_count, const char *nvar, bool opt) { + binding_t *new_bindings = malloc(((*bind_count * extra_count) + 1) * sizeof(binding_t)); + int new_count = 0; + for (int bi = 0; bi < *bind_count; bi++) { + for (int ni = 0; ni < extra_count; ni++) { + binding_t nb = {0}; + binding_copy(&nb, &(*bindings)[bi]); + binding_set(&nb, nvar, &extra_nodes[ni]); + new_bindings[new_count++] = nb; + } + if (opt && extra_count == 0) { + binding_t nb = {0}; + binding_copy(&nb, &(*bindings)[bi]); + new_bindings[new_count++] = nb; + } + } + for (int bi = 0; bi < *bind_count; bi++) { + binding_free(&(*bindings)[bi]); + } + free(*bindings); + *bindings = new_bindings; + *bind_count = new_count; +} + +/* Cross-join pattern-with-rels into existing bindings */ +static void cross_join_with_rels(cbm_store_t *store, cbm_pattern_t *patn, binding_t **bindings, + int *bind_count, cbm_node_t *extra_nodes, int extra_count, + const char *nvar, bool opt) { + binding_t *new_bindings = malloc(((*bind_count * extra_count * 10) + 1) * sizeof(binding_t)); + int new_count = 0; + for (int bi = 0; bi < *bind_count; bi++) { + for (int ni = 0; ni < extra_count; ni++) { + binding_t nb = {0}; + binding_copy(&nb, &(*bindings)[bi]); + binding_set(&nb, nvar, &extra_nodes[ni]); + binding_t *tmp = malloc(2 * sizeof(binding_t)); + tmp[0] = nb; + int tc = 1; + int tcap = 1; + const char *tv = nvar; + expand_pattern_rels(store, patn, &tmp, &tc, &tcap, &tv, opt); + for (int ti = 0; ti < tc; ti++) { + new_bindings[new_count++] = tmp[ti]; + } + free(tmp); + } + if (opt && extra_count == 0) { + binding_t nb = {0}; + binding_copy(&nb, &(*bindings)[bi]); + new_bindings[new_count++] = nb; + } + } + for (int bi = 0; bi < *bind_count; bi++) { + binding_free(&(*bindings)[bi]); + } + free(*bindings); + *bindings = new_bindings; + *bind_count = new_count; +} + +/* Expand additional MATCH patterns (pi >= 1) */ +static void expand_additional_patterns(cbm_store_t *store, cbm_query_t *q, const char *project, + int max_rows, binding_t **bindings, int *bind_count, + int *bind_cap) { + for (int pi = 1; pi < q->pattern_count; pi++) { + cbm_pattern_t *patn = &q->patterns[pi]; + bool opt = q->pattern_optional[pi]; + const char *nvar = patn->nodes[0].variable ? patn->nodes[0].variable : "_n_extra"; + bool start_bound = *bind_count > 0 && binding_get(&(*bindings)[0], nvar) != NULL; + + if (start_bound && patn->rel_count > 0) { + const char *tv = nvar; + expand_pattern_rels(store, patn, bindings, bind_count, bind_cap, &tv, opt); + } else { + cbm_node_t *extra_nodes = NULL; + int extra_count = 0; + scan_pattern_nodes(store, project, max_rows, &patn->nodes[0], &extra_nodes, + &extra_count); + if (patn->rel_count == 0) { + cross_join_nodes(bindings, bind_count, extra_nodes, extra_count, nvar, opt); + } else { + cross_join_with_rels(store, patn, bindings, bind_count, extra_nodes, extra_count, + nvar, opt); + } + cbm_store_free_nodes(extra_nodes, extra_count); + } + } +} + +/* Project RETURN clause results */ +static void execute_return_clause(cbm_query_t *q, cbm_return_clause_t *ret, binding_t *bindings, + int bind_count, int max_rows, result_builder_t *rb) { + bool has_agg = false; + for (int i = 0; i < ret->count; i++) { + if (is_aggregate_func(ret->items[i].func)) { + has_agg = true; + break; + } + } + + if (ret->star) { + execute_return_star(q, bindings, bind_count, max_rows, rb); + } else { + build_return_columns(rb, ret); + if (has_agg) { + execute_return_agg(ret, bindings, bind_count, rb); + } else { + execute_return_simple(ret, bindings, bind_count, max_rows, rb); + } + } + + rb_apply_order_by(rb, ret); + rb_apply_skip_limit(rb, ret->skip, ret->limit > 0 ? ret->limit : max_rows); + if (ret->distinct) { + rb_apply_distinct(rb); + } +} + static int execute_single(cbm_store_t *store, cbm_query_t *q, const char *project, int max_rows, result_builder_t *rb) { cbm_pattern_t *pat0 = &q->patterns[0]; - /* Step 1: Scan initial nodes for first pattern */ + /* Step 1: Scan initial nodes */ cbm_node_t *scanned = NULL; int scan_count = 0; scan_pattern_nodes(store, project, max_rows, &pat0->nodes[0], &scanned, &scan_count); - /* Build initial bindings */ + /* Build initial bindings with early WHERE */ int bind_cap = scan_count > 0 ? scan_count : 1; binding_t *bindings = malloc((bind_cap + 1) * sizeof(binding_t)); int bind_count = 0; const char *var_name = pat0->nodes[0].variable ? pat0->nodes[0].variable : "_n0"; - /* Early WHERE: evaluate full expression tree (unbound vars return true) */ for (int i = 0; i < scan_count && bind_count < bind_cap; i++) { binding_t b = {0}; binding_set(&b, var_name, &scanned[i]); - - bool pass = true; - if (q->where && pat0->rel_count > 0) { - /* With expression tree, evaluate full tree — unbound vars pass through */ - pass = eval_where(q->where, &b); - } else if (q->where && pat0->rel_count == 0) { - pass = eval_where(q->where, &b); - } - + bool pass = !q->where || eval_where(q->where, &b); if (pass) { bindings[bind_count++] = b; } else { @@ -2660,106 +3259,15 @@ static int execute_single(cbm_store_t *store, cbm_query_t *q, const char *projec } /* Step 2: Expand first pattern's relationships */ - bool opt0 = q->pattern_optional[0]; - expand_pattern_rels(store, pat0, &bindings, &bind_count, &bind_cap, &var_name, opt0); + expand_pattern_rels(store, pat0, &bindings, &bind_count, &bind_cap, &var_name, + q->pattern_optional[0]); - /* Step 2b: Additional MATCH / OPTIONAL MATCH patterns */ - for (int pi = 1; pi < q->pattern_count; pi++) { - cbm_pattern_t *patn = &q->patterns[pi]; - bool opt = q->pattern_optional[pi]; + /* Step 2b: Additional patterns */ + expand_additional_patterns(store, q, project, max_rows, &bindings, &bind_count, &bind_cap); - /* Check if the second pattern's start node variable is already bound */ - const char *nvar = patn->nodes[0].variable ? patn->nodes[0].variable : "_n_extra"; - bool start_bound = (bind_count > 0 && binding_get(&bindings[0], nvar) != NULL) != 0; - - if (start_bound && patn->rel_count > 0) { - /* Start variable already bound: expand rels directly from existing bindings */ - const char *tv = nvar; - expand_pattern_rels(store, patn, &bindings, &bind_count, &bind_cap, &tv, opt); - } else { - /* Start variable not bound: scan + cross-join */ - cbm_node_t *extra_nodes = NULL; - int extra_count = 0; - scan_pattern_nodes(store, project, max_rows, &patn->nodes[0], &extra_nodes, - &extra_count); - - if (patn->rel_count == 0) { - /* Node-only pattern: cross join */ - binding_t *new_bindings = - malloc(((bind_count * extra_count) + 1) * sizeof(binding_t)); - int new_count = 0; - for (int bi = 0; bi < bind_count; bi++) { - for (int ni = 0; ni < extra_count; ni++) { - binding_t nb = {0}; - binding_copy(&nb, &bindings[bi]); - binding_set(&nb, nvar, &extra_nodes[ni]); - new_bindings[new_count++] = nb; - } - if (opt && extra_count == 0) { - binding_t nb = {0}; - binding_copy(&nb, &bindings[bi]); - new_bindings[new_count++] = nb; - } - } - for (int bi = 0; bi < bind_count; bi++) { - binding_free(&bindings[bi]); - } - free(bindings); - bindings = new_bindings; - bind_count = new_count; - } else { - /* Pattern with rels: for each binding, set start node + expand */ - binding_t *new_bindings = - malloc(((bind_count * extra_count * 10) + 1) * sizeof(binding_t)); - int new_count = 0; - for (int bi = 0; bi < bind_count; bi++) { - for (int ni = 0; ni < extra_count; ni++) { - binding_t nb = {0}; - binding_copy(&nb, &bindings[bi]); - binding_set(&nb, nvar, &extra_nodes[ni]); - - binding_t *tmp = malloc(2 * sizeof(binding_t)); - tmp[0] = nb; - int tc = 1; - int tcap = 1; - const char *tv = nvar; - expand_pattern_rels(store, patn, &tmp, &tc, &tcap, &tv, opt); - for (int ti = 0; ti < tc; ti++) { - new_bindings[new_count++] = tmp[ti]; - } - free(tmp); - } - if (opt && extra_count == 0) { - binding_t nb = {0}; - binding_copy(&nb, &bindings[bi]); - new_bindings[new_count++] = nb; - } - } - for (int bi = 0; bi < bind_count; bi++) { - binding_free(&bindings[bi]); - } - free(bindings); - bindings = new_bindings; - bind_count = new_count; - } - cbm_store_free_nodes(extra_nodes, extra_count); - } - } - - /* Step 3: Apply late WHERE (re-evaluate after all bindings are fully expanded) */ + /* Step 3: Late WHERE */ if (q->where && (pat0->rel_count > 0 || q->pattern_count > 1)) { - int kept = 0; - for (int i = 0; i < bind_count; i++) { - if (eval_where(q->where, &bindings[i])) { - if (kept != i) { - bindings[kept] = bindings[i]; - } - kept++; - } else { - binding_free(&bindings[i]); - } - } - bind_count = kept; + filter_bindings_where(q->where, bindings, &bind_count); } /* Step 3b: WITH clause */ @@ -2767,326 +3275,10 @@ static int execute_single(cbm_store_t *store, cbm_query_t *q, const char *projec /* Step 4: Project results */ rb_init(rb); - cbm_return_clause_t *ret = q->ret; - - if (ret) { - /* Check for aggregation */ - bool has_agg = false; - for (int i = 0; i < ret->count; i++) { - if (ret->items[i].func && - (strcmp(ret->items[i].func, "COUNT") == 0 || - strcmp(ret->items[i].func, "SUM") == 0 || strcmp(ret->items[i].func, "AVG") == 0 || - strcmp(ret->items[i].func, "MIN") == 0 || strcmp(ret->items[i].func, "MAX") == 0 || - strcmp(ret->items[i].func, "COLLECT") == 0)) { - has_agg = true; - break; - } - } - - /* RETURN * */ - if (ret->star) { - /* Collect all named variables from all patterns */ - const char *vars[32]; - int vc = 0; - for (int pi = 0; pi < q->pattern_count; pi++) { - for (int ni = 0; ni < q->patterns[pi].node_count && vc < 32; ni++) { - if (q->patterns[pi].nodes[ni].variable) { - vars[vc++] = q->patterns[pi].nodes[ni].variable; - } - } - for (int ri2 = 0; ri2 < q->patterns[pi].rel_count && vc < 32; ri2++) { - if (q->patterns[pi].rels[ri2].variable) { - vars[vc++] = q->patterns[pi].rels[ri2].variable; - } - } - } - /* Build columns: var.name, var.qualified_name, var.label, var.file_path for nodes; - * var.type for edges */ - int col_n = vc * 4; - const char *col_names[128]; - for (int v = 0; v < vc; v++) { - char buf[128]; - snprintf(buf, sizeof(buf), "%s.name", vars[v]); - col_names[(size_t)v * 4] = heap_strdup(buf); - snprintf(buf, sizeof(buf), "%s.qualified_name", vars[v]); - col_names[((size_t)v * 4) + 1] = heap_strdup(buf); - snprintf(buf, sizeof(buf), "%s.label", vars[v]); - col_names[((size_t)v * 4) + 2] = heap_strdup(buf); - snprintf(buf, sizeof(buf), "%s.file_path", vars[v]); - col_names[((size_t)v * 4) + 3] = heap_strdup(buf); - } - rb_set_columns(rb, col_names, col_n); - for (int i = 0; i < col_n; i++) { - free((void *)col_names[i]); - } - - for (int bi = 0; bi < bind_count && rb->row_count < max_rows; bi++) { - const char *vals[128]; - for (int v = 0; v < vc; v++) { - cbm_edge_t *edge = binding_get_edge(&bindings[bi], vars[v]); - if (edge) { - vals[(size_t)v * 4] = edge_prop(edge, "type"); - vals[((size_t)v * 4) + 1] = ""; - vals[((size_t)v * 4) + 2] = ""; - vals[((size_t)v * 4) + 3] = ""; - } else { - cbm_node_t *n = binding_get(&bindings[bi], vars[v]); - vals[(size_t)v * 4] = n ? (n->name ? n->name : "") : ""; - vals[((size_t)v * 4) + 1] = - n ? (n->qualified_name ? n->qualified_name : "") : ""; - vals[((size_t)v * 4) + 2] = n ? (n->label ? n->label : "") : ""; - vals[((size_t)v * 4) + 3] = n ? (n->file_path ? n->file_path : "") : ""; - } - } - rb_add_row(rb, vals); - } - goto postprocess; - } - - /* Build column names */ - const char *col_names[32]; - for (int i = 0; i < ret->count && i < 32; i++) { - cbm_return_item_t *item = &ret->items[i]; - if (item->alias) { - col_names[i] = item->alias; - } else if (item->func) { - char buf[128]; - snprintf(buf, sizeof(buf), "%s(%s)", item->func, item->variable); - col_names[i] = heap_strdup(buf); - } else if (item->kase) { - col_names[i] = "CASE"; - } else if (item->property) { - char buf[128]; - snprintf(buf, sizeof(buf), "%s.%s", item->variable, item->property); - col_names[i] = heap_strdup(buf); - } else { - col_names[i] = item->variable; - } - } - rb_set_columns(rb, col_names, ret->count); - /* Free heap_strdup'd column names (rb_set_columns made its own copies). - * Only func/property branches heap-allocate; alias takes priority. */ - for (int i = 0; i < ret->count && i < 32; i++) { - cbm_return_item_t *item = &ret->items[i]; - if (!item->alias && (item->func || (!item->kase && item->property))) { - free((void *)col_names[i]); - } - } - - if (has_agg) { - /* Generalized aggregation: COUNT, SUM, AVG, MIN, MAX, COLLECT */ - typedef struct { - char group_key[1024]; - const char **group_vals; - double *sums; - int *counts; - double *mins, *maxs; - char ***collect_lists; - int *collect_counts; - } agg_entry_t; - - int agg_cap = 256; - agg_entry_t *aggs = calloc(agg_cap, sizeof(agg_entry_t)); - int agg_count = 0; - - for (int bi = 0; bi < bind_count; bi++) { - char key[1024] = ""; - int klen = 0; - const char *vals[32]; - for (int ci = 0; ci < ret->count; ci++) { - cbm_return_item_t *item = &ret->items[ci]; - if (item->func) { - vals[ci] = "0"; - continue; - } - char func_buf[512]; - vals[ci] = project_item(&bindings[bi], item, func_buf, sizeof(func_buf)); - klen += snprintf(key + klen, sizeof(key) - (size_t)klen, "%s|", vals[ci]); - if (klen >= (int)sizeof(key)) { - klen = (int)sizeof(key) - 1; - } - } - - int found = -1; - for (int a = 0; a < agg_count; a++) { - if (strcmp(aggs[a].group_key, key) == 0) { - found = a; - break; - } - } - if (found < 0) { - if (agg_count >= agg_cap) { - agg_cap *= 2; - aggs = safe_realloc(aggs, agg_cap * sizeof(agg_entry_t)); - } - found = agg_count++; - snprintf(aggs[found].group_key, sizeof(aggs[found].group_key), "%s", key); - aggs[found].group_vals = malloc(ret->count * sizeof(const char *)); - aggs[found].sums = calloc(ret->count, sizeof(double)); - aggs[found].counts = calloc(ret->count, sizeof(int)); - aggs[found].mins = malloc(ret->count * sizeof(double)); - aggs[found].maxs = malloc(ret->count * sizeof(double)); - aggs[found].collect_lists = calloc(ret->count, sizeof(char **)); - aggs[found].collect_counts = calloc(ret->count, sizeof(int)); - for (int ci = 0; ci < ret->count; ci++) { - aggs[found].mins[ci] = 1e308; - aggs[found].maxs[ci] = -1e308; - aggs[found].group_vals[ci] = heap_strdup(vals[ci]); - } - } - for (int ci = 0; ci < ret->count; ci++) { - if (!ret->items[ci].func) { - continue; - } - aggs[found].counts[ci]++; - const char *raw = binding_get_virtual(&bindings[bi], ret->items[ci].variable, - ret->items[ci].property); - double dv = strtod(raw, NULL); - aggs[found].sums[ci] += dv; - if (dv < aggs[found].mins[ci]) { - aggs[found].mins[ci] = dv; - } - if (dv > aggs[found].maxs[ci]) { - aggs[found].maxs[ci] = dv; - } - if (strcmp(ret->items[ci].func, "COLLECT") == 0) { - int idx = aggs[found].collect_counts[ci]++; - aggs[found].collect_lists[ci] = - safe_realloc(aggs[found].collect_lists[ci], (idx + 1) * sizeof(char *)); - aggs[found].collect_lists[ci][idx] = heap_strdup(raw); - } - } - } - - for (int a = 0; a < agg_count; a++) { - const char *row[32]; - char bufs[32][64]; - for (int ci = 0; ci < ret->count; ci++) { - if (!ret->items[ci].func) { - row[ci] = aggs[a].group_vals[ci]; - continue; - } - const char *f = ret->items[ci].func; - if (strcmp(f, "COUNT") == 0) { - { - snprintf(bufs[ci], sizeof(bufs[ci]), "%d", aggs[a].counts[ci]); - } - } else if (strcmp(f, "SUM") == 0) { - { snprintf(bufs[ci], sizeof(bufs[ci]), "%.10g", aggs[a].sums[ci]); } - } else if (strcmp(f, "AVG") == 0) { - { - snprintf(bufs[ci], sizeof(bufs[ci]), "%.10g", - aggs[a].counts[ci] > 0 ? aggs[a].sums[ci] / aggs[a].counts[ci] - : 0.0); - } - } else if (strcmp(f, "MIN") == 0) { - { snprintf(bufs[ci], sizeof(bufs[ci]), "%.10g", aggs[a].mins[ci]); } - } else if (strcmp(f, "MAX") == 0) { - { snprintf(bufs[ci], sizeof(bufs[ci]), "%.10g", aggs[a].maxs[ci]); } - } else if (strcmp(f, "COLLECT") == 0) { - char cbuf[2048] = "["; - int bl = 1; - for (int ci2 = 0; ci2 < aggs[a].collect_counts[ci]; ci2++) { - if (ci2 > 0) { - cbuf[bl++] = ','; - } - bl += snprintf(cbuf + bl, sizeof(cbuf) - (size_t)bl, "\"%s\"", - aggs[a].collect_lists[ci][ci2]); - if (bl >= (int)sizeof(cbuf)) { - bl = (int)sizeof(cbuf) - 1; - } - } - if (bl < (int)sizeof(cbuf) - 1) { - cbuf[bl++] = ']'; - } - cbuf[bl] = '\0'; - snprintf(bufs[ci], sizeof(bufs[ci]), "%s", cbuf); - } else { - snprintf(bufs[ci], sizeof(bufs[ci]), "%d", aggs[a].counts[ci]); - } - row[ci] = bufs[ci]; - } - rb_add_row(rb, row); - } - - for (int a = 0; a < agg_count; a++) { - for (int ci = 0; ci < ret->count; ci++) { - free((void *)aggs[a].group_vals[ci]); - for (int j = 0; j < aggs[a].collect_counts[ci]; j++) { - free(aggs[a].collect_lists[ci][j]); - } - free(aggs[a].collect_lists[ci]); - } - free(aggs[a].group_vals); - free(aggs[a].sums); - free(aggs[a].counts); - free(aggs[a].mins); - free(aggs[a].maxs); - free(aggs[a].collect_lists); - free(aggs[a].collect_counts); - } - free(aggs); - } else { - /* Simple projection — when ORDER BY or SKIP are present, project all rows - * since we need the full set before sorting/skipping. Only cap early when - * there's a simple LIMIT with no ORDER BY and no SKIP. */ - int proj_cap = max_rows; - if (ret->limit > 0 && !ret->order_by && ret->skip <= 0) { - proj_cap = ret->limit; - } - for (int bi = 0; bi < bind_count && rb->row_count < proj_cap; bi++) { - const char *vals[32]; - char func_bufs[32][512]; - for (int ci = 0; ci < ret->count; ci++) { - vals[ci] = project_item(&bindings[bi], &ret->items[ci], func_bufs[ci], - sizeof(func_bufs[ci])); - } - rb_add_row(rb, vals); - } - } - - postprocess: - rb_apply_order_by(rb, ret); - rb_apply_skip_limit(rb, ret->skip, ret->limit > 0 ? ret->limit : max_rows); - if (ret->distinct) { - rb_apply_distinct(rb); - } - + if (q->ret) { + execute_return_clause(q, q->ret, bindings, bind_count, max_rows, rb); } else { - /* Default projection */ - const char *vars[16]; - int vc = 0; - for (int ni = 0; ni < pat0->node_count && vc < 16; ni++) { - if (pat0->nodes[ni].variable) { - vars[vc++] = pat0->nodes[ni].variable; - } - } - int col_n = vc * 3; - const char *col_names[48]; - for (int v = 0; v < vc; v++) { - char buf[128]; - snprintf(buf, sizeof(buf), "%s.name", vars[v]); - col_names[(size_t)v * 3] = heap_strdup(buf); - snprintf(buf, sizeof(buf), "%s.qualified_name", vars[v]); - col_names[((size_t)v * 3) + 1] = heap_strdup(buf); - snprintf(buf, sizeof(buf), "%s.label", vars[v]); - col_names[((size_t)v * 3) + 2] = heap_strdup(buf); - } - rb_set_columns(rb, col_names, col_n); - for (int i = 0; i < col_n; i++) { - free((void *)col_names[i]); - } - - for (int bi = 0; bi < bind_count && rb->row_count < max_rows; bi++) { - const char *vals[48]; - for (int v = 0; v < vc; v++) { - cbm_node_t *n = binding_get(&bindings[bi], vars[v]); - vals[(size_t)v * 3] = n && n->name ? n->name : ""; - vals[((size_t)v * 3) + 1] = n && n->qualified_name ? n->qualified_name : ""; - vals[((size_t)v * 3) + 2] = n && n->label ? n->label : ""; - } - rb_add_row(rb, vals); - } + execute_default_projection(pat0, bindings, bind_count, max_rows, rb); } for (int bi = 0; bi < bind_count; bi++) { diff --git a/src/discover/discover.c b/src/discover/discover.c index 07c566828..0f82c6f67 100644 --- a/src/discover/discover.c +++ b/src/discover/discover.c @@ -226,7 +226,97 @@ static void fl_add(file_list_t *fl, const char *abs_path, const char *rel_path, /* ── Recursive walk ──────────────────────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Check if a directory entry should be skipped (hardcoded dirs + gitignore). */ +static bool should_skip_directory(const char *entry_name, const char *rel_path, + const cbm_discover_opts_t *opts, const cbm_gitignore_t *gitignore, + const cbm_gitignore_t *cbmignore) { + if (cbm_should_skip_dir(entry_name, opts ? opts->mode : CBM_MODE_FULL)) { + return true; + } + if (gitignore && cbm_gitignore_matches(gitignore, rel_path, true)) { + return true; + } + if (cbmignore && cbm_gitignore_matches(cbmignore, rel_path, true)) { + return true; + } + return false; +} + +/* Check if a regular file should be skipped (filters + gitignore + size). */ +static bool should_skip_file(const char *entry_name, const char *rel_path, + const cbm_discover_opts_t *opts, const cbm_gitignore_t *gitignore, + const cbm_gitignore_t *cbmignore, off_t file_size) { + cbm_index_mode_t mode = opts ? opts->mode : CBM_MODE_FULL; + if (cbm_has_ignored_suffix(entry_name, mode)) { + return true; + } + if (cbm_should_skip_filename(entry_name, mode)) { + return true; + } + if (cbm_matches_fast_pattern(entry_name, mode)) { + return true; + } + if (gitignore && cbm_gitignore_matches(gitignore, rel_path, false)) { + return true; + } + if (cbmignore && cbm_gitignore_matches(cbmignore, rel_path, false)) { + return true; + } + if (opts && opts->max_file_size > 0 && file_size > opts->max_file_size) { + return true; + } + return false; +} + +/* Detect language for a file, handling .m disambiguation and JSON filtering. */ +static CBMLanguage detect_file_language(const char *entry_name, const char *abs_path) { + CBMLanguage lang = cbm_language_for_filename(entry_name); + if (lang == CBM_LANG_COUNT) { + return CBM_LANG_COUNT; + } + /* Special: .m files need content-based disambiguation */ + const char *dot = strrchr(entry_name, '.'); + if (dot && strcmp(dot, ".m") == 0) { + lang = cbm_disambiguate_m(abs_path); + } + /* Check ignored JSON files */ + if (lang == CBM_LANG_JSON && str_in_list(entry_name, IGNORED_JSON_FILES)) { + return CBM_LANG_COUNT; + } + return lang; +} + +/* Stat a path, skipping symlinks. Returns 0 on success, -1 to skip. */ +static int safe_stat(const char *abs_path, struct stat *st) { +#ifdef _WIN32 + if (stat(abs_path, st) != 0) { + return -1; + } +#else + if (lstat(abs_path, st) != 0) { + return -1; + } + if (S_ISLNK(st->st_mode)) { + return -1; + } +#endif + return 0; +} + +/* Process a single regular file entry during directory walk. */ +static void walk_dir_process_file(const char *abs_path, const char *rel_path, const char *name, + const cbm_discover_opts_t *opts, const cbm_gitignore_t *gitignore, + const cbm_gitignore_t *cbmignore, off_t size, file_list_t *out) { + if (should_skip_file(name, rel_path, opts, gitignore, cbmignore, size)) { + return; + } + CBMLanguage lang = detect_file_language(name, abs_path); + if (lang == CBM_LANG_COUNT) { + return; + } + fl_add(out, abs_path, rel_path, lang, size); +} + static void walk_dir(const char *dir_path, const char *rel_prefix, const cbm_discover_opts_t *opts, const cbm_gitignore_t *gitignore, const cbm_gitignore_t *cbmignore, file_list_t *out) { @@ -237,101 +327,27 @@ static void walk_dir(const char *dir_path, const char *rel_prefix, const cbm_dis cbm_dirent_t *entry; while ((entry = cbm_readdir(d)) != NULL) { - - /* Build full and relative paths */ char abs_path[4096]; char rel_path[4096]; snprintf(abs_path, sizeof(abs_path), "%s/%s", dir_path, entry->name); - if (rel_prefix[0]) { + if (rel_prefix[0] != '\0') { snprintf(rel_path, sizeof(rel_path), "%s/%s", rel_prefix, entry->name); } else { snprintf(rel_path, sizeof(rel_path), "%s", entry->name); } struct stat st; -#ifdef _WIN32 - if (stat(abs_path, &st) != 0) { + if (safe_stat(abs_path, &st) != 0) { continue; } - /* Windows: no symlink detection via stat */ -#else - if (lstat(abs_path, &st) != 0) { - continue; - } - /* Skip symlinks */ - if (S_ISLNK(st.st_mode)) { - continue; - } -#endif if (S_ISDIR(st.st_mode)) { - /* Check hardcoded directory skip */ - if (cbm_should_skip_dir(entry->name, opts ? opts->mode : CBM_MODE_FULL)) { - continue; + if (!should_skip_directory(entry->name, rel_path, opts, gitignore, cbmignore)) { + walk_dir(abs_path, rel_path, opts, gitignore, cbmignore, out); } - - /* Check gitignore */ - if (gitignore && cbm_gitignore_matches(gitignore, rel_path, true)) { - continue; - } - if (cbmignore && cbm_gitignore_matches(cbmignore, rel_path, true)) { - continue; - } - - /* Recurse */ - walk_dir(abs_path, rel_path, opts, gitignore, cbmignore, out); } else if (S_ISREG(st.st_mode)) { - cbm_index_mode_t mode = opts ? opts->mode : CBM_MODE_FULL; - - /* Check suffix filter */ - if (cbm_has_ignored_suffix(entry->name, mode)) { - continue; - } - - /* Check fast-mode filename skip */ - if (cbm_should_skip_filename(entry->name, mode)) { - continue; - } - - /* Check fast-mode patterns */ - if (cbm_matches_fast_pattern(entry->name, mode)) { - continue; - } - - /* Check gitignore */ - if (gitignore && cbm_gitignore_matches(gitignore, rel_path, false)) { - continue; - } - if (cbmignore && cbm_gitignore_matches(cbmignore, rel_path, false)) { - continue; - } - - /* Check max file size */ - if (opts && opts->max_file_size > 0 && st.st_size > opts->max_file_size) { - continue; - } - - /* Detect language */ - CBMLanguage lang = cbm_language_for_filename(entry->name); - - /* Handle .m disambiguation */ - if (lang == CBM_LANG_COUNT) { - /* No language detected — skip */ - continue; - } - - /* Special: .m files need content-based disambiguation */ - const char *dot = strrchr(entry->name, '.'); - if (dot && strcmp(dot, ".m") == 0) { - lang = cbm_disambiguate_m(abs_path); - } - - /* Check ignored JSON files */ - if (lang == CBM_LANG_JSON && str_in_list(entry->name, IGNORED_JSON_FILES)) { - continue; - } - - fl_add(out, abs_path, rel_path, lang, st.st_size); + walk_dir_process_file(abs_path, rel_path, entry->name, opts, gitignore, cbmignore, + st.st_size, out); } } diff --git a/src/discover/gitignore.c b/src/discover/gitignore.c index ff57e6a01..97c97fad1 100644 --- a/src/discover/gitignore.c +++ b/src/discover/gitignore.c @@ -35,62 +35,105 @@ struct cbm_gitignore { /* ── Pattern matching engine ─────────────────────────────────────── */ +/* Forward declaration for recursive calls. */ +static bool glob_match(const char *pat, const char *str); + +/* Match a ** (doublestar-slash) pattern: try rest at every / boundary. */ +static bool glob_match_doublestar_slash(const char *pat, const char *str) { + if (glob_match(pat, str)) { + return true; + } + for (const char *s = str; *s; s++) { + if (*s == '/' && glob_match(pat, s + 1)) { + return true; + } + } + return false; +} + +/* Match a ** (doublestar) followed by non-slash: try at every position. */ +static bool glob_match_doublestar_any(const char *pat, const char *str) { + for (const char *s = str;; s++) { + if (glob_match(pat, s)) { + return true; + } + if (!*s) { + return false; + } + } +} + +/* Match a * (single star): match any sequence not containing /. */ +static bool glob_match_star(const char *pat, const char *str) { + for (const char *s = str;; s++) { + if (glob_match(pat, s)) { + return true; + } + if (!*s || *s == '/') { + return false; + } + } +} + +/* Match a [...] character class at current position. + * Returns true if matched. Advances *pat_out past the closing ']'. */ +static bool glob_match_charclass(const char *pat, char ch, const char **pat_out) { + bool negate_class = false; + if (*pat == '!' || *pat == '^') { + negate_class = true; + pat++; + } + bool matched = false; + char prev = 0; + while (*pat && *pat != ']') { + if (*pat == '-' && prev && pat[1] && pat[1] != ']') { + pat++; + if (ch >= prev && ch <= *pat) { + matched = true; + } + prev = *pat; + pat++; + } else { + if (ch == *pat) { + matched = true; + } + prev = *pat; + pat++; + } + } + if (*pat == ']') { + pat++; + } + *pat_out = pat; + return negate_class ? !matched : matched; +} + /* * Match a glob pattern against a string. * Handles: * (non-slash), ** (any path), ? (single non-slash), [class] */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Handle ** at current position. Returns match result. */ +static bool glob_match_doublestar(const char *pat, const char *str) { + if (pat[2] == '/') { + return glob_match_doublestar_slash(pat + 3, str); + } + if (pat[2] == '\0') { + return true; + } + return glob_match_doublestar_any(pat + 2, str); +} + static bool glob_match(const char *pat, const char *str) { while (*pat && *str) { if (pat[0] == '*' && pat[1] == '*') { - /* ** — match any number of path components */ - if (pat[2] == '/') { - /* "** /" at start or middle: try matching rest at every / boundary */ - pat += 3; - /* Try matching from current position */ - if (glob_match(pat, str)) { - return true; - } - /* Try matching from every / in str */ - for (const char *s = str; *s; s++) { - if (*s == '/' && glob_match(pat, s + 1)) { - return true; - } - } - return false; - } - if (pat[2] == '\0') { - /* ** at end — matches everything */ - return true; - } - /* ** followed by non-slash — treat as two *'s, match anything */ - pat += 2; - for (const char *s = str;; s++) { - if (glob_match(pat, s)) { - return true; - } - if (!*s) { - return false; - } - } + return glob_match_doublestar(pat, str); } if (*pat == '*') { - /* * — match any sequence not containing / */ - pat++; - /* Try matching rest at every position that doesn't cross / */ - for (const char *s = str;; s++) { - if (glob_match(pat, s)) { - return true; - } - if (!*s || *s == '/') { - return false; - } - } + return glob_match_star(pat + 1, str); } if (*pat == '?') { - /* ? — match any single character except / */ if (*str == '/') { return false; } @@ -100,46 +143,15 @@ static bool glob_match(const char *pat, const char *str) { } if (*pat == '[') { - /* Character class */ - pat++; - bool negate_class = false; - if (*pat == '!' || *pat == '^') { - negate_class = true; - pat++; - } - bool matched = false; - char prev = 0; - while (*pat && *pat != ']') { - if (*pat == '-' && prev && pat[1] && pat[1] != ']') { - /* Range: [a-z] */ - pat++; - if (*str >= prev && *str <= *pat) { - matched = true; - } - prev = *pat; - pat++; - } else { - if (*str == *pat) { - matched = true; - } - prev = *pat; - pat++; - } - } - if (*pat == ']') { - pat++; - } - if (negate_class) { - matched = !matched; - } - if (!matched) { + const char *new_pat = NULL; + if (!glob_match_charclass(pat + 1, *str, &new_pat)) { return false; } + pat = new_pat; str++; continue; } - /* Literal character */ if (*pat != *str) { return false; } @@ -147,11 +159,9 @@ static bool glob_match(const char *pat, const char *str) { str++; } - /* Handle trailing * or ** in pattern */ while (*pat == '*') { pat++; } - return *pat == '\0' && *str == '\0'; } @@ -301,7 +311,29 @@ cbm_gitignore_t *cbm_gitignore_load(const char *path) { return gi; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Match a non-rooted pattern against basename and path suffixes. */ +static bool match_unrooted(const char *pattern, const char *rel_path, const char *basename) { + if (glob_match(pattern, basename)) { + return true; + } + if (!strchr(rel_path, '/')) { + return false; + } + /* Try matching at every / boundary */ + const char *s = rel_path; + while (*s) { + if (glob_match(pattern, s)) { + return true; + } + const char *next = strchr(s, '/'); + if (!next) { + break; + } + s = next + 1; + } + return false; +} + bool cbm_gitignore_matches(const cbm_gitignore_t *gi, const char *rel_path, bool is_dir) { if (!gi || !rel_path) { return false; @@ -316,40 +348,12 @@ bool cbm_gitignore_matches(const cbm_gitignore_t *gi, const char *rel_path, bool for (int i = 0; i < gi->count; i++) { const gi_pattern_t *p = &gi->patterns[i]; - /* Directory-only patterns only match directories */ if (p->dir_only && !is_dir) { continue; } - bool this_match = false; - - if (p->rooted) { - /* Rooted: match against full relative path */ - this_match = glob_match(p->pattern, rel_path); - } else { - /* Non-rooted: match against basename, OR against full path - * (a non-rooted pattern like "*.log" should match "error.log" - * but NOT "logs/error.log" — unless the pattern uses **) */ - this_match = glob_match(p->pattern, basename); - - /* Also try matching against every suffix of the path. - * This handles "build/" matching "src/build" as a directory. */ - if (!this_match && strchr(rel_path, '/')) { - /* Try matching at every / boundary */ - const char *s = rel_path; - while (*s) { - if (glob_match(p->pattern, s)) { - this_match = true; - break; - } - const char *next = strchr(s, '/'); - if (!next) { - break; - } - s = next + 1; - } - } - } + bool this_match = p->rooted ? glob_match(p->pattern, rel_path) + : match_unrooted(p->pattern, rel_path, basename); if (this_match) { matched = !p->negated; diff --git a/src/discover/language.c b/src/discover/language.c index 09d566b0f..aa3283521 100644 --- a/src/discover/language.c +++ b/src/discover/language.c @@ -439,7 +439,61 @@ static bool str_contains(const char *haystack, const char *needle) { return strstr(haystack, needle) != NULL; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +static bool has_objc_markers(const char *buf) { + return str_contains(buf, "@interface") || str_contains(buf, "@implementation") || + str_contains(buf, "@protocol") || str_contains(buf, "@property") || + str_contains(buf, "#import") || str_contains(buf, "@selector") || + str_contains(buf, "@encode") || str_contains(buf, "@synthesize") || + str_contains(buf, "@dynamic"); +} + +static bool has_magma_end_markers(const char *buf) { + return str_contains(buf, "end function;") || str_contains(buf, "end procedure;") || + str_contains(buf, "end intrinsic;") || str_contains(buf, "end if;") || + str_contains(buf, "end for;") || str_contains(buf, "end while;"); +} + +/* Check for "intrinsic Name(" or "procedure Name(" patterns. */ +static bool has_magma_callable_pattern(const char *buf) { + const char *markers[] = {"intrinsic ", "procedure "}; + for (int i = 0; i < 2; i++) { + const char *p = strstr(buf, markers[i]); + if (!p) { + continue; + } + p += strlen(markers[i]); + while (*p && isalpha((unsigned char)*p)) { + p++; + } + if (*p == '(') { + return true; + } + } + return false; +} + +/* Scan lines for MATLAB-specific markers (function/classdef/%%). */ +static bool has_matlab_line_markers(const char *buf) { + const char *line = buf; + while (*line) { + const char *p = line; + while (*p == ' ' || *p == '\t') { + p++; + } + if (strncmp(p, "function ", 9) == 0 || strncmp(p, "function\t", 9) == 0 || + strncmp(p, "classdef ", 9) == 0 || strncmp(p, "classdef\t", 9) == 0 || + strncmp(p, "%%", 2) == 0 || (*p == '%' && *(p + 1) != '{')) { + return true; + } + const char *nl = strchr(line, '\n'); + if (!nl) { + break; + } + line = nl + 1; + } + return false; +} + CBMLanguage cbm_disambiguate_m(const char *path) { if (!path) { return CBM_LANG_MATLAB; @@ -456,65 +510,19 @@ CBMLanguage cbm_disambiguate_m(const char *path) { buf[n] = '\0'; (void)fclose(f); - /* Check Objective-C markers first */ - if (str_contains(buf, "@interface") || str_contains(buf, "@implementation") || - str_contains(buf, "@protocol") || str_contains(buf, "@property") || - str_contains(buf, "#import") || str_contains(buf, "@selector") || - str_contains(buf, "@encode") || str_contains(buf, "@synthesize") || - str_contains(buf, "@dynamic")) { + if (has_objc_markers(buf)) { return CBM_LANG_OBJC; } - - /* Check Magma markers (before MATLAB — both have 'function') */ - if (str_contains(buf, "end function;") || str_contains(buf, "end procedure;") || - str_contains(buf, "end intrinsic;") || str_contains(buf, "end if;") || - str_contains(buf, "end for;") || str_contains(buf, "end while;")) { + if (has_magma_end_markers(buf)) { return CBM_LANG_MAGMA; } - - /* Also check Magma-specific patterns with regex-like heuristics */ - if (str_contains(buf, "intrinsic ") || str_contains(buf, "procedure ")) { - /* Look for "intrinsic Name(" or "procedure Name(" patterns */ - const char *markers[] = {"intrinsic ", "procedure "}; - for (int i = 0; i < 2; i++) { - const char *p = strstr(buf, markers[i]); - if (p) { - p += strlen(markers[i]); - /* Skip to see if there's an identifier followed by '(' */ - while (*p && isalpha((unsigned char)*p)) { - p++; - } - if (*p == '(') { - return CBM_LANG_MAGMA; - } - } - } + if ((str_contains(buf, "intrinsic ") || str_contains(buf, "procedure ")) && + has_magma_callable_pattern(buf)) { + return CBM_LANG_MAGMA; + } + if (has_matlab_line_markers(buf)) { + return CBM_LANG_MATLAB; } - /* Check MATLAB markers */ - /* Look for: function keyword at start of line, classdef, %% section markers */ - const char *line = buf; - while (*line) { - /* Skip leading whitespace */ - const char *p = line; - while (*p == ' ' || *p == '\t') { - p++; - } - - if (strncmp(p, "function ", 9) == 0 || strncmp(p, "function\t", 9) == 0 || - strncmp(p, "classdef ", 9) == 0 || strncmp(p, "classdef\t", 9) == 0 || - strncmp(p, "%%", 2) == 0 || (*p == '%' && *(p + 1) != '{')) { - return CBM_LANG_MATLAB; - } - - /* Advance to next line */ - const char *nl = strchr(line, '\n'); - if (!nl) { - break; - } - line = nl + 1; - } - - /* Default to MATLAB */ return CBM_LANG_MATLAB; } diff --git a/src/foundation/yaml.c b/src/foundation/yaml.c index f9170356e..b9805b9b2 100644 --- a/src/foundation/yaml.c +++ b/src/foundation/yaml.c @@ -117,7 +117,121 @@ typedef struct { int indent; } stack_entry_t; -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Parse a list item "- value" and add to the correct parent. */ +static void parse_list_item(const char *content, int content_len, cbm_yaml_node_t *parent) { + const char *item_start = content + 2; + int item_len = content_len - 2; + while (item_len > 0 && isspace((unsigned char)item_start[0])) { + item_start++; + item_len--; + } + + /* If parent is a map, the list items belong to the last child */ + cbm_yaml_node_t *list_parent = parent; + if (parent->type == YAML_MAP && parent->child_count > 0) { + cbm_yaml_node_t *last = parent->children[parent->child_count - 1]; + if (last->type == YAML_LIST) { + list_parent = last; + } + } + + cbm_yaml_node_t *item = node_new(YAML_SCALAR); + if (item) { + item->value = trim_dup(item_start, item_start + item_len); + node_add_child(list_parent, item); + } +} + +/* Strip inline comments from a value string (not inside quotes). + * Returns the adjusted length. */ +static int strip_inline_comment(const char *after, int after_len) { + if (after_len > 0 && after[0] != '"' && after[0] != '\'') { + for (int i = 0; i < after_len; i++) { + if (after[i] == '#' && i > 0 && after[i - 1] == ' ') { + after_len = i; + while (after_len > 0 && isspace((unsigned char)after[after_len - 1])) { + after_len--; + } + break; + } + } + } + return after_len; +} + +/* Peek ahead to check if next content line is a list item. */ +static bool peek_is_list(const char *eol, const char *end) { + const char *peek = (eol < end) ? eol + 1 : end; + while (peek < end) { + const char *peek_eol = memchr(peek, '\n', (size_t)(end - peek)); + if (!peek_eol) { + peek_eol = end; + } + int pi = leading_spaces(peek); + const char *pc = peek + pi; + int pcl = (int)(peek_eol - pc); + if (pcl > 0 && pc[pcl - 1] == '\r') { + pcl--; + } + if (pcl == 0 || pc[0] == '#') { + peek = (peek_eol < end) ? peek_eol + 1 : end; + continue; + } + return pc[0] == '-'; + } + return false; +} + +/* Parse a "key: value" or "key:" line. */ +static void parse_key_line(const char *content, int content_len, int indent, + cbm_yaml_node_t *parent, stack_entry_t *stack, int *stack_depth, + const char *eol, const char *end) { + const char *colon = memchr(content, ':', (size_t)content_len); + if (!colon) { + return; + } + + char *key = trim_dup(content, colon); + if (!key) { + return; + } + + /* After colon: value or nothing */ + const char *after = colon + 1; + int after_len = content_len - (int)(after - content); + while (after_len > 0 && isspace((unsigned char)*after)) { + after++; + after_len--; + } + + after_len = strip_inline_comment(after, after_len); + + if (after_len > 0) { + /* "key: value" — scalar */ + cbm_yaml_node_t *child = node_new(YAML_SCALAR); + if (child) { + child->key = key; + child->value = trim_dup(after, after + after_len); + node_add_child(parent, child); + } else { + free(key); + } + } else { + /* "key:" — could be map or list (determined by next lines) */ + bool is_list = peek_is_list(eol, end); + cbm_yaml_node_t *child = node_new(is_list ? YAML_LIST : YAML_MAP); + if (child) { + child->key = key; + node_add_child(parent, child); + if (*stack_depth < 32) { + stack[(*stack_depth)++] = (stack_entry_t){.node = child, .indent = indent}; + } + } else { + free(key); + } + } +} + cbm_yaml_node_t *cbm_yaml_parse(const char *text, int len) { if (!text || len <= 0) { return node_new(YAML_MAP); @@ -166,117 +280,10 @@ cbm_yaml_node_t *cbm_yaml_parse(const char *text, int len) { } cbm_yaml_node_t *parent = stack[stack_depth - 1].node; - /* Check for list item: "- value" */ if (content[0] == '-' && content_len >= 2 && content[1] == ' ') { - /* Ensure parent is a list (or convert map parent's last child) */ - const char *item_start = content + 2; - int item_len = content_len - 2; - while (item_len > 0 && isspace((unsigned char)item_start[0])) { - item_start++; - item_len--; - } - - /* If parent is a map, the list items belong to the last child */ - cbm_yaml_node_t *list_parent = parent; - if (parent->type == YAML_MAP && parent->child_count > 0) { - cbm_yaml_node_t *last = parent->children[parent->child_count - 1]; - if (last->type == YAML_LIST) { - list_parent = last; - } - } - - cbm_yaml_node_t *item = node_new(YAML_SCALAR); - if (item) { - item->value = trim_dup(item_start, item_start + item_len); - node_add_child(list_parent, item); - } - - p = (eol < end) ? eol + 1 : end; - continue; - } - - /* Find colon separator for "key: value" or "key:" */ - const char *colon = memchr(content, ':', (size_t)content_len); - if (!colon) { - /* Not a valid YAML line — skip */ - p = (eol < end) ? eol + 1 : end; - continue; - } - - char *key = trim_dup(content, colon); - if (!key) { - p = (eol < end) ? eol + 1 : end; - continue; - } - - /* After colon: value or nothing */ - const char *after = colon + 1; - int after_len = content_len - (int)(after - content); - while (after_len > 0 && isspace((unsigned char)*after)) { - after++; - after_len--; - } - - /* Strip inline comments (but not inside quoted strings) */ - if (after_len > 0 && after[0] != '"' && after[0] != '\'') { - for (int i = 0; i < after_len; i++) { - if (after[i] == '#' && i > 0 && after[i - 1] == ' ') { - after_len = i; - while (after_len > 0 && isspace((unsigned char)after[after_len - 1])) { - after_len--; - } - break; - } - } - } - - if (after_len > 0) { - /* "key: value" — scalar */ - cbm_yaml_node_t *child = node_new(YAML_SCALAR); - if (child) { - child->key = key; - child->value = trim_dup(after, after + after_len); - node_add_child(parent, child); - } else { - free(key); - } + parse_list_item(content, content_len, parent); } else { - /* "key:" — could be map or list (determined by next lines) */ - /* Peek ahead to check if next content line is a list item */ - const char *peek = (eol < end) ? eol + 1 : end; - bool is_list = false; - while (peek < end) { - const char *peek_eol = memchr(peek, '\n', (size_t)(end - peek)); - if (!peek_eol) { - peek_eol = end; - } - int pi = leading_spaces(peek); - const char *pc = peek + pi; - int pcl = (int)(peek_eol - pc); - if (pcl > 0 && pc[pcl - 1] == '\r') { - pcl--; - } - if (pcl == 0 || pc[0] == '#') { - peek = (peek_eol < end) ? peek_eol + 1 : end; - continue; - } - if (pc[0] == '-') { - is_list = true; - } - break; - } - - cbm_yaml_node_t *child = node_new(is_list ? YAML_LIST : YAML_MAP); - if (child) { - child->key = key; - node_add_child(parent, child); - /* Push onto stack */ - if (stack_depth < 32) { - stack[stack_depth++] = (stack_entry_t){.node = child, .indent = indent}; - } - } else { - free(key); - } + parse_key_line(content, content_len, indent, parent, stack, &stack_depth, eol, end); } p = (eol < end) ? eol + 1 : end; diff --git a/src/graph_buffer/graph_buffer.c b/src/graph_buffer/graph_buffer.c index 3f330c6d7..13291d65b 100644 --- a/src/graph_buffer/graph_buffer.c +++ b/src/graph_buffer/graph_buffer.c @@ -149,6 +149,170 @@ static void free_edge_strings(cbm_gbuf_edge_t *e) { free(e->properties_json); } +/* Allocate the next buffer-local or shared-atomic ID. */ +static int64_t alloc_next_id(cbm_gbuf_t *gb) { + if (gb->shared_ids) { + return atomic_fetch_add_explicit(gb->shared_ids, 1, memory_order_relaxed); + } + return gb->next_id++; +} + +/* Swap-remove an edge from a pointer array by ID. */ +static void remove_edge_from_ptr_array(edge_ptr_array_t *arr, int64_t edge_id) { + if (!arr) { + return; + } + for (int j = 0; j < arr->count; j++) { + if (arr->items[j]->id == edge_id) { + arr->items[j] = arr->items[--arr->count]; + return; + } + } +} + +/* Swap-remove a node from a node_ptr_array by ID. */ +static void remove_node_from_ptr_array(node_ptr_array_t *arr, int64_t node_id) { + if (!arr) { + return; + } + for (int j = 0; j < arr->count; j++) { + if (arr->items[j]->id == node_id) { + arr->items[j] = arr->items[--arr->count]; + return; + } + } +} + +/* Remove an edge from all indexes (dedup + source_type + target_type + type). */ +static void unindex_edge(cbm_gbuf_t *gb, const cbm_gbuf_edge_t *e) { + char key[EDGE_KEY_BUF]; + + make_edge_key(key, sizeof(key), e->source_id, e->target_id, e->type); + const char *ekey = cbm_ht_get_key(gb->edge_by_key, key); + cbm_ht_delete(gb->edge_by_key, key); + free((void *)ekey); + + make_src_type_key(key, sizeof(key), e->source_id, e->type); + remove_edge_from_ptr_array(cbm_ht_get(gb->edges_by_source_type, key), e->id); + + make_src_type_key(key, sizeof(key), e->target_id, e->type); + remove_edge_from_ptr_array(cbm_ht_get(gb->edges_by_target_type, key), e->id); + + remove_edge_from_ptr_array(cbm_ht_get(gb->edges_by_type, e->type), e->id); +} + +/* Cascade-delete all edges touching nodes in deleted_set. */ +static void cascade_delete_edges(cbm_gbuf_t *gb, CBMHashTable *deleted_set) { + int write_idx = 0; + for (int i = 0; i < gb->edges.count; i++) { + cbm_gbuf_edge_t *e = gb->edges.items[i]; + char src_id[32]; + char tgt_id[32]; + make_id_key(src_id, sizeof(src_id), e->source_id); + make_id_key(tgt_id, sizeof(tgt_id), e->target_id); + + if (cbm_ht_get(deleted_set, src_id) || cbm_ht_get(deleted_set, tgt_id)) { + unindex_edge(gb, e); + free_edge_strings(e); + free(e); + } else { + gb->edges.items[write_idx++] = gb->edges.items[i]; + } + } + gb->edges.count = write_idx; +} + +/* Register a node in primary (QN, ID) and secondary (label, name) indexes. */ +static void register_node_in_indexes(cbm_gbuf_t *gb, cbm_gbuf_node_t *node) { + cbm_ht_set(gb->node_by_qn, node->qualified_name, node); + + char id_buf[32]; + make_id_key(id_buf, sizeof(id_buf), node->id); + const char *old_key = cbm_ht_get_key(gb->node_by_id, id_buf); + if (old_key) { + cbm_ht_set(gb->node_by_id, old_key, node); + } else { + cbm_ht_set(gb->node_by_id, strdup(id_buf), node); + } + + node_ptr_array_t *by_label = + get_or_create_node_array(gb->nodes_by_label, node->label ? node->label : ""); + cbm_da_push(by_label, (const cbm_gbuf_node_t *)node); + + node_ptr_array_t *by_name = + get_or_create_node_array(gb->nodes_by_name, node->name ? node->name : ""); + cbm_da_push(by_name, (const cbm_gbuf_node_t *)node); +} + +/* Push an edge pointer into a dynamic array (wraps macro to reduce CC contribution). */ +static void edge_array_push(edge_ptr_array_t *arr, const cbm_gbuf_edge_t *edge) { + cbm_da_push(arr, edge); +} + +/* Index an edge by one key into a hash table bucket. */ +static void index_edge_by_key(CBMHashTable *ht, const char *key, cbm_gbuf_edge_t *edge) { + edge_ptr_array_t *arr = get_or_create_edge_array(ht, key); + edge_array_push(arr, (const cbm_gbuf_edge_t *)edge); +} + +/* Register an edge in secondary indexes (source_type, target_type, type). */ +static void register_edge_in_indexes(cbm_gbuf_t *gb, cbm_gbuf_edge_t *edge) { + char key[EDGE_KEY_BUF]; + + make_src_type_key(key, sizeof(key), edge->source_id, edge->type); + index_edge_by_key(gb->edges_by_source_type, key, edge); + + make_src_type_key(key, sizeof(key), edge->target_id, edge->type); + index_edge_by_key(gb->edges_by_target_type, key, edge); + + index_edge_by_key(gb->edges_by_type, edge->type, edge); +} + +/* Rebuild edge secondary indexes from scratch (after bulk deletion). */ +static void rebuild_edge_secondary_indexes(cbm_gbuf_t *gb) { + cbm_ht_foreach(gb->edges_by_source_type, free_edge_array, NULL); + cbm_ht_free(gb->edges_by_source_type); + cbm_ht_foreach(gb->edges_by_target_type, free_edge_array, NULL); + cbm_ht_free(gb->edges_by_target_type); + cbm_ht_foreach(gb->edges_by_type, free_edge_array, NULL); + cbm_ht_free(gb->edges_by_type); + + gb->edges_by_source_type = cbm_ht_create(256); + gb->edges_by_target_type = cbm_ht_create(256); + gb->edges_by_type = cbm_ht_create(32); + + for (int i = 0; i < gb->edges.count; i++) { + register_edge_in_indexes(gb, gb->edges.items[i]); + } +} + +/* Release all lookup hash tables (used by dump after building arrays). */ +static void release_gbuf_indexes(cbm_gbuf_t *gb) { + cbm_ht_free(gb->node_by_qn); + gb->node_by_qn = NULL; + cbm_ht_foreach(gb->node_by_id, free_key_only, NULL); + cbm_ht_free(gb->node_by_id); + gb->node_by_id = NULL; + cbm_ht_foreach(gb->nodes_by_label, free_node_array, NULL); + cbm_ht_free(gb->nodes_by_label); + gb->nodes_by_label = NULL; + cbm_ht_foreach(gb->nodes_by_name, free_node_array, NULL); + cbm_ht_free(gb->nodes_by_name); + gb->nodes_by_name = NULL; + cbm_ht_foreach(gb->edge_by_key, free_key_only, NULL); + cbm_ht_free(gb->edge_by_key); + gb->edge_by_key = NULL; + cbm_ht_foreach(gb->edges_by_source_type, free_edge_array, NULL); + cbm_ht_free(gb->edges_by_source_type); + gb->edges_by_source_type = NULL; + cbm_ht_foreach(gb->edges_by_target_type, free_edge_array, NULL); + cbm_ht_free(gb->edges_by_target_type); + gb->edges_by_target_type = NULL; + cbm_ht_foreach(gb->edges_by_type, free_edge_array, NULL); + cbm_ht_free(gb->edges_by_type); + gb->edges_by_type = NULL; +} + /* ── Lifecycle ──────────────────────────────────────────────────── */ cbm_gbuf_t *cbm_gbuf_new(const char *project, const char *root_path) { @@ -264,7 +428,6 @@ void cbm_gbuf_set_next_id(cbm_gbuf_t *gb, int64_t next_id) { /* ── Node operations ─────────────────────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) int64_t cbm_gbuf_upsert_node(cbm_gbuf_t *gb, const char *label, const char *name, const char *qualified_name, const char *file_path, int start_line, int end_line, const char *properties_json) { @@ -302,12 +465,7 @@ int64_t cbm_gbuf_upsert_node(cbm_gbuf_t *gb, const char *label, const char *name return 0; } - int64_t id; - if (gb->shared_ids) { - id = atomic_fetch_add_explicit(gb->shared_ids, 1, memory_order_relaxed); - } else { - id = gb->next_id++; - } + int64_t id = alloc_next_id(gb); node->id = id; node->project = gb->project; node->label = heap_strdup(label); @@ -318,29 +476,9 @@ int64_t cbm_gbuf_upsert_node(cbm_gbuf_t *gb, const char *label, const char *name node->end_line = end_line; node->properties_json = heap_strdup(properties_json); - /* Store pointer in array */ + /* Store pointer in array and register in all indexes */ cbm_da_push(&gb->nodes, node); - - /* Primary indexes — QN key is borrowed from the node's own string */ - cbm_ht_set(gb->node_by_qn, node->qualified_name, node); - - char id_buf[32]; - make_id_key(id_buf, sizeof(id_buf), id); - /* Check if key exists — if so, reuse old heap key to avoid leak. - * cbm_ht_set replaces key pointer, leaking old strdup'd key. */ - const char *existing_key = cbm_ht_get_key(gb->node_by_id, id_buf); - if (existing_key) { - cbm_ht_set(gb->node_by_id, existing_key, node); - } else { - cbm_ht_set(gb->node_by_id, strdup(id_buf), node); - } - - /* Secondary indexes */ - node_ptr_array_t *by_label = get_or_create_node_array(gb->nodes_by_label, label ? label : ""); - cbm_da_push(by_label, (const cbm_gbuf_node_t *)node); - - node_ptr_array_t *by_name = get_or_create_node_array(gb->nodes_by_name, name ? name : ""); - cbm_da_push(by_name, (const cbm_gbuf_node_t *)node); + register_node_in_indexes(gb, node); return id; } @@ -398,7 +536,6 @@ int cbm_gbuf_node_count(const cbm_gbuf_t *gb) { return gb ? (int)cbm_ht_count(gb->node_by_qn) : 0; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) int cbm_gbuf_delete_by_label(cbm_gbuf_t *gb, const char *label) { if (!gb || !label) { return -1; @@ -420,7 +557,6 @@ int cbm_gbuf_delete_by_label(cbm_gbuf_t *gb, const char *label) { /* Remove from primary indexes */ cbm_ht_delete(gb->node_by_qn, n->qualified_name); - /* Free the strdup'd key stored in node_by_id (get before delete) */ const char *stored_key = cbm_ht_get_key(gb->node_by_id, id_buf); cbm_ht_delete(gb->node_by_id, id_buf); free((void *)stored_key); @@ -429,74 +565,14 @@ int cbm_gbuf_delete_by_label(cbm_gbuf_t *gb, const char *label) { /* Clear the label array */ cbm_da_clear(arr); - /* Cascade-delete edges referencing deleted nodes. - * Use hash set for O(1) membership check instead of O(n) scan. - * Incrementally remove from secondary indexes to avoid full rebuild. */ - int write_idx = 0; - for (int i = 0; i < gb->edges.count; i++) { - cbm_gbuf_edge_t *e = gb->edges.items[i]; - - char src_id[32]; - char tgt_id[32]; - make_id_key(src_id, sizeof(src_id), e->source_id); - make_id_key(tgt_id, sizeof(tgt_id), e->target_id); - - if (cbm_ht_get(deleted_set, src_id) || cbm_ht_get(deleted_set, tgt_id)) { - /* Remove from edge dedup index (free the strdup'd key) */ - char key[EDGE_KEY_BUF]; - make_edge_key(key, sizeof(key), e->source_id, e->target_id, e->type); - const char *ekey = cbm_ht_get_key(gb->edge_by_key, key); - cbm_ht_delete(gb->edge_by_key, key); - free((void *)ekey); - - /* Remove from secondary indexes incrementally */ - make_src_type_key(key, sizeof(key), e->source_id, e->type); - edge_ptr_array_t *st = cbm_ht_get(gb->edges_by_source_type, key); - if (st) { - for (int j = 0; j < st->count; j++) { - if (st->items[j]->id == e->id) { - st->items[j] = st->items[--st->count]; - break; - } - } - } - - make_src_type_key(key, sizeof(key), e->target_id, e->type); - edge_ptr_array_t *tt = cbm_ht_get(gb->edges_by_target_type, key); - if (tt) { - for (int j = 0; j < tt->count; j++) { - if (tt->items[j]->id == e->id) { - tt->items[j] = tt->items[--tt->count]; - break; - } - } - } - - edge_ptr_array_t *bt = cbm_ht_get(gb->edges_by_type, e->type); - if (bt) { - for (int j = 0; j < bt->count; j++) { - if (bt->items[j]->id == e->id) { - bt->items[j] = bt->items[--bt->count]; - break; - } - } - } - - /* Free the edge */ - free_edge_strings(e); - free(e); - } else { - gb->edges.items[write_idx++] = gb->edges.items[i]; - } - } - gb->edges.count = write_idx; + /* Cascade-delete edges referencing deleted nodes */ + cascade_delete_edges(gb, deleted_set); cbm_ht_foreach(deleted_set, free_key_only, NULL); cbm_ht_free(deleted_set); return 0; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) int cbm_gbuf_delete_by_file(cbm_gbuf_t *gb, const char *file_path) { if (!gb || !file_path) { return -1; @@ -521,28 +597,11 @@ int cbm_gbuf_delete_by_file(cbm_gbuf_t *gb, const char *file_path) { make_id_key(id_buf, sizeof(id_buf), n->id); cbm_ht_set(deleted_set, strdup(id_buf), (void *)1); - /* Remove from label index */ - node_ptr_array_t *label_arr = cbm_ht_get(gb->nodes_by_label, n->label); - if (label_arr) { - for (int j = 0; j < label_arr->count; j++) { - if (label_arr->items[j]->id == n->id) { - label_arr->items[j] = label_arr->items[--label_arr->count]; - break; - } - } - } - - /* Remove from name index */ - node_ptr_array_t *name_arr = cbm_ht_get(gb->nodes_by_name, n->name); - if (name_arr) { - for (int j = 0; j < name_arr->count; j++) { - if (name_arr->items[j]->id == n->id) { - name_arr->items[j] = name_arr->items[--name_arr->count]; - break; - } - } - } + /* Remove from secondary indexes */ + remove_node_from_ptr_array(cbm_ht_get(gb->nodes_by_label, n->label), n->id); + remove_node_from_ptr_array(cbm_ht_get(gb->nodes_by_name, n->name), n->id); + /* Remove from primary indexes */ cbm_ht_delete(gb->node_by_qn, n->qualified_name); const char *stored_key = cbm_ht_get_key(gb->node_by_id, id_buf); cbm_ht_delete(gb->node_by_id, id_buf); @@ -561,60 +620,7 @@ int cbm_gbuf_delete_by_file(cbm_gbuf_t *gb, const char *file_path) { } /* Cascade-delete edges referencing deleted nodes */ - int write_idx = 0; - for (int i = 0; i < gb->edges.count; i++) { - cbm_gbuf_edge_t *e = gb->edges.items[i]; - char src_id[32]; - char tgt_id[32]; - make_id_key(src_id, sizeof(src_id), e->source_id); - make_id_key(tgt_id, sizeof(tgt_id), e->target_id); - - if (cbm_ht_get(deleted_set, src_id) || cbm_ht_get(deleted_set, tgt_id)) { - char key[EDGE_KEY_BUF]; - make_edge_key(key, sizeof(key), e->source_id, e->target_id, e->type); - const char *ekey = cbm_ht_get_key(gb->edge_by_key, key); - cbm_ht_delete(gb->edge_by_key, key); - free((void *)ekey); - - make_src_type_key(key, sizeof(key), e->source_id, e->type); - edge_ptr_array_t *st = cbm_ht_get(gb->edges_by_source_type, key); - if (st) { - for (int j = 0; j < st->count; j++) { - if (st->items[j]->id == e->id) { - st->items[j] = st->items[--st->count]; - break; - } - } - } - - make_src_type_key(key, sizeof(key), e->target_id, e->type); - edge_ptr_array_t *tt = cbm_ht_get(gb->edges_by_target_type, key); - if (tt) { - for (int j = 0; j < tt->count; j++) { - if (tt->items[j]->id == e->id) { - tt->items[j] = tt->items[--tt->count]; - break; - } - } - } - - edge_ptr_array_t *bt = cbm_ht_get(gb->edges_by_type, e->type); - if (bt) { - for (int j = 0; j < bt->count; j++) { - if (bt->items[j]->id == e->id) { - bt->items[j] = bt->items[--bt->count]; - break; - } - } - } - - free_edge_strings(e); - free(e); - } else { - gb->edges.items[write_idx++] = gb->edges.items[i]; - } - } - gb->edges.count = write_idx; + cascade_delete_edges(gb, deleted_set); cbm_ht_foreach(deleted_set, free_key_only, NULL); cbm_ht_free(deleted_set); @@ -746,7 +752,6 @@ void cbm_gbuf_foreach_edge(const cbm_gbuf_t *gb, cbm_gbuf_edge_visitor_fn fn, vo /* ── Edge operations ─────────────────────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) int64_t cbm_gbuf_insert_edge(cbm_gbuf_t *gb, int64_t source_id, int64_t target_id, const char *type, const char *properties_json) { if (!gb || !type) { @@ -773,12 +778,7 @@ int64_t cbm_gbuf_insert_edge(cbm_gbuf_t *gb, int64_t source_id, int64_t target_i return 0; } - int64_t id; - if (gb->shared_ids) { - id = atomic_fetch_add_explicit(gb->shared_ids, 1, memory_order_relaxed); - } else { - id = gb->next_id++; - } + int64_t id = alloc_next_id(gb); edge->id = id; edge->project = gb->project; edge->source_id = source_id; @@ -793,16 +793,7 @@ int64_t cbm_gbuf_insert_edge(cbm_gbuf_t *gb, int64_t source_id, int64_t target_i cbm_ht_set(gb->edge_by_key, strdup(key), edge); /* Secondary indexes */ - make_src_type_key(key, sizeof(key), source_id, type); - edge_ptr_array_t *st = get_or_create_edge_array(gb->edges_by_source_type, key); - cbm_da_push(st, (const cbm_gbuf_edge_t *)edge); - - make_src_type_key(key, sizeof(key), target_id, type); - edge_ptr_array_t *tt = get_or_create_edge_array(gb->edges_by_target_type, key); - cbm_da_push(tt, (const cbm_gbuf_edge_t *)edge); - - edge_ptr_array_t *bt = get_or_create_edge_array(gb->edges_by_type, type); - cbm_da_push(bt, (const cbm_gbuf_edge_t *)edge); + register_edge_in_indexes(gb, edge); return id; } @@ -871,13 +862,12 @@ int cbm_gbuf_edge_count_by_type(const cbm_gbuf_t *gb, const char *type) { return arr ? arr->count : 0; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) int cbm_gbuf_delete_edges_by_type(cbm_gbuf_t *gb, const char *type) { if (!gb || !type) { return -1; } - /* Rebuild edges array without the deleted type */ + /* Remove edges of the given type from array and dedup index */ int write_idx = 0; for (int i = 0; i < gb->edges.count; i++) { cbm_gbuf_edge_t *e = gb->edges.items[i]; @@ -896,32 +886,7 @@ int cbm_gbuf_delete_edges_by_type(cbm_gbuf_t *gb, const char *type) { gb->edges.count = write_idx; /* Rebuild edge secondary indexes */ - cbm_ht_foreach(gb->edges_by_source_type, free_edge_array, NULL); - cbm_ht_free(gb->edges_by_source_type); - cbm_ht_foreach(gb->edges_by_target_type, free_edge_array, NULL); - cbm_ht_free(gb->edges_by_target_type); - cbm_ht_foreach(gb->edges_by_type, free_edge_array, NULL); - cbm_ht_free(gb->edges_by_type); - - gb->edges_by_source_type = cbm_ht_create(256); - gb->edges_by_target_type = cbm_ht_create(256); - gb->edges_by_type = cbm_ht_create(32); - - for (int i = 0; i < gb->edges.count; i++) { - cbm_gbuf_edge_t *e = gb->edges.items[i]; - char key[EDGE_KEY_BUF]; - - make_src_type_key(key, sizeof(key), e->source_id, e->type); - edge_ptr_array_t *st = get_or_create_edge_array(gb->edges_by_source_type, key); - cbm_da_push(st, (const cbm_gbuf_edge_t *)e); - - make_src_type_key(key, sizeof(key), e->target_id, e->type); - edge_ptr_array_t *tt = get_or_create_edge_array(gb->edges_by_target_type, key); - cbm_da_push(tt, (const cbm_gbuf_edge_t *)e); - - edge_ptr_array_t *bt = get_or_create_edge_array(gb->edges_by_type, e->type); - cbm_da_push(bt, (const cbm_gbuf_edge_t *)e); - } + rebuild_edge_secondary_indexes(gb); return 0; } @@ -935,7 +900,86 @@ static void free_remap_entry(const char *key, void *val, void *ud) { free(val); } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Handle QN collision: update dst node fields (src wins), record remap if IDs differ. */ +static void merge_update_existing(cbm_gbuf_node_t *existing, const cbm_gbuf_node_t *sn, + CBMHashTable **remap) { + free(existing->label); + existing->label = heap_strdup(sn->label); + free(existing->name); + existing->name = heap_strdup(sn->name); + free(existing->file_path); + existing->file_path = heap_strdup(sn->file_path); + existing->start_line = sn->start_line; + existing->end_line = sn->end_line; + if (sn->properties_json) { + free(existing->properties_json); + existing->properties_json = heap_strdup(sn->properties_json); + } + + if (sn->id != existing->id) { + if (!*remap) { + *remap = cbm_ht_create(32); + } + char key[32]; + make_id_key(key, sizeof(key), sn->id); + int64_t *val = malloc(sizeof(int64_t)); + *val = existing->id; + cbm_ht_set(*remap, strdup(key), val); + } +} + +/* Copy a non-colliding src node into dst with its original ID. */ +static void merge_copy_new_node(cbm_gbuf_t *dst, const cbm_gbuf_node_t *sn) { + cbm_gbuf_node_t *node = calloc(1, sizeof(cbm_gbuf_node_t)); + if (!node) { + return; + } + + node->id = sn->id; + node->project = dst->project; + node->label = heap_strdup(sn->label); + node->name = heap_strdup(sn->name); + node->qualified_name = heap_strdup(sn->qualified_name); + node->file_path = heap_strdup(sn->file_path); + node->start_line = sn->start_line; + node->end_line = sn->end_line; + node->properties_json = heap_strdup(sn->properties_json); + + cbm_da_push(&dst->nodes, node); + register_node_in_indexes(dst, node); + + if (node->id >= dst->next_id) { + dst->next_id = node->id + 1; + } +} + +/* Remap edge IDs using the collision remap table and insert into dst. */ +static void merge_remap_edges(cbm_gbuf_t *dst, cbm_gbuf_t *src, CBMHashTable *remap) { + for (int i = 0; i < src->edges.count; i++) { + cbm_gbuf_edge_t *se = src->edges.items[i]; + + int64_t new_src = se->source_id; + int64_t new_tgt = se->target_id; + + if (remap) { + char key[32]; + make_id_key(key, sizeof(key), se->source_id); + int64_t *remapped = cbm_ht_get(remap, key); + if (remapped) { + new_src = *remapped; + } + + make_id_key(key, sizeof(key), se->target_id); + remapped = cbm_ht_get(remap, key); + if (remapped) { + new_tgt = *remapped; + } + } + + cbm_gbuf_insert_edge(dst, new_src, new_tgt, se->type, se->properties_json); + } +} + int cbm_gbuf_merge(cbm_gbuf_t *dst, cbm_gbuf_t *src) { if (!dst || !src) { return -1; @@ -961,102 +1005,14 @@ int cbm_gbuf_merge(cbm_gbuf_t *dst, cbm_gbuf_t *src) { cbm_gbuf_node_t *existing = cbm_ht_get(dst->node_by_qn, sn->qualified_name); if (existing) { - /* QN collision: update dst node fields (src wins), keep dst's ID */ - free(existing->label); - existing->label = heap_strdup(sn->label); - free(existing->name); - existing->name = heap_strdup(sn->name); - free(existing->file_path); - existing->file_path = heap_strdup(sn->file_path); - existing->start_line = sn->start_line; - existing->end_line = sn->end_line; - if (sn->properties_json) { - free(existing->properties_json); - existing->properties_json = heap_strdup(sn->properties_json); - } - - /* Record remap if IDs differ */ - if (sn->id != existing->id) { - if (!remap) { - remap = cbm_ht_create(32); - } - char key[32]; - make_id_key(key, sizeof(key), sn->id); - int64_t *val = malloc(sizeof(int64_t)); - *val = existing->id; - cbm_ht_set(remap, strdup(key), val); - } + merge_update_existing(existing, sn, &remap); } else { - /* No collision: copy node with original src ID */ - cbm_gbuf_node_t *node = calloc(1, sizeof(cbm_gbuf_node_t)); - if (!node) { - continue; - } - - node->id = sn->id; - node->project = dst->project; - node->label = heap_strdup(sn->label); - node->name = heap_strdup(sn->name); - node->qualified_name = heap_strdup(sn->qualified_name); - node->file_path = heap_strdup(sn->file_path); - node->start_line = sn->start_line; - node->end_line = sn->end_line; - node->properties_json = heap_strdup(sn->properties_json); - - /* Store in array */ - cbm_da_push(&dst->nodes, node); - - /* Primary indexes */ - cbm_ht_set(dst->node_by_qn, node->qualified_name, node); - char id_buf[32]; - make_id_key(id_buf, sizeof(id_buf), node->id); - const char *old_id_key = cbm_ht_get_key(dst->node_by_id, id_buf); - if (old_id_key) { - cbm_ht_set(dst->node_by_id, old_id_key, node); - } else { - cbm_ht_set(dst->node_by_id, strdup(id_buf), node); - } - - /* Secondary indexes */ - node_ptr_array_t *by_label = - get_or_create_node_array(dst->nodes_by_label, node->label ? node->label : ""); - cbm_da_push(by_label, (const cbm_gbuf_node_t *)node); - - node_ptr_array_t *by_name = - get_or_create_node_array(dst->nodes_by_name, node->name ? node->name : ""); - cbm_da_push(by_name, (const cbm_gbuf_node_t *)node); - - /* Keep next_id above all inserted IDs */ - if (node->id >= dst->next_id) { - dst->next_id = node->id + 1; - } + merge_copy_new_node(dst, sn); } } /* Merge edges with optional ID remapping */ - for (int i = 0; i < src->edges.count; i++) { - cbm_gbuf_edge_t *se = src->edges.items[i]; - - int64_t new_src = se->source_id; - int64_t new_tgt = se->target_id; - - if (remap) { - char key[32]; - make_id_key(key, sizeof(key), se->source_id); - int64_t *remapped = cbm_ht_get(remap, key); - if (remapped) { - new_src = *remapped; - } - - make_id_key(key, sizeof(key), se->target_id); - remapped = cbm_ht_get(remap, key); - if (remapped) { - new_tgt = *remapped; - } - } - - cbm_gbuf_insert_edge(dst, new_src, new_tgt, se->type, se->properties_json); - } + merge_remap_edges(dst, src, remap); if (remap) { cbm_ht_foreach(remap, free_remap_entry, NULL); @@ -1086,7 +1042,96 @@ static char *extract_url_path(const char *props) { return cbm_strndup(key, (size_t)(end - key)); } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Remap a temp edge ID to its final sequential ID, or 0 if out of range. */ +static int64_t remap_id(const int64_t *temp_to_final, int64_t max_temp_id, int64_t temp_id) { + return (temp_id < max_temp_id) ? temp_to_final[temp_id] : 0; +} + +/* Build dump-ready node array with sequential IDs. Populates temp_to_final mapping. */ +static CBMDumpNode *build_dump_nodes(cbm_gbuf_t *gb, int live_count, int64_t *temp_to_final, + int64_t max_temp_id, int *out_count) { + CBMDumpNode *dump_nodes = + malloc((size_t)(live_count > 0 ? live_count : 1) * sizeof(CBMDumpNode)); + int idx = 0; + + for (int i = 0; i < gb->nodes.count; i++) { + cbm_gbuf_node_t *n = gb->nodes.items[i]; + if (!n->qualified_name || !cbm_ht_get(gb->node_by_qn, n->qualified_name)) { + continue; + } + + int64_t final_id = idx + 1; /* 1-based sequential */ + if (n->id < max_temp_id) { + temp_to_final[n->id] = final_id; + } + + const char *fp = n->file_path ? n->file_path : ""; + const char *props = n->properties_json ? n->properties_json : "{}"; + dump_nodes[idx] = (CBMDumpNode){ + .id = final_id, + .project = gb->project, + .label = n->label, + .name = n->name, + .qualified_name = n->qualified_name, + .file_path = fp, + .start_line = n->start_line, + .end_line = n->end_line, + .properties = props, + }; + idx++; + } + + *out_count = idx; + return dump_nodes; +} + +/* Build dump-ready edge array with remapped IDs. Returns url_paths via out param. */ +static CBMDumpEdge *build_dump_edges(cbm_gbuf_t *gb, const int64_t *temp_to_final, + int64_t max_temp_id, int *out_count, char ***out_url_paths) { + /* Count valid edges (both endpoints resolved) */ + int valid_edges = 0; + for (int i = 0; i < gb->edges.count; i++) { + cbm_gbuf_edge_t *e = gb->edges.items[i]; + if (remap_id(temp_to_final, max_temp_id, e->source_id) > 0 && + remap_id(temp_to_final, max_temp_id, e->target_id) > 0) { + valid_edges++; + } + } + + CBMDumpEdge *dump_edges = + malloc((size_t)(valid_edges > 0 ? valid_edges : 1) * sizeof(CBMDumpEdge)); + char **url_paths = calloc((size_t)(valid_edges > 0 ? valid_edges : 1), sizeof(char *)); + int idx = 0; + + for (int i = 0; i < gb->edges.count; i++) { + cbm_gbuf_edge_t *e = gb->edges.items[i]; + int64_t src = remap_id(temp_to_final, max_temp_id, e->source_id); + int64_t tgt = remap_id(temp_to_final, max_temp_id, e->target_id); + if (src == 0 || tgt == 0) { + continue; + } + + char *url_path = extract_url_path(e->properties_json); + url_paths[idx] = url_path; + + const char *props = e->properties_json ? e->properties_json : "{}"; + dump_edges[idx] = (CBMDumpEdge){ + .id = idx + 1, + .project = gb->project, + .source_id = src, + .target_id = tgt, + .type = e->type, + .properties = props, + .url_path = url_path ? url_path : "", + }; + idx++; + } + + *out_count = idx; + *out_url_paths = url_paths; + return dump_edges; +} + int cbm_gbuf_dump_to_sqlite(cbm_gbuf_t *gb, const char *path) { if (!gb || !path) { return -1; @@ -1108,75 +1153,14 @@ int cbm_gbuf_dump_to_sqlite(cbm_gbuf_t *gb, const char *path) { return -1; } - /* Build CBMDumpNode array with sequential IDs (1..N) */ - CBMDumpNode *dump_nodes = - malloc((size_t)(live_count > 0 ? live_count : 1) * sizeof(CBMDumpNode)); int node_idx = 0; + CBMDumpNode *dump_nodes = + build_dump_nodes(gb, live_count, temp_to_final, max_temp_id, &node_idx); - for (int i = 0; i < gb->nodes.count; i++) { - cbm_gbuf_node_t *n = gb->nodes.items[i]; - if (!n->qualified_name || !cbm_ht_get(gb->node_by_qn, n->qualified_name)) { - continue; - } - - int64_t final_id = node_idx + 1; /* 1-based sequential */ - if (n->id < max_temp_id) { - temp_to_final[n->id] = final_id; - } - - dump_nodes[node_idx] = (CBMDumpNode){ - .id = final_id, - .project = gb->project, - .label = n->label, - .name = n->name, - .qualified_name = n->qualified_name, - .file_path = n->file_path ? n->file_path : "", - .start_line = n->start_line, - .end_line = n->end_line, - .properties = n->properties_json ? n->properties_json : "{}", - }; - node_idx++; - } - - /* Count valid edges (both endpoints resolved) */ - int valid_edges = 0; - for (int i = 0; i < gb->edges.count; i++) { - cbm_gbuf_edge_t *e = gb->edges.items[i]; - int64_t src = (e->source_id < max_temp_id) ? temp_to_final[e->source_id] : 0; - int64_t tgt = (e->target_id < max_temp_id) ? temp_to_final[e->target_id] : 0; - if (src > 0 && tgt > 0) { - valid_edges++; - } - } - - /* Build CBMDumpEdge array with remapped IDs */ - CBMDumpEdge *dump_edges = - malloc((size_t)(valid_edges > 0 ? valid_edges : 1) * sizeof(CBMDumpEdge)); - char **url_paths = calloc((size_t)(valid_edges > 0 ? valid_edges : 1), sizeof(char *)); int edge_idx = 0; - - for (int i = 0; i < gb->edges.count; i++) { - cbm_gbuf_edge_t *e = gb->edges.items[i]; - int64_t src = (e->source_id < max_temp_id) ? temp_to_final[e->source_id] : 0; - int64_t tgt = (e->target_id < max_temp_id) ? temp_to_final[e->target_id] : 0; - if (src == 0 || tgt == 0) { - continue; - } - - char *url_path = extract_url_path(e->properties_json); - url_paths[edge_idx] = url_path; - - dump_edges[edge_idx] = (CBMDumpEdge){ - .id = edge_idx + 1, - .project = gb->project, - .source_id = src, - .target_id = tgt, - .type = e->type, - .properties = e->properties_json ? e->properties_json : "{}", - .url_path = url_path ? url_path : "", - }; - edge_idx++; - } + char **url_paths = NULL; + CBMDumpEdge *dump_edges = + build_dump_edges(gb, temp_to_final, max_temp_id, &edge_idx, &url_paths); /* Generate ISO 8601 timestamp */ time_t now = time(NULL); @@ -1187,29 +1171,7 @@ int cbm_gbuf_dump_to_sqlite(cbm_gbuf_t *gb, const char *path) { /* Release lookup tables — no longer needed now that dump arrays are built. * Frees hash tables (~200-400MB on large codebases) while keeping the * raw node/edge data alive for cbm_write_db() to read via dump arrays. */ - cbm_ht_free(gb->node_by_qn); - gb->node_by_qn = NULL; - cbm_ht_foreach(gb->node_by_id, free_key_only, NULL); - cbm_ht_free(gb->node_by_id); - gb->node_by_id = NULL; - cbm_ht_foreach(gb->nodes_by_label, free_node_array, NULL); - cbm_ht_free(gb->nodes_by_label); - gb->nodes_by_label = NULL; - cbm_ht_foreach(gb->nodes_by_name, free_node_array, NULL); - cbm_ht_free(gb->nodes_by_name); - gb->nodes_by_name = NULL; - cbm_ht_foreach(gb->edge_by_key, free_key_only, NULL); - cbm_ht_free(gb->edge_by_key); - gb->edge_by_key = NULL; - cbm_ht_foreach(gb->edges_by_source_type, free_edge_array, NULL); - cbm_ht_free(gb->edges_by_source_type); - gb->edges_by_source_type = NULL; - cbm_ht_foreach(gb->edges_by_target_type, free_edge_array, NULL); - cbm_ht_free(gb->edges_by_target_type); - gb->edges_by_target_type = NULL; - cbm_ht_foreach(gb->edges_by_type, free_edge_array, NULL); - cbm_ht_free(gb->edges_by_type); - gb->edges_by_type = NULL; + release_gbuf_indexes(gb); /* Write directly to final path — no .tmp + rename. * Callers must delete the old .db before calling this (reindex) diff --git a/src/main.c b/src/main.c index 0574afad5..979ee2fc5 100644 --- a/src/main.c +++ b/src/main.c @@ -154,9 +154,9 @@ static void print_help(void) { /* ── Main ───────────────────────────────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) -int main(int argc, char **argv) { - /* Parse arguments */ +/* Try to handle a subcommand (cli/install/uninstall/update/config/--version/--help). + * Returns -1 if no subcommand matched, otherwise the exit code. */ +static int handle_subcommand(int argc, char **argv) { for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--version") == 0) { printf("codebase-memory-mcp %s\n", CBM_VERSION); @@ -183,6 +183,48 @@ int main(int argc, char **argv) { return cbm_cmd_config(argc - i - 1, argv + i + 1); } } + return -1; +} + +/* Parse --ui= and --port= flags. Returns true if config was modified. */ +static bool parse_ui_flags(int argc, char **argv, cbm_ui_config_t *cfg) { + bool changed = false; + for (int i = 1; i < argc; i++) { + if (strncmp(argv[i], "--ui=", 5) == 0) { + cfg->ui_enabled = (strcmp(argv[i] + 5, "true") == 0); + changed = true; + } + if (strncmp(argv[i], "--port=", 7) == 0) { + int p = (int)strtol(argv[i] + 7, NULL, 10); + if (p > 0 && p < 65536) { + cfg->ui_port = p; + changed = true; + } + } + } + return changed; +} + +/* Install platform-specific signal handlers. */ +static void setup_signal_handlers(void) { +#ifdef _WIN32 + signal(SIGTERM, signal_handler); + signal(SIGINT, signal_handler); +#else + struct sigaction sa = {0}; + sa.sa_handler = signal_handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sigaction(SIGTERM, &sa, NULL); + sigaction(SIGINT, &sa, NULL); +#endif +} + +int main(int argc, char **argv) { + int subcmd = handle_subcommand(argc, argv); + if (subcmd >= 0) { + return subcmd; + } /* Default: MCP server on stdio */ cbm_mem_init(0.5); /* 50% of RAM — safe now because mimalloc tracks ALL @@ -197,37 +239,11 @@ int main(int argc, char **argv) { /* Parse --ui and --port flags (persisted config) */ cbm_ui_config_t ui_cfg; cbm_ui_config_load(&ui_cfg); - - bool config_changed = false; - for (int i = 1; i < argc; i++) { - if (strncmp(argv[i], "--ui=", 5) == 0) { - ui_cfg.ui_enabled = (strcmp(argv[i] + 5, "true") == 0); - config_changed = true; - } - if (strncmp(argv[i], "--port=", 7) == 0) { - int p = (int)strtol(argv[i] + 7, NULL, 10); - if (p > 0 && p < 65536) { - ui_cfg.ui_port = p; - config_changed = true; - } - } - } - if (config_changed) { + if (parse_ui_flags(argc, argv, &ui_cfg)) { cbm_ui_config_save(&ui_cfg); } - /* Install signal handlers */ -#ifdef _WIN32 - signal(SIGTERM, signal_handler); - signal(SIGINT, signal_handler); -#else - struct sigaction sa = {0}; - sa.sa_handler = signal_handler; - sigemptyset(&sa.sa_mask); - sa.sa_flags = 0; - sigaction(SIGTERM, &sa, NULL); - sigaction(SIGINT, &sa, NULL); -#endif + setup_signal_handlers(); /* Open config store for runtime settings */ char config_dir[1024]; diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index a11f974fe..dbc1ef4c8 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -696,40 +696,45 @@ static cbm_store_t *resolve_store(cbm_mcp_server_t *srv, const char *project) { return srv->store; } +/* Scan cache dir for .db files, writing comma-separated quoted names into out. + * Returns the number of projects found. */ +static int collect_db_project_names(const char *dir_path, char *out, size_t out_sz) { + int count = 0; + int offset = 0; + cbm_dir_t *d = cbm_opendir(dir_path); + if (!d) { + return 0; + } + cbm_dirent_t *entry; + while ((entry = cbm_readdir(d)) != NULL) { + const char *n = entry->name; + size_t len = strlen(n); + if (len < 4 || strcmp(n + len - 3, ".db") != 0) { + continue; + } + if (strncmp(n, "tmp-", 4) == 0 || strncmp(n, "_", 1) == 0) { + continue; + } + if (count > 0 && offset < (int)out_sz - 2) { + out[offset++] = ','; + } + int wrote = snprintf(out + offset, out_sz - (size_t)offset, "\"%.*s\"", (int)(len - 3), n); + if (wrote > 0) { + offset += wrote; + } + count++; + } + cbm_closedir(d); + return count; +} + /* Build a helpful error listing available projects. Caller must free() result. */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) static char *build_project_list_error(const char *reason) { char dir_path[1024]; cache_dir(dir_path, sizeof(dir_path)); - /* Collect project names from .db files */ char projects[4096] = ""; - int count = 0; - cbm_dir_t *d = cbm_opendir(dir_path); - if (d) { - int offset = 0; - cbm_dirent_t *entry; - while ((entry = cbm_readdir(d)) != NULL) { - const char *n = entry->name; - size_t len = strlen(n); - if (len < 4 || strcmp(n + len - 3, ".db") != 0) { - continue; - } - if (strncmp(n, "tmp-", 4) == 0 || strncmp(n, "_", 1) == 0) { - continue; - } - if (count > 0 && offset < (int)sizeof(projects) - 2) { - projects[offset++] = ','; - } - int wrote = snprintf(projects + offset, sizeof(projects) - (size_t)offset, "\"%.*s\"", - (int)(len - 3), n); - if (wrote > 0) { - offset += wrote; - } - count++; - } - cbm_closedir(d); - } + int count = collect_db_project_names(dir_path, projects, sizeof(projects)); enum { ERR_BUF_SZ = 5120 }; char buf[ERR_BUF_SZ]; @@ -761,9 +766,58 @@ static char *build_project_list_error(const char *reason) { /* ── Tool handler implementations ─────────────────────────────── */ +/* Return true if filename is a valid project .db file (not temp/internal). */ +static bool is_project_db_file(const char *name, size_t len) { + if (len < 4 || strcmp(name + len - 3, ".db") != 0) { + return false; + } + if (strncmp(name, "tmp-", 4) == 0 || strncmp(name, "_", 1) == 0 || + strncmp(name, ":memory:", 8) == 0) { + return false; + } + return true; +} + +/* Open a .db file briefly, collect node/edge counts and root_path, + * then append a JSON entry to arr. */ +static void build_project_json_entry(yyjson_mut_doc *doc, yyjson_mut_val *arr, const char *dir_path, + const char *name, size_t name_len, const struct stat *st) { + char project_name[1024]; + snprintf(project_name, sizeof(project_name), "%.*s", (int)(name_len - 3), name); + + char full_path[2048]; + snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, name); + + cbm_store_t *pstore = cbm_store_open_path(full_path); + int nodes = 0; + int edges = 0; + char root_path_buf[1024] = ""; + if (pstore) { + nodes = cbm_store_count_nodes(pstore, project_name); + edges = cbm_store_count_edges(pstore, project_name); + cbm_project_t proj = {0}; + if (cbm_store_get_project(pstore, project_name, &proj) == CBM_STORE_OK) { + if (proj.root_path) { + snprintf(root_path_buf, sizeof(root_path_buf), "%s", proj.root_path); + } + free((void *)proj.name); + free((void *)proj.indexed_at); + free((void *)proj.root_path); + } + cbm_store_close(pstore); + } + + yyjson_mut_val *p = yyjson_mut_obj(doc); + yyjson_mut_obj_add_strcpy(doc, p, "name", project_name); + yyjson_mut_obj_add_strcpy(doc, p, "root_path", root_path_buf); + yyjson_mut_obj_add_int(doc, p, "nodes", nodes); + yyjson_mut_obj_add_int(doc, p, "edges", edges); + yyjson_mut_obj_add_int(doc, p, "size_bytes", (int64_t)st->st_size); + yyjson_mut_arr_add_val(arr, p); +} + /* list_projects: scan cache directory for .db files. * Each project is a single .db file — no central registry needed. */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) static char *handle_list_projects(cbm_mcp_server_t *srv, const char *args) { (void)srv; (void)args; @@ -783,57 +837,16 @@ static char *handle_list_projects(cbm_mcp_server_t *srv, const char *args) { while ((entry = cbm_readdir(d)) != NULL) { const char *name = entry->name; size_t len = strlen(name); - - /* Must end with .db and be at least 4 chars (x.db) */ - if (len < 4 || strcmp(name + len - 3, ".db") != 0) { + if (!is_project_db_file(name, len)) { continue; } - - /* Skip temp/internal files */ - if (strncmp(name, "tmp-", 4) == 0 || strncmp(name, "_", 1) == 0 || - strncmp(name, ":memory:", 8) == 0) { - continue; - } - - /* Extract project name = filename without .db suffix */ - char project_name[1024]; - snprintf(project_name, sizeof(project_name), "%.*s", (int)(len - 3), name); - - /* Get file metadata */ char full_path[2048]; snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, name); struct stat st; if (stat(full_path, &st) != 0) { continue; } - - /* Open briefly to get node/edge count + root_path */ - cbm_store_t *pstore = cbm_store_open_path(full_path); - int nodes = 0; - int edges = 0; - char root_path_buf[1024] = ""; - if (pstore) { - nodes = cbm_store_count_nodes(pstore, project_name); - edges = cbm_store_count_edges(pstore, project_name); - cbm_project_t proj = {0}; - if (cbm_store_get_project(pstore, project_name, &proj) == CBM_STORE_OK) { - if (proj.root_path) { - snprintf(root_path_buf, sizeof(root_path_buf), "%s", proj.root_path); - } - free((void *)proj.name); - free((void *)proj.indexed_at); - free((void *)proj.root_path); - } - cbm_store_close(pstore); - } - - yyjson_mut_val *p = yyjson_mut_obj(doc); - yyjson_mut_obj_add_strcpy(doc, p, "name", project_name); - yyjson_mut_obj_add_strcpy(doc, p, "root_path", root_path_buf); - yyjson_mut_obj_add_int(doc, p, "nodes", nodes); - yyjson_mut_obj_add_int(doc, p, "edges", edges); - yyjson_mut_obj_add_int(doc, p, "size_bytes", (int64_t)st.st_size); - yyjson_mut_arr_add_val(arr, p); + build_project_json_entry(doc, arr, dir_path, name, len, &st); } cbm_closedir(d); } @@ -1221,7 +1234,70 @@ static char *handle_get_architecture(cbm_mcp_server_t *srv, const char *args) { return result; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Resolve edge types from args: explicit array > mode-based > default ("CALLS"). + * Writes types into out_types (max 16). Returns the parsed yyjson_doc if explicit + * edge_types were found (caller must keep alive until types are consumed), or NULL. */ +static yyjson_doc *resolve_trace_edge_types(const char *args, const char *mode, + const char **out_types, int *out_count) { + static const char *mode_calls[] = {"CALLS"}; + static const char *mode_data_flow[] = {"CALLS", "DATA_FLOWS"}; + static const char *mode_cross_svc[] = {"HTTP_CALLS", "ASYNC_CALLS", "DATA_FLOWS", "CALLS"}; + + *out_count = 0; + + yyjson_doc *et_doc = yyjson_read(args, strlen(args), 0); + if (et_doc) { + yyjson_val *et_arr = yyjson_obj_get(yyjson_doc_get_root(et_doc), "edge_types"); + if (et_arr && yyjson_is_arr(et_arr)) { + size_t idx2; + size_t max2; + yyjson_val *val2; + yyjson_arr_foreach(et_arr, idx2, max2, val2) { + if (yyjson_is_str(val2) && *out_count < 16) { + out_types[(*out_count)++] = yyjson_get_str(val2); + } + } + } + } + + if (*out_count > 0) { + return et_doc; /* caller must keep alive — pointers reference doc memory */ + } + + yyjson_doc_free(et_doc); /* no explicit types found, free */ + + const char **defaults = mode_calls; + int n_defaults = 1; + if (mode && strcmp(mode, "data_flow") == 0) { + defaults = mode_data_flow; + n_defaults = 2; + } else if (mode && strcmp(mode, "cross_service") == 0) { + defaults = mode_cross_svc; + n_defaults = 4; + } + for (int i = 0; i < n_defaults; i++) { + out_types[i] = defaults[i]; + } + *out_count = n_defaults; + return NULL; +} + +/* Convert BFS traversal results into a yyjson_mut array of {name, qualified_name, hop}. */ +static yyjson_mut_val *bfs_to_json_array(yyjson_mut_doc *doc, cbm_traverse_result_t *tr) { + yyjson_mut_val *arr = yyjson_mut_arr(doc); + for (int i = 0; i < tr->visited_count; i++) { + yyjson_mut_val *item = yyjson_mut_obj(doc); + yyjson_mut_obj_add_str(doc, item, "name", + tr->visited[i].node.name ? tr->visited[i].node.name : ""); + yyjson_mut_obj_add_str( + doc, item, "qualified_name", + tr->visited[i].node.qualified_name ? tr->visited[i].node.qualified_name : ""); + yyjson_mut_obj_add_int(doc, item, "hop", tr->visited[i].hop); + yyjson_mut_arr_add_val(arr, item); + } + return arr; +} + static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) { char *func_name = cbm_mcp_get_string_arg(args, "function_name"); char *project = cbm_mcp_get_string_arg(args, "project"); @@ -1290,50 +1366,9 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) { } /* Edge types: explicit > mode-based > default */ - static const char *mode_calls[] = {"CALLS"}; - static const char *mode_data_flow[] = {"CALLS", "DATA_FLOWS"}; - static const char *mode_cross_svc[] = {"HTTP_CALLS", "ASYNC_CALLS", "DATA_FLOWS", "CALLS"}; - const char *edge_types[16]; int edge_type_count = 0; - - /* Try parsing explicit edge_types array from args */ - yyjson_doc *et_doc = yyjson_read(args, strlen(args), 0); - if (et_doc) { - yyjson_val *et_arr = yyjson_obj_get(yyjson_doc_get_root(et_doc), "edge_types"); - if (et_arr && yyjson_is_arr(et_arr)) { - size_t idx2; - size_t max2; - yyjson_val *val2; - yyjson_arr_foreach(et_arr, idx2, max2, val2) { - if (yyjson_is_str(val2) && edge_type_count < 16) { - edge_types[edge_type_count++] = yyjson_get_str(val2); - } - } - } - } - - yyjson_doc *et_doc_keep = et_doc; - if (edge_type_count == 0) { - /* Select defaults by mode */ - const char **defaults = mode_calls; - int n_defaults = 1; - if (mode && strcmp(mode, "data_flow") == 0) { - defaults = mode_data_flow; - n_defaults = 2; - } else if (mode && strcmp(mode, "cross_service") == 0) { - defaults = mode_cross_svc; - n_defaults = 4; - } - for (int i = 0; i < n_defaults; i++) { - edge_types[i] = defaults[i]; - } - edge_type_count = n_defaults; - if (et_doc_keep) { - yyjson_doc_free(et_doc_keep); - et_doc_keep = NULL; - } - } + yyjson_doc *et_doc_keep = resolve_trace_edge_types(args, mode, edge_types, &edge_type_count); /* Run BFS for each requested direction. * IMPORTANT: yyjson_mut_obj_add_str borrows pointers — we must keep @@ -1347,37 +1382,13 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) { if (do_outbound) { cbm_store_bfs(store, nodes[0].id, "outbound", edge_types, edge_type_count, depth, 100, &tr_out); - - yyjson_mut_val *callees = yyjson_mut_arr(doc); - for (int i = 0; i < tr_out.visited_count; i++) { - yyjson_mut_val *item = yyjson_mut_obj(doc); - yyjson_mut_obj_add_str(doc, item, "name", - tr_out.visited[i].node.name ? tr_out.visited[i].node.name : ""); - yyjson_mut_obj_add_str( - doc, item, "qualified_name", - tr_out.visited[i].node.qualified_name ? tr_out.visited[i].node.qualified_name : ""); - yyjson_mut_obj_add_int(doc, item, "hop", tr_out.visited[i].hop); - yyjson_mut_arr_add_val(callees, item); - } - yyjson_mut_obj_add_val(doc, root, "callees", callees); + yyjson_mut_obj_add_val(doc, root, "callees", bfs_to_json_array(doc, &tr_out)); } if (do_inbound) { cbm_store_bfs(store, nodes[0].id, "inbound", edge_types, edge_type_count, depth, 100, &tr_in); - - yyjson_mut_val *callers = yyjson_mut_arr(doc); - for (int i = 0; i < tr_in.visited_count; i++) { - yyjson_mut_val *item = yyjson_mut_obj(doc); - yyjson_mut_obj_add_str(doc, item, "name", - tr_in.visited[i].node.name ? tr_in.visited[i].node.name : ""); - yyjson_mut_obj_add_str( - doc, item, "qualified_name", - tr_in.visited[i].node.qualified_name ? tr_in.visited[i].node.qualified_name : ""); - yyjson_mut_obj_add_int(doc, item, "hop", tr_in.visited[i].hop); - yyjson_mut_arr_add_val(callers, item); - } - yyjson_mut_obj_add_val(doc, root, "callers", callers); + yyjson_mut_obj_add_val(doc, root, "callers", bfs_to_json_array(doc, &tr_in)); } /* Serialize BEFORE freeing traversal results (yyjson borrows strings) */ @@ -1622,8 +1633,95 @@ static char *snippet_suggestions(const char *input, cbm_node_t *nodes, int count return result; } +/* Enrich a mutable JSON object with key-value pairs from a node's properties_json. + * Returns the parsed yyjson_doc (caller frees AFTER serialization — zero-copy). */ +static yyjson_doc *enrich_node_properties(yyjson_mut_doc *doc, yyjson_mut_val *obj, + const char *properties_json) { + if (!properties_json || properties_json[0] == '\0') { + return NULL; + } + yyjson_doc *props_doc = yyjson_read(properties_json, strlen(properties_json), 0); + if (!props_doc) { + return NULL; + } + yyjson_val *props_root = yyjson_doc_get_root(props_doc); + if (!props_root || !yyjson_is_obj(props_root)) { + yyjson_doc_free(props_doc); + return NULL; + } + yyjson_obj_iter iter; + yyjson_obj_iter_init(props_root, &iter); + yyjson_val *key; + while ((key = yyjson_obj_iter_next(&iter))) { + yyjson_val *val = yyjson_obj_iter_get_val(key); + const char *k = yyjson_get_str(key); + if (!k) { + continue; + } + if (yyjson_is_str(val)) { + yyjson_mut_obj_add_str(doc, obj, k, yyjson_get_str(val)); + } else if (yyjson_is_bool(val)) { + yyjson_mut_obj_add_bool(doc, obj, k, yyjson_get_bool(val)); + } else if (yyjson_is_int(val)) { + yyjson_mut_obj_add_int(doc, obj, k, yyjson_get_int(val)); + } else if (yyjson_is_real(val)) { + yyjson_mut_obj_add_real(doc, obj, k, yyjson_get_real(val)); + } + } + return props_doc; /* caller frees after serialization */ +} + +/* Resolve an absolute path from root_path + file_path, verify containment, + * and read source lines. Sets *out_abs_path (caller frees). Returns source + * string (caller frees) or NULL if path is invalid/unreadable. */ +static char *resolve_snippet_source(const char *root_path, const char *file_path, int start, + int end, char **out_abs_path) { + *out_abs_path = NULL; + if (!root_path || !file_path) { + return NULL; + } + size_t apsz = strlen(root_path) + strlen(file_path) + 2; + char *abs_path = malloc(apsz); + snprintf(abs_path, apsz, "%s/%s", root_path, file_path); + + char real_root[4096]; + char real_file[4096]; + bool path_ok = false; +#ifdef _WIN32 + if (_fullpath(real_root, root_path, sizeof(real_root)) && + _fullpath(real_file, abs_path, sizeof(real_file))) { + cbm_normalize_path_sep(real_root); + cbm_normalize_path_sep(real_file); +#else + if (realpath(root_path, real_root) && realpath(abs_path, real_file)) { +#endif + size_t root_len = strlen(real_root); + if (strncmp(real_file, real_root, root_len) == 0 && + (real_file[root_len] == '/' || real_file[root_len] == '\0')) { + path_ok = true; + } + } + *out_abs_path = abs_path; + if (path_ok) { + return read_file_lines(abs_path, start, end); + } + return NULL; +} + /* Build an enriched snippet response for a resolved node. */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Add a string array to a JSON object (no-op if count == 0). */ +static void add_string_array(yyjson_mut_doc *doc, yyjson_mut_val *obj, const char *key, + char **strings, int count) { + if (count <= 0) { + return; + } + yyjson_mut_val *arr = yyjson_mut_arr(doc); + for (int i = 0; i < count; i++) { + yyjson_mut_arr_add_str(doc, arr, strings[i]); + } + yyjson_mut_obj_add_val(doc, obj, key, arr); +} + static char *build_snippet_response(cbm_mcp_server_t *srv, cbm_node_t *node, const char *match_method, bool include_neighbors, cbm_node_t *alternatives, int alt_count) { @@ -1631,38 +1729,8 @@ static char *build_snippet_response(cbm_mcp_server_t *srv, cbm_node_t *node, int start = node->start_line > 0 ? node->start_line : 1; int end = node->end_line > start ? node->end_line : start + SNIPPET_DEFAULT_LINES; - char *source = NULL; - - /* Build absolute path and verify it's within the project root. - * Prevents path traversal via crafted file_path (e.g., "../../.ssh/id_rsa"). */ char *abs_path = NULL; - if (root_path && node->file_path) { - size_t apsz = strlen(root_path) + strlen(node->file_path) + 2; - abs_path = malloc(apsz); - snprintf(abs_path, apsz, "%s/%s", root_path, node->file_path); - - /* Path containment: resolve symlinks/../ and verify file stays within root */ - char real_root[4096]; - char real_file[4096]; - bool path_ok = false; -#ifdef _WIN32 - if (_fullpath(real_root, root_path, sizeof(real_root)) && - _fullpath(real_file, abs_path, sizeof(real_file))) { - cbm_normalize_path_sep(real_root); - cbm_normalize_path_sep(real_file); -#else - if (realpath(root_path, real_root) && realpath(abs_path, real_file)) { -#endif - size_t root_len = strlen(real_root); - if (strncmp(real_file, real_root, root_len) == 0 && - (real_file[root_len] == '/' || real_file[root_len] == '\0')) { - path_ok = true; - } - } - if (path_ok) { - source = read_file_lines(abs_path, start, end); - } - } + char *source = resolve_snippet_source(root_path, node->file_path, start, end, &abs_path); yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); yyjson_mut_val *root_obj = yyjson_mut_obj(doc); @@ -1694,37 +1762,8 @@ static char *build_snippet_response(cbm_mcp_server_t *srv, cbm_node_t *node, yyjson_mut_obj_add_str(doc, root_obj, "match_method", match_method); } - /* Enrich with node properties. - * props_doc is freed AFTER serialization since yyjson_mut_obj_add_str - * stores pointers into it (zero-copy). */ - yyjson_doc *props_doc = NULL; - if (node->properties_json && node->properties_json[0] != '\0') { - props_doc = yyjson_read(node->properties_json, strlen(node->properties_json), 0); - if (props_doc) { - yyjson_val *props_root = yyjson_doc_get_root(props_doc); - if (props_root && yyjson_is_obj(props_root)) { - yyjson_obj_iter iter; - yyjson_obj_iter_init(props_root, &iter); - yyjson_val *key; - while ((key = yyjson_obj_iter_next(&iter))) { - yyjson_val *val = yyjson_obj_iter_get_val(key); - const char *k = yyjson_get_str(key); - if (!k) { - continue; - } - if (yyjson_is_str(val)) { - yyjson_mut_obj_add_str(doc, root_obj, k, yyjson_get_str(val)); - } else if (yyjson_is_bool(val)) { - yyjson_mut_obj_add_bool(doc, root_obj, k, yyjson_get_bool(val)); - } else if (yyjson_is_int(val)) { - yyjson_mut_obj_add_int(doc, root_obj, k, yyjson_get_int(val)); - } else if (yyjson_is_real(val)) { - yyjson_mut_obj_add_real(doc, root_obj, k, yyjson_get_real(val)); - } - } - } - } - } + /* Enrich with node properties (freed AFTER serialization — zero-copy). */ + yyjson_doc *props_doc = enrich_node_properties(doc, root_obj, node->properties_json); /* Caller/callee counts — store already resolved by calling handler */ cbm_store_t *store = srv->store; @@ -1734,8 +1773,6 @@ static char *build_snippet_response(cbm_mcp_server_t *srv, cbm_node_t *node, yyjson_mut_obj_add_int(doc, root_obj, "callers", in_deg); yyjson_mut_obj_add_int(doc, root_obj, "callees", out_deg); - /* Include neighbor names (opt-in). - * Strings stored by yyjson reference — freed after serialization. */ char **nb_callers = NULL; int nb_caller_count = 0; char **nb_callees = NULL; @@ -1743,20 +1780,8 @@ static char *build_snippet_response(cbm_mcp_server_t *srv, cbm_node_t *node, if (include_neighbors) { cbm_store_node_neighbor_names(store, node->id, 10, &nb_callers, &nb_caller_count, &nb_callees, &nb_callee_count); - if (nb_caller_count > 0) { - yyjson_mut_val *arr = yyjson_mut_arr(doc); - for (int i = 0; i < nb_caller_count; i++) { - yyjson_mut_arr_add_str(doc, arr, nb_callers[i]); - } - yyjson_mut_obj_add_val(doc, root_obj, "caller_names", arr); - } - if (nb_callee_count > 0) { - yyjson_mut_val *arr = yyjson_mut_arr(doc); - for (int i = 0; i < nb_callee_count; i++) { - yyjson_mut_arr_add_str(doc, arr, nb_callees[i]); - } - yyjson_mut_obj_add_val(doc, root_obj, "callee_names", arr); - } + add_string_array(doc, root_obj, "caller_names", nb_callers, nb_caller_count); + add_string_array(doc, root_obj, "callee_names", nb_callees, nb_callee_count); } /* Alternatives (when auto-resolved from ambiguous) */ @@ -1957,8 +1982,118 @@ static void build_grep_cmd(char *cmd, size_t cmd_sz, bool use_regex, bool scoped } } +/* Build deduplicated file list from search results + raw matches. */ +static yyjson_mut_val *build_dedup_files_array(yyjson_mut_doc *doc, search_result_t *sr, + int output_count, grep_match_t *raw, int raw_count) { + yyjson_mut_val *files_arr = yyjson_mut_arr(doc); + char *seen_files[512]; + int seen_count = 0; + for (int fi = 0; fi < output_count; fi++) { + bool dup = false; + for (int j = 0; j < seen_count; j++) { + if (strcmp(seen_files[j], sr[fi].file) == 0) { + dup = true; + break; + } + } + if (!dup && seen_count < 512) { + seen_files[seen_count++] = sr[fi].file; + yyjson_mut_arr_add_str(doc, files_arr, sr[fi].file); + } + } + for (int fi = 0; fi < raw_count && seen_count < 512; fi++) { + bool dup = false; + for (int j = 0; j < seen_count; j++) { + if (strcmp(seen_files[j], raw[fi].file) == 0) { + dup = true; + break; + } + } + if (!dup) { + seen_files[seen_count++] = raw[fi].file; + yyjson_mut_arr_add_str(doc, files_arr, raw[fi].file); + } + } + return files_arr; +} + +/* Attach source or context lines to a search result JSON item. */ +static void attach_result_source(yyjson_mut_doc *doc, yyjson_mut_val *item, search_result_t *r, + int mode, int context_lines, const char *root_path) { + enum { MODE_FULL = 1 }; + if (r->start_line <= 0 || r->end_line <= 0) { + return; + } + char abs_path[1024]; + snprintf(abs_path, sizeof(abs_path), "%s/%s", root_path, r->file); + + if (mode == MODE_FULL) { + char *source = read_file_lines(abs_path, r->start_line, r->end_line); + if (source) { + sanitize_ascii(source); + yyjson_mut_obj_add_strcpy(doc, item, "source", source); + free(source); + } + } else if (context_lines > 0 && r->match_count > 0) { + int ctx_start = r->match_lines[0] - context_lines; + int ctx_end = r->match_lines[r->match_count - 1] + context_lines; + if (ctx_start < 1) { + ctx_start = 1; + } + char *ctx = read_file_lines(abs_path, ctx_start, ctx_end); + if (ctx) { + sanitize_ascii(ctx); + yyjson_mut_obj_add_strcpy(doc, item, "context", ctx); + yyjson_mut_obj_add_int(doc, item, "context_start", ctx_start); + free(ctx); + } + } +} + +/* Build directory distribution object from search results (top-level dir → count). */ +static yyjson_mut_val *build_dir_distribution(yyjson_mut_doc *doc, search_result_t *sr, + int sr_count) { + yyjson_mut_val *dirs = yyjson_mut_obj(doc); + char dir_names[64][128]; + int dir_counts[64]; + int dir_n = 0; + for (int di = 0; di < sr_count; di++) { + char top[128] = ""; + const char *slash = strchr(sr[di].file, '/'); + if (slash) { + size_t dlen = (size_t)(slash - sr[di].file + 1); + if (dlen >= sizeof(top)) { + dlen = sizeof(top) - 1; + } + memcpy(top, sr[di].file, dlen); + top[dlen] = '\0'; + } else { + snprintf(top, sizeof(top), "%s", sr[di].file); + } + int found = -1; + for (int d = 0; d < dir_n; d++) { + if (strcmp(dir_names[d], top) == 0) { + found = d; + break; + } + } + if (found >= 0) { + dir_counts[found]++; + } else if (dir_n < 64) { + snprintf(dir_names[dir_n], sizeof(dir_names[0]), "%s", top); + dir_counts[dir_n] = 1; + dir_n++; + } + } + for (int d = 0; d < dir_n; d++) { + yyjson_mut_val *key = yyjson_mut_strcpy(doc, dir_names[d]); + yyjson_mut_val *val = yyjson_mut_int(doc, dir_counts[d]); + yyjson_mut_obj_add(dirs, key, val); + } + return dirs; +} + /* Phase 4: assemble JSON output from search results */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) static char *assemble_search_output(search_result_t *sr, int sr_count, grep_match_t *raw, int raw_count, int gm_count, int limit, int mode, int context_lines, const char *root_path) { @@ -1971,36 +2106,8 @@ static char *assemble_search_output(search_result_t *sr, int sr_count, grep_matc int output_count = sr_count < limit ? sr_count : limit; if (mode == MODE_FILES) { - yyjson_mut_val *files_arr = yyjson_mut_arr(doc); - char *seen_files[512]; - int seen_count = 0; - for (int fi = 0; fi < output_count; fi++) { - bool dup = false; - for (int j = 0; j < seen_count; j++) { - if (strcmp(seen_files[j], sr[fi].file) == 0) { - dup = true; - break; - } - } - if (!dup && seen_count < 512) { - seen_files[seen_count++] = sr[fi].file; - yyjson_mut_arr_add_str(doc, files_arr, sr[fi].file); - } - } - for (int fi = 0; fi < raw_count && seen_count < 512; fi++) { - bool dup = false; - for (int j = 0; j < seen_count; j++) { - if (strcmp(seen_files[j], raw[fi].file) == 0) { - dup = true; - break; - } - } - if (!dup) { - seen_files[seen_count++] = raw[fi].file; - yyjson_mut_arr_add_str(doc, files_arr, raw[fi].file); - } - } - yyjson_mut_obj_add_val(doc, root_obj, "files", files_arr); + yyjson_mut_obj_add_val(doc, root_obj, "files", + build_dedup_files_array(doc, sr, output_count, raw, raw_count)); } else { yyjson_mut_val *results_arr = yyjson_mut_arr(doc); for (int ri = 0; ri < output_count; ri++) { @@ -2021,36 +2128,7 @@ static char *assemble_search_output(search_result_t *sr, int sr_count, grep_matc yyjson_mut_arr_add_int(doc, ml, r->match_lines[j]); } yyjson_mut_obj_add_val(doc, item, "match_lines", ml); - - if (r->start_line > 0 && r->end_line > 0) { - char abs_path[1024]; - snprintf(abs_path, sizeof(abs_path), "%s/%s", root_path, r->file); - - if (mode == MODE_FULL) { - char *source = read_file_lines(abs_path, r->start_line, r->end_line); - if (source) { - sanitize_ascii(source); - yyjson_mut_obj_add_strcpy(doc, item, "source", source); - free(source); - } - } else if (context_lines > 0 && r->match_count > 0) { - int first_match = r->match_lines[0]; - int last_match = r->match_lines[r->match_count - 1]; - int ctx_start = first_match - context_lines; - int ctx_end = last_match + context_lines; - if (ctx_start < 1) { - ctx_start = 1; - } - char *ctx = read_file_lines(abs_path, ctx_start, ctx_end); - if (ctx) { - sanitize_ascii(ctx); - yyjson_mut_obj_add_strcpy(doc, item, "context", ctx); - yyjson_mut_obj_add_int(doc, item, "context_start", ctx_start); - free(ctx); - } - } - } - + attach_result_source(doc, item, r, mode, context_lines, root_path); yyjson_mut_arr_add_val(results_arr, item); } yyjson_mut_obj_add_val(doc, root_obj, "results", results_arr); @@ -2068,47 +2146,7 @@ static char *assemble_search_output(search_result_t *sr, int sr_count, grep_matc yyjson_mut_obj_add_val(doc, root_obj, "raw_matches", raw_arr); } - /* Directory distribution */ - { - yyjson_mut_val *dirs = yyjson_mut_obj(doc); - char dir_names[64][128]; - int dir_counts[64]; - int dir_n = 0; - for (int di = 0; di < sr_count; di++) { - char top[128] = ""; - const char *slash = strchr(sr[di].file, '/'); - if (slash) { - size_t dlen = (size_t)(slash - sr[di].file + 1); - if (dlen >= sizeof(top)) { - dlen = sizeof(top) - 1; - } - memcpy(top, sr[di].file, dlen); - top[dlen] = '\0'; - } else { - snprintf(top, sizeof(top), "%s", sr[di].file); - } - int found = -1; - for (int d = 0; d < dir_n; d++) { - if (strcmp(dir_names[d], top) == 0) { - found = d; - break; - } - } - if (found >= 0) { - dir_counts[found]++; - } else if (dir_n < 64) { - snprintf(dir_names[dir_n], sizeof(dir_names[0]), "%s", top); - dir_counts[dir_n] = 1; - dir_n++; - } - } - for (int d = 0; d < dir_n; d++) { - yyjson_mut_val *key = yyjson_mut_strcpy(doc, dir_names[d]); - yyjson_mut_val *val = yyjson_mut_int(doc, dir_counts[d]); - yyjson_mut_obj_add(dirs, key, val); - } - yyjson_mut_obj_add_val(doc, root_obj, "directories", dirs); - } + yyjson_mut_obj_add_val(doc, root_obj, "directories", build_dir_distribution(doc, sr, sr_count)); /* Summary stats */ yyjson_mut_obj_add_int(doc, root_obj, "total_grep_matches", gm_count); @@ -2131,7 +2169,252 @@ static char *assemble_search_output(search_result_t *sr, int sr_count, grep_matc return result; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity,readability-function-size) +/* Read grep output from fp, parse file:line:content format, apply path filter, + * and return a dynamically-allocated grep_match_t array. */ +/* Strip root path prefix from a file path. */ +static const char *strip_root_prefix(const char *path, const char *root, size_t root_len) { + if (strncmp(path, root, root_len) != 0) { + return path; + } + const char *p = path + root_len; + if (*p == '/') { + p++; + } + return p; +} + +static grep_match_t *collect_grep_matches(FILE *fp, const char *root_path, size_t root_len, + bool has_path_filter, cbm_regex_t *path_regex, + int grep_limit, int *out_count) { + int gm_cap = 64; + int gm_count = 0; + grep_match_t *gm = malloc(gm_cap * sizeof(grep_match_t)); + char line[2048]; + + while (fgets(line, sizeof(line), fp) && gm_count < grep_limit) { + size_t len = strlen(line); + while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) { + line[--len] = '\0'; + } + if (len == 0) { + continue; + } + + char *colon1 = strchr(line, ':'); + if (!colon1) { + continue; + } + char *colon2 = strchr(colon1 + 1, ':'); + if (!colon2) { + continue; + } + + *colon1 = '\0'; + *colon2 = '\0'; + + /* After colon1 truncation, line contains only the file path portion. */ + const char *path = line; + const char *file = strip_root_prefix(path, root_path, root_len); + + if (has_path_filter && cbm_regexec(path_regex, file, 0, NULL, 0) != CBM_REG_OK) { + continue; + } + + if (gm_count >= gm_cap) { + gm_cap *= 2; + gm = safe_realloc(gm, gm_cap * sizeof(grep_match_t)); + } + snprintf(gm[gm_count].file, sizeof(gm[0].file), "%s", file); + gm[gm_count].line = (int)strtol(colon1 + 1, NULL, 10); + snprintf(gm[gm_count].content, sizeof(gm[0].content), "%s", colon2 + 1); + sanitize_ascii(gm[gm_count].content); + gm_count++; + } + + *out_count = gm_count; + return gm; +} + +/* Find the tightest node containing a line in a file. Returns index or -1. */ +static int find_tightest_node(cbm_node_t *nodes, int count, int line) { + int best = -1; + int best_span = MAX_LINE_SPAN; + for (int j = 0; j < count; j++) { + if (nodes[j].start_line <= line && nodes[j].end_line >= line) { + int span = nodes[j].end_line - nodes[j].start_line; + if (span < best_span) { + best = j; + best_span = span; + } + } + } + return best; +} + +/* Add a grep hit to the search result set (merge into existing or create new). */ +static void add_to_search_results(search_result_t **sr, int *sr_count, int *sr_cap, cbm_node_t *n, + int line) { + for (int j = 0; j < *sr_count; j++) { + if ((*sr)[j].node_id == n->id) { + if ((*sr)[j].match_count < 64) { + (*sr)[j].match_lines[(*sr)[j].match_count++] = line; + } + return; + } + } + if (*sr_count >= *sr_cap) { + *sr_cap *= 2; + *sr = safe_realloc(*sr, *sr_cap * sizeof(search_result_t)); + memset(&(*sr)[*sr_count], 0, (*sr_cap - *sr_count) * sizeof(search_result_t)); + } + search_result_t *r = &(*sr)[*sr_count]; + r->node_id = n->id; + snprintf(r->node_name, sizeof(r->node_name), "%s", n->name ? n->name : ""); + snprintf(r->qualified_name, sizeof(r->qualified_name), "%s", + n->qualified_name ? n->qualified_name : ""); + snprintf(r->label, sizeof(r->label), "%s", n->label ? n->label : ""); + snprintf(r->file, sizeof(r->file), "%s", n->file_path ? n->file_path : ""); + r->start_line = n->start_line; + r->end_line = n->end_line; + r->match_lines[0] = line; + r->match_count = 1; + (*sr_count)++; +} + +/* Match a single grep hit to the tightest containing node, then add to sr or raw. */ +static void classify_grep_hit(grep_match_t *hit, cbm_node_t *file_nodes, int file_node_count, + search_result_t **sr, int *sr_count, int *sr_cap, grep_match_t **raw, + int *raw_count, int *raw_cap) { + int best = find_tightest_node(file_nodes, file_node_count, hit->line); + if (best >= 0) { + add_to_search_results(sr, sr_count, sr_cap, &file_nodes[best], hit->line); + } else { + if (*raw_count >= *raw_cap) { + *raw_cap = (*raw_cap == 0) ? 32 : *raw_cap * 2; + *raw = safe_realloc(*raw, *raw_cap * sizeof(grep_match_t)); + } + if (*raw) { + (*raw)[(*raw_count)++] = *hit; + } + } +} + +/* Free a file_nodes array returned from cbm_store_find_nodes_by_file. */ +static void free_file_nodes(cbm_node_t *nodes, int count) { + for (int j = 0; j < count; j++) { + free((void *)nodes[j].project); + free((void *)nodes[j].label); + free((void *)nodes[j].name); + free((void *)nodes[j].qualified_name); + free((void *)nodes[j].file_path); + free((void *)nodes[j].properties_json); + } + free(nodes); +} + +/* Classify all grep matches file-by-file into search results and raw hits. */ +static void classify_all_grep_hits(grep_match_t *gm, int gm_count, cbm_store_t *store, + const char *project, search_result_t **sr, int *sr_count, + int *sr_cap, grep_match_t **raw, int *raw_count, int *raw_cap) { + qsort(gm, gm_count, sizeof(grep_match_t), (int (*)(const void *, const void *))strcmp); + int i = 0; + while (i < gm_count) { + const char *cur_file = gm[i].file; + int file_start = i; + while (i < gm_count && strcmp(gm[i].file, cur_file) == 0) { + i++; + } + cbm_node_t *file_nodes = NULL; + int file_node_count = 0; + if (store) { + cbm_store_find_nodes_by_file(store, project, cur_file, &file_nodes, &file_node_count); + } + for (int mi = file_start; mi < i; mi++) { + classify_grep_hit(&gm[mi], file_nodes, file_node_count, sr, sr_count, sr_cap, raw, + raw_count, raw_cap); + } + free_file_nodes(file_nodes, file_node_count); + } +} + +/* Write indexed file list for scoped grep. Returns true if scoped. */ +static bool write_scoped_filelist(cbm_mcp_server_t *srv, const char *project, const char *root_path, + const char *filelist) { + cbm_store_t *pre_store = resolve_store(srv, project); + if (!pre_store) { + return false; + } + char **indexed_files = NULL; + int indexed_count = 0; + if (cbm_store_list_files(pre_store, project, &indexed_files, &indexed_count) != CBM_STORE_OK || + indexed_count == 0) { + return false; + } + FILE *fl = fopen(filelist, "w"); + bool ok = false; + if (fl) { + for (int fi = 0; fi < indexed_count; fi++) { + fprintf(fl, "%s/%s\n", root_path, indexed_files[fi]); + } + fclose(fl); + ok = true; + } + for (int fi = 0; fi < indexed_count; fi++) { + free(indexed_files[fi]); + } + free(indexed_files); + return ok; +} + +/* Parse search mode string (0=compact, 1=full, 2=files). */ +static int parse_search_mode(const char *mode_str) { + if (!mode_str) { + return 0; + } + if (strcmp(mode_str, "full") == 0) { + return 1; + } + if (strcmp(mode_str, "files") == 0) { + return 2; + } + return 0; +} + +/* Validate shell-safe arguments for search. */ +static bool validate_search_args(const char *root_path, const char *file_pattern) { + if (!cbm_validate_shell_arg(root_path)) { + return false; + } + if (file_pattern && !cbm_validate_shell_arg(file_pattern)) { + return false; + } + return true; +} + +/* Write pattern to a temp file for grep -f. Returns true on success. */ +static bool write_pattern_file(char *tmpfile, int tmpfile_sz, const char *pattern) { +#ifdef _WIN32 + snprintf(tmpfile, tmpfile_sz, "/tmp/cbm_search_%d.pat", (int)_getpid()); +#else + snprintf(tmpfile, tmpfile_sz, "/tmp/cbm_search_%d.pat", (int)getpid()); +#endif + FILE *tf = fopen(tmpfile, "w"); + if (!tf) { + return false; + } + fprintf(tf, "%s\n", pattern); + fclose(tf); + return true; +} + +/* Compile a path filter regex. Returns true if compiled successfully. */ +static bool compile_path_filter(const char *filter, cbm_regex_t *re) { + if (!filter || !filter[0]) { + return false; + } + return cbm_regcomp(re, filter, CBM_REG_EXTENDED | CBM_REG_NOSUB) == CBM_REG_OK; +} + static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) { char *pattern = cbm_mcp_get_string_arg(args, "pattern"); char *project = cbm_mcp_get_string_arg(args, "project"); @@ -2142,31 +2425,13 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) { int context_lines = cbm_mcp_get_int_arg(args, "context", 0); bool use_regex = cbm_mcp_get_bool_arg(args, "regex"); - /* Parse mode: compact (default), full, files */ - enum { MODE_COMPACT, MODE_FULL, MODE_FILES }; - int mode = MODE_COMPACT; - if (mode_str) { - if (strcmp(mode_str, "full") == 0) { - mode = MODE_FULL; - } else if (strcmp(mode_str, "files") == 0) { - mode = MODE_FILES; - } - free(mode_str); - } + int mode = parse_search_mode(mode_str); + free(mode_str); - /* Compile path_filter regex if provided */ cbm_regex_t path_regex; - bool has_path_filter = false; - if (path_filter && path_filter[0]) { - if (cbm_regcomp(&path_regex, path_filter, CBM_REG_EXTENDED | CBM_REG_NOSUB) == CBM_REG_OK) { - has_path_filter = true; - } - free(path_filter); - path_filter = NULL; - } else { - free(path_filter); - path_filter = NULL; - } + bool has_path_filter = compile_path_filter(path_filter, &path_regex); + free(path_filter); + path_filter = NULL; if (!pattern) { free(project); @@ -2195,9 +2460,7 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) { return _res; } - /* Reject shell metacharacters in user-supplied arguments */ - if (!cbm_validate_shell_arg(root_path) || - (file_pattern && !cbm_validate_shell_arg(file_pattern))) { + if (!validate_search_args(root_path, file_pattern)) { free(root_path); free(pattern); free(project); @@ -2206,23 +2469,14 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) { } /* ── Phase 1: Grep scan ──────────────────────────────────── */ - char tmpfile[256]; -#ifdef _WIN32 - snprintf(tmpfile, sizeof(tmpfile), "/tmp/cbm_search_%d.pat", (int)_getpid()); -#else - snprintf(tmpfile, sizeof(tmpfile), "/tmp/cbm_search_%d.pat", (int)getpid()); -#endif - FILE *tf = fopen(tmpfile, "w"); - if (!tf) { + if (!write_pattern_file(tmpfile, sizeof(tmpfile), pattern)) { free(root_path); free(pattern); free(project); free(file_pattern); return cbm_mcp_text_result("search failed: temp file", true); } - (void)fprintf(tf, "%s\n", pattern); - (void)fclose(tf); /* No grep-level match limit — let grep find all matches, then dedup and * cap in our code. The -m flag caused results from large vendored files @@ -2238,27 +2492,7 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) { snprintf(filelist, sizeof(filelist), "%s.files", tmpfile); bool scoped = false; - cbm_store_t *pre_store = resolve_store(srv, project); - if (pre_store) { - char **indexed_files = NULL; - int indexed_count = 0; - if (cbm_store_list_files(pre_store, project, &indexed_files, &indexed_count) == - CBM_STORE_OK && - indexed_count > 0) { - FILE *fl = fopen(filelist, "w"); - if (fl) { - for (int fi = 0; fi < indexed_count; fi++) { - fprintf(fl, "%s/%s\n", root_path, indexed_files[fi]); - } - fclose(fl); - scoped = true; - } - for (int fi = 0; fi < indexed_count; fi++) { - free(indexed_files[fi]); - } - free(indexed_files); - } - } + scoped = write_scoped_filelist(srv, project, root_path, filelist); char cmd[4096]; build_grep_cmd(cmd, sizeof(cmd), use_regex, scoped, file_pattern, tmpfile, filelist, root_path); @@ -2277,56 +2511,9 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) { } /* Collect grep matches into array */ - int gm_cap = 64; int gm_count = 0; - grep_match_t *gm = malloc(gm_cap * sizeof(grep_match_t)); - char line[2048]; - size_t root_len = strlen(root_path); - - while (fgets(line, sizeof(line), fp) && gm_count < grep_limit) { - size_t len = strlen(line); - while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) { - line[--len] = '\0'; - } - if (len == 0) { - continue; - } - - char *colon1 = strchr(line, ':'); - if (!colon1) { - continue; - } - char *colon2 = strchr(colon1 + 1, ':'); - if (!colon2) { - continue; - } - - *colon1 = '\0'; - *colon2 = '\0'; - - const char *file = line; - if (strncmp(file, root_path, root_len) == 0) { - file += root_len; - if (*file == '/') { - file++; - } - } - - /* Apply path_filter regex — skip files that don't match */ - if (has_path_filter && cbm_regexec(&path_regex, file, 0, NULL, 0) != CBM_REG_OK) { - continue; - } - - if (gm_count >= gm_cap) { - gm_cap *= 2; - gm = safe_realloc(gm, gm_cap * sizeof(grep_match_t)); - } - snprintf(gm[gm_count].file, sizeof(gm[0].file), "%s", file); - gm[gm_count].line = (int)strtol(colon1 + 1, NULL, 10); - snprintf(gm[gm_count].content, sizeof(gm[0].content), "%s", colon2 + 1); - sanitize_ascii(gm[gm_count].content); - gm_count++; - } + grep_match_t *gm = collect_grep_matches(fp, root_path, strlen(root_path), has_path_filter, + &path_regex, grep_limit, &gm_count); cbm_pclose(fp); cbm_unlink(tmpfile); if (scoped) { @@ -2350,95 +2537,8 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) { /* Sort matches by file path for contiguous per-file processing */ qsort(gm, gm_count, sizeof(grep_match_t), (int (*)(const void *, const void *))strcmp); - /* Process matches file-by-file (contiguous runs after sort) */ - int i = 0; - while (i < gm_count) { - const char *cur_file = gm[i].file; - int file_start = i; - - /* Find end of this file's run */ - while (i < gm_count && strcmp(gm[i].file, cur_file) == 0) { - i++; - } - int file_end = i; /* [file_start, file_end) */ - - /* One SQL query: load all nodes in this file */ - cbm_node_t *file_nodes = NULL; - int file_node_count = 0; - if (store) { - cbm_store_find_nodes_by_file(store, project, cur_file, &file_nodes, &file_node_count); - } - - /* Match each grep hit to tightest containing node (in-memory) */ - for (int mi = file_start; mi < file_end; mi++) { - int best = -1; - int best_span = MAX_LINE_SPAN; - for (int j = 0; j < file_node_count; j++) { - if (file_nodes[j].start_line <= gm[mi].line && - file_nodes[j].end_line >= gm[mi].line) { - int span = file_nodes[j].end_line - file_nodes[j].start_line; - if (span < best_span) { - best = j; - best_span = span; - } - } - } - - if (best >= 0) { - cbm_node_t *n = &file_nodes[best]; - - /* Dedup: check if node already in results */ - int existing = -1; - for (int j = 0; j < sr_count; j++) { - if (sr[j].node_id == n->id) { - existing = j; - break; - } - } - - if (existing >= 0) { - if (sr[existing].match_count < 64) { - sr[existing].match_lines[sr[existing].match_count++] = gm[mi].line; - } - } else { - if (sr_count >= sr_cap) { - sr_cap *= 2; - sr = safe_realloc(sr, sr_cap * sizeof(search_result_t)); - memset(&sr[sr_count], 0, (sr_cap - sr_count) * sizeof(search_result_t)); - } - search_result_t *r = &sr[sr_count]; - r->node_id = n->id; - snprintf(r->node_name, sizeof(r->node_name), "%s", n->name ? n->name : ""); - snprintf(r->qualified_name, sizeof(r->qualified_name), "%s", - n->qualified_name ? n->qualified_name : ""); - snprintf(r->label, sizeof(r->label), "%s", n->label ? n->label : ""); - snprintf(r->file, sizeof(r->file), "%s", n->file_path ? n->file_path : ""); - r->start_line = n->start_line; - r->end_line = n->end_line; - r->match_lines[0] = gm[mi].line; - r->match_count = 1; - sr_count++; - } - } else { - if (raw_count >= raw_cap) { - raw_cap *= 2; - raw = safe_realloc(raw, raw_cap * sizeof(grep_match_t)); - } - raw[raw_count++] = gm[mi]; - } - } - - /* Free file nodes */ - for (int j = 0; j < file_node_count; j++) { - free((void *)file_nodes[j].project); - free((void *)file_nodes[j].label); - free((void *)file_nodes[j].name); - free((void *)file_nodes[j].qualified_name); - free((void *)file_nodes[j].file_path); - free((void *)file_nodes[j].properties_json); - } - free(file_nodes); - } + classify_all_grep_hits(gm, gm_count, store, project, &sr, &sr_count, &sr_cap, &raw, &raw_count, + &raw_cap); /* Phase 3: batch degree query — ONE query for all results instead of 2×N */ if (store && sr_count > 0) { @@ -2601,7 +2701,57 @@ static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) { /* ── manage_adr ───────────────────────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* ADR "sections" mode: list markdown headers from file. */ +static void adr_list_sections(yyjson_mut_doc *doc, yyjson_mut_val *root_obj, const char *adr_path) { + yyjson_mut_val *sections = yyjson_mut_arr(doc); + FILE *fp = fopen(adr_path, "r"); + if (fp) { + char line[1024]; + while (fgets(line, sizeof(line), fp)) { + if (line[0] == '#') { + size_t len = strlen(line); + while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) { + line[--len] = '\0'; + } + yyjson_mut_arr_add_strcpy(doc, sections, line); + } + } + (void)fclose(fp); + } + yyjson_mut_obj_add_val(doc, root_obj, "sections", sections); +} + +/* ADR "get" mode: read content from file. Returns heap buffer (caller frees + * AFTER serialization since yyjson borrows the pointer). */ +static char *adr_read_content(yyjson_mut_doc *doc, yyjson_mut_val *root_obj, const char *adr_path) { + FILE *fp = fopen(adr_path, "r"); + if (fp) { + (void)fseek(fp, 0, SEEK_END); + long sz = ftell(fp); + if (sz < 0) { + sz = 0; + } + (void)fseek(fp, 0, SEEK_SET); + char *buf = malloc((size_t)sz + 1); + size_t n = (sz > 0) ? fread(buf, 1, (size_t)sz, fp) : 0; + buf[n] = '\0'; + (void)fclose(fp); + yyjson_mut_obj_add_str(doc, root_obj, "content", buf); + return buf; + } + yyjson_mut_obj_add_str(doc, root_obj, "content", ""); + yyjson_mut_obj_add_str(doc, root_obj, "status", "no_adr"); + yyjson_mut_obj_add_str( + doc, root_obj, "adr_hint", + "No ADR yet. Create one with manage_adr(mode='update', " + "content='## PURPOSE\\n...\\n\\n## STACK\\n...\\n\\n## ARCHITECTURE\\n..." + "\\n\\n## PATTERNS\\n...\\n\\n## TRADEOFFS\\n...\\n\\n## PHILOSOPHY\\n...'). " + "For guided creation: explore the codebase with get_architecture, " + "then draft and store. Sections: PURPOSE, STACK, ARCHITECTURE, " + "PATTERNS, TRADEOFFS, PHILOSOPHY."); + return NULL; +} + static char *handle_manage_adr(cbm_mcp_server_t *srv, const char *args) { char *project = cbm_mcp_get_string_arg(args, "project"); char *mode_str = cbm_mcp_get_string_arg(args, "mode"); @@ -2624,13 +2774,12 @@ static char *handle_manage_adr(cbm_mcp_server_t *srv, const char *args) { char adr_path[4096]; snprintf(adr_path, sizeof(adr_path), "%s/adr.md", adr_dir); - char *adr_buf = NULL; /* freed after yy_doc_to_str — yyjson holds pointer, not copy */ + char *adr_buf = NULL; yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); yyjson_mut_val *root_obj = yyjson_mut_obj(doc); yyjson_mut_doc_set_root(doc, root_obj); if (strcmp(mode_str, "update") == 0 && content) { - /* Create dir if needed */ cbm_mkdir(adr_dir); FILE *fp = fopen(adr_path, "w"); if (fp) { @@ -2641,53 +2790,14 @@ static char *handle_manage_adr(cbm_mcp_server_t *srv, const char *args) { yyjson_mut_obj_add_str(doc, root_obj, "status", "write_error"); } } else if (strcmp(mode_str, "sections") == 0) { - /* List section headers from ADR */ - FILE *fp = fopen(adr_path, "r"); - yyjson_mut_val *sections = yyjson_mut_arr(doc); - if (fp) { - char line[1024]; - while (fgets(line, sizeof(line), fp)) { - if (line[0] == '#') { - size_t len = strlen(line); - while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) { - line[--len] = '\0'; - } - yyjson_mut_arr_add_strcpy(doc, sections, line); - } - } - (void)fclose(fp); - } - yyjson_mut_obj_add_val(doc, root_obj, "sections", sections); + adr_list_sections(doc, root_obj, adr_path); } else { - /* get: read ADR content */ - FILE *fp = fopen(adr_path, "r"); - if (fp) { - (void)fseek(fp, 0, SEEK_END); - long sz = ftell(fp); - (void)fseek(fp, 0, SEEK_SET); - adr_buf = malloc(sz + 1); - size_t n = fread(adr_buf, 1, sz, fp); - adr_buf[n] = '\0'; - (void)fclose(fp); - yyjson_mut_obj_add_str(doc, root_obj, "content", adr_buf); - /* do NOT free adr_buf here: yyjson stores the pointer, not a copy */ - } else { - yyjson_mut_obj_add_str(doc, root_obj, "content", ""); - yyjson_mut_obj_add_str(doc, root_obj, "status", "no_adr"); - yyjson_mut_obj_add_str( - doc, root_obj, "adr_hint", - "No ADR yet. Create one with manage_adr(mode='update', " - "content='## PURPOSE\\n...\\n\\n## STACK\\n...\\n\\n## ARCHITECTURE\\n..." - "\\n\\n## PATTERNS\\n...\\n\\n## TRADEOFFS\\n...\\n\\n## PHILOSOPHY\\n...'). " - "For guided creation: explore the codebase with get_architecture, " - "then draft and store. Sections: PURPOSE, STACK, ARCHITECTURE, " - "PATTERNS, TRADEOFFS, PHILOSOPHY."); - } + adr_buf = adr_read_content(doc, root_obj, adr_path); } char *json = yy_doc_to_str(doc); yyjson_mut_doc_free(doc); - free(adr_buf); /* safe to free now — doc has been serialized */ + free(adr_buf); free(root_path); free(project); free(mode_str); @@ -3086,9 +3196,96 @@ char *cbm_mcp_server_handle(cbm_mcp_server_t *srv, const char *line) { return out; } +/* Handle a Content-Length-framed message (LSP-style transport). + * Reads headers, body, processes request, writes framed response. */ +static void handle_content_length_frame(cbm_mcp_server_t *srv, FILE *in, FILE *out, char **line, + size_t *cap, int content_len) { + /* Skip blank line(s) between header and body */ + while (cbm_getline(line, cap, in) > 0) { + size_t hlen = strlen(*line); + while (hlen > 0 && ((*line)[hlen - 1] == '\n' || (*line)[hlen - 1] == '\r')) { + (*line)[--hlen] = '\0'; + } + if (hlen == 0) { + break; + } + } + + char *body = malloc((size_t)content_len + 1); + if (!body) { + return; + } + size_t nread = fread(body, 1, (size_t)content_len, in); + body[nread] = '\0'; + + char *resp = cbm_mcp_server_handle(srv, body); + free(body); + + if (resp) { + size_t rlen = strlen(resp); + (void)fprintf(out, "Content-Length: %zu\r\n\r\n%s", rlen, resp); + (void)fflush(out); + free(resp); + } +} + +#ifndef _WIN32 +/* Unix 3-phase poll: non-blocking fd check, FILE* buffer peek, blocking poll. + * Returns: 1 = data ready, 0 = timeout (evicted idle stores), -1 = error/EOF. */ +static int poll_for_input_unix(cbm_mcp_server_t *srv, int fd, FILE *in) { + struct pollfd pfd = {.fd = fd, .events = POLLIN}; + int pr = poll(&pfd, 1, 0); /* Phase 1: non-blocking */ + + if (pr < 0) { + return -1; + } + if (pr > 0) { + return 1; + } + + /* Phase 2: peek FILE* buffer */ + int saved_flags = fcntl(fd, F_GETFL); + if (saved_flags < 0) { + /* fcntl failed — fall through to blocking poll */ + pr = poll(&pfd, 1, STORE_IDLE_TIMEOUT_S * 1000); + if (pr < 0) { + return -1; + } + if (pr == 0) { + cbm_mcp_server_evict_idle(srv, STORE_IDLE_TIMEOUT_S); + return 0; + } + return 1; + } + + (void)fcntl(fd, F_SETFL, saved_flags | O_NONBLOCK); + int c = fgetc(in); + (void)fcntl(fd, F_SETFL, saved_flags); + + if (c == EOF) { + if (feof(in)) { + return -1; /* true EOF */ + } + clearerr(in); + /* Phase 3: blocking poll */ + pr = poll(&pfd, 1, STORE_IDLE_TIMEOUT_S * 1000); + if (pr < 0) { + return -1; + } + if (pr == 0) { + cbm_mcp_server_evict_idle(srv, STORE_IDLE_TIMEOUT_S); + return 0; + } + return 1; + } + + (void)ungetc(c, in); + return 1; +} +#endif + /* ── Event loop ───────────────────────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) int cbm_mcp_server_run(cbm_mcp_server_t *srv, FILE *in, FILE *out) { char *line = NULL; size_t cap = 0; @@ -3125,62 +3322,12 @@ int cbm_mcp_server_run(cbm_mcp_server_t *srv, FILE *in, FILE *out) { continue; } #else - /* Phase 1: non-blocking poll — catches data already in the kernel fd - * AND handles the case where a prior getline() drained the kernel fd - * into libc's FILE* buffer (raw fd appears empty but data is buffered). - * We always try a zero-timeout poll first; if it misses buffered data, - * phase 2 below catches it via an explicit FILE* peek. */ - struct pollfd pfd = {.fd = fd, .events = POLLIN}; - int pr = poll(&pfd, 1, 0); /* non-blocking */ - + int pr = poll_for_input_unix(srv, fd, in); if (pr < 0) { - break; /* error or signal */ + break; } if (pr == 0) { - /* Raw fd appears empty. Check whether libc has already buffered - * data from a previous over-read by peeking one byte via fgetc. - * IMPORTANT: temporarily set O_NONBLOCK so fgetc() returns - * immediately when the FILE* buffer AND kernel fd are both empty - * (EAGAIN → EOF + ferror). Without this, fgetc() on a blocking fd - * would block indefinitely, preventing the Phase 3 idle eviction - * timeout from ever firing. */ - int saved_flags = fcntl(fd, F_GETFL); - if (saved_flags < 0) { - /* fcntl failed (should not happen on a valid fd) — skip the - * FILE* peek and fall straight through to the blocking poll so - * idle eviction still fires on timeout. */ - pr = poll(&pfd, 1, STORE_IDLE_TIMEOUT_S * 1000); - if (pr < 0) { - break; - } - if (pr == 0) { - cbm_mcp_server_evict_idle(srv, STORE_IDLE_TIMEOUT_S); - continue; - } - } else { - (void)fcntl(fd, F_SETFL, saved_flags | O_NONBLOCK); - int c = fgetc(in); - (void)fcntl(fd, F_SETFL, saved_flags); /* restore blocking */ - if (c == EOF) { - if (feof(in)) { - break; /* true EOF */ - } - /* No buffered data (EAGAIN from non-blocking read) — clear - * the ferror indicator set by EAGAIN, then blocking poll. */ - clearerr(in); - pr = poll(&pfd, 1, STORE_IDLE_TIMEOUT_S * 1000); - if (pr < 0) { - break; - } - if (pr == 0) { - cbm_mcp_server_evict_idle(srv, STORE_IDLE_TIMEOUT_S); - continue; - } - } else { - /* Buffered data found — push back and fall through to getline */ - (void)ungetc(c, in); - } - } + continue; /* timeout — idle stores evicted */ } #endif @@ -3197,45 +3344,11 @@ int cbm_mcp_server_run(cbm_mcp_server_t *srv, FILE *in, FILE *out) { continue; } - /* Content-Length framing support (LSP-style transport). - * Some MCP clients (OpenCode, VS Code extensions) send: - * Content-Length: \r\n\r\n - * instead of bare JSONL. Detect the header, read the payload, - * and respond with the same framing. */ + /* Content-Length framing (LSP-style transport) */ if (strncmp(line, "Content-Length:", 15) == 0) { int content_len = (int)strtol(line + 15, NULL, 10); - if (content_len <= 0 || content_len > 10 * 1024 * 1024) { - continue; /* invalid or too large */ - } - - /* Skip blank line(s) between header and body */ - while (cbm_getline(&line, &cap, in) > 0) { - size_t hlen = strlen(line); - while (hlen > 0 && (line[hlen - 1] == '\n' || line[hlen - 1] == '\r')) { - line[--hlen] = '\0'; - } - if (hlen == 0) { - break; /* found the blank separator */ - } - /* Skip other headers (e.g. Content-Type) */ - } - - /* Read exact content_len bytes */ - char *body = malloc((size_t)content_len + 1); - if (!body) { - continue; - } - size_t nread = fread(body, 1, (size_t)content_len, in); - body[nread] = '\0'; - - char *resp = cbm_mcp_server_handle(srv, body); - free(body); - - if (resp) { - size_t rlen = strlen(resp); - (void)fprintf(out, "Content-Length: %zu\r\n\r\n%s", rlen, resp); - (void)fflush(out); - free(resp); + if (content_len > 0 && content_len <= 10 * 1024 * 1024) { + handle_content_length_frame(srv, in, out, &line, &cap, content_len); } continue; } diff --git a/src/pipeline/fqn.c b/src/pipeline/fqn.c index 2a322b38e..bd0663767 100644 --- a/src/pipeline/fqn.c +++ b/src/pipeline/fqn.c @@ -47,70 +47,70 @@ static char *join_segments(const char **segments, int count) { return result; } +/* Strip file extension from the last path component. */ +static void strip_file_extension(char *path) { + char *last_slash = strrchr(path, '/'); + char *start = last_slash ? last_slash + 1 : path; + char *ext = strrchr(start, '.'); + if (ext) { + *ext = '\0'; + } +} + +/* Tokenize path by '/' into segments array. Returns number of segments added. */ +static int tokenize_path(char *path, const char **segments, int max_segs) { + int count = 0; + if (path[0] == '\0') { + return 0; + } + char *tok = path; + while (tok && *tok && count < max_segs) { + char *slash = strchr(tok, '/'); + if (slash) { + *slash = '\0'; + } + if (tok[0] != '\0') { + segments[count++] = tok; + } + tok = slash ? slash + 1 : NULL; + } + return count; +} + +/* Strip __init__ (Python) / index (JS/TS) from the last segment when a + * symbol name is provided. Keeps it when no name is given to avoid QN + * collision with Folder nodes for the same directory. */ +static void strip_init_or_index(const char **segments, int *seg_count, const char *name) { + if (*seg_count <= 1) { + return; + } + const char *last = segments[*seg_count - 1]; + if (strcmp(last, "__init__") != 0 && strcmp(last, "index") != 0) { + return; + } + if (name && name[0] != '\0') { + (*seg_count)--; + } +} + /* ── Public API ──────────────────────────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) char *cbm_pipeline_fqn_compute(const char *project, const char *rel_path, const char *name) { if (!project) { return strdup(""); } - /* Work on a mutable copy for path manipulation */ char *path = strdup(rel_path ? rel_path : ""); - - /* Normalize path separators */ cbm_normalize_path_sep(path); + strip_file_extension(path); - /* Strip file extension */ - { - char *last_slash = strrchr(path, '/'); - char *start = last_slash ? last_slash + 1 : path; - char *ext = strrchr(start, '.'); - if (ext) { - *ext = '\0'; - } - } - - /* Split by '/' into segments */ const char *segments[256]; int seg_count = 0; - - /* First segment is always the project */ segments[seg_count++] = project; + seg_count += tokenize_path(path, segments + seg_count, FQN_MAX_PATH_SEGS); - /* Add path segments */ - if (path[0] != '\0') { - char *tok = path; - while (tok && *tok && seg_count < FQN_MAX_PATH_SEGS) { - char *slash = strchr(tok, '/'); - if (slash) { - *slash = '\0'; - } - if (tok[0] != '\0') { - segments[seg_count++] = tok; - } - tok = slash ? slash + 1 : NULL; - } - } + strip_init_or_index(segments, &seg_count, name); - /* Handle __init__ (Python) and index (JS/TS): - * Strip from module QN to get the package name, BUT only when a name - * suffix is provided (e.g., fqn_compute("proj", "pkg/__init__.py", "MyClass") - * → "proj.pkg.MyClass"). When no name is given (fqn_module for the file - * itself), keep "__init__" to avoid QN collision with the Folder node - * for the same directory. */ - if (seg_count > 1) { - const char *last = segments[seg_count - 1]; - if (strcmp(last, "__init__") == 0 || strcmp(last, "index") == 0) { - if (name && name[0] != '\0') { - /* Has a symbol name — strip __init__ so symbols get clean package QN */ - seg_count--; - } - /* else: no name → keep __init__/index as disambiguator */ - } - } - - /* Add name if provided */ if (name && name[0] != '\0') { segments[seg_count++] = name; } diff --git a/src/pipeline/httplink.c b/src/pipeline/httplink.c index 9fd5c094b..63826f7f3 100644 --- a/src/pipeline/httplink.c +++ b/src/pipeline/httplink.c @@ -177,7 +177,50 @@ const char *cbm_confidence_band(double score) { /* ── Path matching ─────────────────────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Strip URL scheme + host, returning pointer to path portion. + * Returns NULL if URL has no path component. */ +static char *strip_scheme_host(char *work) { + char *path_start = work; + char *scheme = strstr(work, "://"); + if (scheme) { + path_start = scheme + 3; + while (*path_start && *path_start != '/') { + path_start++; + } + if (!*path_start) { + return NULL; + } + } + return path_start; +} + +/* Replace :param and {param} path segments with wildcard '*'. */ +static int replace_path_params(const char *path_start, int plen, char *out, int out_max) { + int out_len = 0; + for (int i = 0; i < plen && out_len < out_max;) { + if (path_start[i] == ':' && (i == 0 || path_start[i - 1] == '/')) { + out[out_len++] = '*'; + i++; + while (i < plen && path_start[i] != '/') { + i++; + } + } else if (path_start[i] == '{') { + out[out_len++] = '*'; + i++; + while (i < plen && path_start[i] != '}') { + i++; + } + if (i < plen) { + i++; + } + } else { + out[out_len++] = path_start[i++]; + } + } + out[out_len] = '\0'; + return out_len; +} + const char *cbm_normalize_path(const char *input) { static CBM_TLS char buf[1024]; if (!input || !*input) { @@ -193,19 +236,10 @@ const char *cbm_normalize_path(const char *input) { } work[len] = '\0'; - /* Strip URL scheme + host: https://host/path → /path */ - char *path_start = work; - char *scheme = strstr(work, "://"); - if (scheme) { - path_start = scheme + 3; - /* Skip host */ - while (*path_start && *path_start != '/') { - path_start++; - } - if (!*path_start) { - buf[0] = '\0'; - return buf; - } + char *path_start = strip_scheme_host(work); + if (!path_start) { + buf[0] = '\0'; + return buf; } /* Strip trailing slash */ @@ -214,29 +248,7 @@ const char *cbm_normalize_path(const char *input) { path_start[--plen] = '\0'; } - /* Replace :param and {param} with * */ - int out_len = 0; - for (int i = 0; i < plen && out_len < PARAM_BUF_GUARD;) { - if (path_start[i] == ':' && (i == 0 || path_start[i - 1] == '/')) { - buf[out_len++] = '*'; - i++; - while (i < plen && path_start[i] != '/') { - i++; - } - } else if (path_start[i] == '{') { - buf[out_len++] = '*'; - i++; - while (i < plen && path_start[i] != '}') { - i++; - } - if (i < plen) { - i++; - } - } else { - buf[out_len++] = path_start[i++]; - } - } - buf[out_len] = '\0'; + replace_path_params(path_start, plen, buf, PARAM_BUF_GUARD); if (strcmp(buf, "/") == 0) { buf[0] = '\0'; @@ -256,7 +268,20 @@ static void normalize_to_buf(const char *input, char *out, int out_size) { out[len] = '\0'; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Compare two segment arrays with wildcard matching. + * a_segs[a_off..a_off+count-1] vs b_segs[0..count-1]. */ +static bool segments_match_wildcard(char **a_segs, int a_off, char **b_segs, int count) { + for (int i = 0; i < count; i++) { + if (strcmp(a_segs[a_off + i], "*") == 0 || strcmp(b_segs[i], "*") == 0) { + continue; + } + if (strcmp(a_segs[a_off + i], b_segs[i]) != 0) { + return false; + } + } + return true; +} + bool cbm_paths_match(const char *call_path, const char *route_path) { char norm_call[1024]; char norm_route[1024]; @@ -267,29 +292,21 @@ bool cbm_paths_match(const char *call_path, const char *route_path) { return false; } - /* Exact match */ if (strcmp(norm_call, norm_route) == 0) { return true; } - /* Suffix match: normalized call ends with normalized route */ int lc = (int)strlen(norm_call); int lr = (int)strlen(norm_route); if (lc > lr && strcmp(norm_call + lc - lr, norm_route) == 0) { return true; } - /* Segment-by-segment comparison with wildcard matching */ - /* Split both into segments */ char call_copy[1024]; char route_copy[1024]; strncpy(call_copy, norm_call, sizeof(call_copy) - 1); call_copy[sizeof(call_copy) - 1] = '\0'; - strncpy(route_copy, norm_route, sizeof(route_copy) - 1); - route_copy[sizeof(route_copy) - 1] = '\0'; - /* For suffix matching with segments: try matching from the end. - * But first try direct segment comparison. */ char *call_segs[64]; char *route_segs[64]; int nc = 0; @@ -301,7 +318,6 @@ bool cbm_paths_match(const char *call_path, const char *route_path) { tok = strtok(NULL, "/"); } - /* Need to re-copy route since strtok consumed call_copy's context */ normalize_to_buf(route_path, route_copy, sizeof(route_copy)); tok = strtok(route_copy, "/"); while (tok && nr < 64) { @@ -309,38 +325,12 @@ bool cbm_paths_match(const char *call_path, const char *route_path) { tok = strtok(NULL, "/"); } - if (nc == nr) { - bool match = true; - for (int i = 0; i < nc; i++) { - if (strcmp(call_segs[i], "*") == 0 || strcmp(route_segs[i], "*") == 0) { - continue; /* wildcard matches anything */ - } - if (strcmp(call_segs[i], route_segs[i]) != 0) { - match = false; - break; - } - } - if (match) { - return true; - } + if (nc == nr && segments_match_wildcard(call_segs, 0, route_segs, nc)) { + return true; } - /* Suffix match at segment level */ - if (nc > nr) { - int offset = nc - nr; - bool match = true; - for (int i = 0; i < nr; i++) { - if (strcmp(call_segs[offset + i], "*") == 0 || strcmp(route_segs[i], "*") == 0) { - continue; - } - if (strcmp(call_segs[offset + i], route_segs[i]) != 0) { - match = false; - break; - } - } - if (match) { - return true; - } + if (nc > nr && segments_match_wildcard(call_segs, nc - nr, route_segs, nr)) { + return true; } return false; @@ -405,7 +395,53 @@ static double segment_jaccard(const char *norm_call, const char *norm_route) { return (double)intersect / (double)max_len; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Determine match type between two normalized paths. + * Returns base score (CONF_PATH_EXACT, CONF_PATH_SUFFIX, or 0.0). + * Sets *is_suffix if it was a suffix match. */ +static double classify_path_match(const char *norm_call, const char *norm_route, bool *is_suffix) { + *is_suffix = false; + + if (strcmp(norm_call, norm_route) == 0) { + return CONF_PATH_EXACT; + } + + int lc = (int)strlen(norm_call); + int lr = (int)strlen(norm_route); + if (lc > lr && strcmp(norm_call + lc - lr, norm_route) == 0) { + *is_suffix = true; + return CONF_PATH_SUFFIX; + } + + /* Segment-wise match with wildcards */ + char c2[1024]; + char r2[1024]; + strncpy(c2, norm_call, sizeof(c2) - 1); + c2[sizeof(c2) - 1] = '\0'; + strncpy(r2, norm_route, sizeof(r2) - 1); + r2[sizeof(r2) - 1] = '\0'; + + char *cs[64]; + char *rs[64]; + int nc2 = 0; + int nr2 = 0; + char *tk = strtok(c2, "/"); + while (tk && nc2 < 64) { + cs[nc2++] = tk; + tk = strtok(NULL, "/"); + } + tk = strtok(r2, "/"); + while (tk && nr2 < 64) { + rs[nr2++] = tk; + tk = strtok(NULL, "/"); + } + + if (nc2 == nr2 && segments_match_wildcard(cs, 0, rs, nc2)) { + return CONF_PATH_EXACT; + } + + return 0.0; +} + double cbm_path_match_score(const char *call_path, const char *route_path) { char norm_call[1024]; char norm_route[1024]; @@ -416,65 +452,12 @@ double cbm_path_match_score(const char *call_path, const char *route_path) { return 0.0; } - /* Determine match type and base score */ - double match_base = 0.0; bool is_suffix = false; - - if (strcmp(norm_call, norm_route) == 0) { - match_base = CONF_PATH_EXACT; - } else { - int lc = (int)strlen(norm_call); - int lr = (int)strlen(norm_route); - - /* Check suffix match */ - if (lc > lr && strcmp(norm_call + lc - lr, norm_route) == 0) { - match_base = CONF_PATH_SUFFIX; - is_suffix = true; - } else { - /* Segment-wise match with wildcards */ - char c2[1024]; - char r2[1024]; - strncpy(c2, norm_call, sizeof(c2) - 1); - c2[sizeof(c2) - 1] = '\0'; - strncpy(r2, norm_route, sizeof(r2) - 1); - r2[sizeof(r2) - 1] = '\0'; - - char *cs[64]; - char *rs[64]; - int nc2 = 0; - int nr2 = 0; - char *tk = strtok(c2, "/"); - while (tk && nc2 < 64) { - cs[nc2++] = tk; - tk = strtok(NULL, "/"); - } - tk = strtok(r2, "/"); - while (tk && nr2 < 64) { - rs[nr2++] = tk; - tk = strtok(NULL, "/"); - } - - if (nc2 == nr2) { - bool all_match = true; - for (int i = 0; i < nc2; i++) { - if (strcmp(cs[i], "*") != 0 && strcmp(rs[i], "*") != 0 && - strcmp(cs[i], rs[i]) != 0) { - all_match = false; - break; - } - } - if (all_match) { - match_base = CONF_PATH_EXACT; - } - } - } - } - + double match_base = classify_path_match(norm_call, norm_route, &is_suffix); if (match_base == 0.0) { return 0.0; } - /* Compute confidence: 0.5 × jaccard + 0.5 × depthFactor */ double jaccard = segment_jaccard(norm_call, norm_route); int depth = count_segments(is_suffix ? norm_route : norm_call); double depth_factor = (double)depth / DEPTH_DIVISOR; @@ -754,7 +737,37 @@ int cbm_extract_json_string_paths(const char *text, char **out, int max_out) { /* ── Route extraction: Python ──────────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Fill common route fields (function_name, qualified_name). */ +static void fill_route_identity(cbm_route_handler_t *r, const char *name, const char *qn) { + strncpy(r->function_name, name ? name : "", sizeof(r->function_name) - 1); + strncpy(r->qualified_name, qn ? qn : "", sizeof(r->qualified_name) - 1); +} + +/* Copy a regex match group into a buffer, uppercase it, return length. */ +static int copy_match_upper(char *dst, int dst_size, const char *src, int so, int eo) { + int len = eo - so; + if (len >= dst_size) { + len = dst_size - 1; + } + memcpy(dst, src + so, (size_t)len); + dst[len] = '\0'; + for (int j = 0; dst[j]; j++) { + dst[j] = (char)toupper((unsigned char)dst[j]); + } + return len; +} + +/* Copy a regex match group into a buffer, return length. */ +static int copy_match(char *dst, int dst_size, const char *src, int so, int eo) { + int len = eo - so; + if (len >= dst_size) { + len = dst_size - 1; + } + memcpy(dst, src + so, (size_t)len); + dst[len] = '\0'; + return len; +} + int cbm_extract_python_routes(const char *name, const char *qn, const char **decorators, int ndec, cbm_route_handler_t *out, int max_out) { if (!decorators || ndec <= 0) { @@ -778,53 +791,26 @@ int cbm_extract_python_routes(const char *name, const char *qn, const char **dec for (int i = 0; i < ndec && count < max_out; i++) { cbm_regmatch_t match[3]; - /* Try websocket first */ if (cbm_regexec(&py_ws_re, decorators[i], 2, match, 0) == 0) { cbm_route_handler_t *r = &out[count]; memset(r, 0, sizeof(*r)); - - int plen = (match[1].rm_eo - match[1].rm_so); - if (plen >= (int)sizeof(r->path)) { - plen = (int)sizeof(r->path) - 1; - } - memcpy(r->path, decorators[i] + match[1].rm_so, (size_t)plen); - r->path[plen] = '\0'; - + copy_match(r->path, (int)sizeof(r->path), decorators[i], match[1].rm_so, + match[1].rm_eo); strncpy(r->method, "WS", sizeof(r->method) - 1); strncpy(r->protocol, "ws", sizeof(r->protocol) - 1); - strncpy(r->function_name, name ? name : "", sizeof(r->function_name) - 1); - strncpy(r->qualified_name, qn ? qn : "", sizeof(r->qualified_name) - 1); + fill_route_identity(r, name, qn); count++; continue; } - /* Try regular HTTP route */ if (cbm_regexec(&py_route_re, decorators[i], 3, match, 0) == 0) { cbm_route_handler_t *r = &out[count]; memset(r, 0, sizeof(*r)); - - /* Method */ - int mlen = (match[1].rm_eo - match[1].rm_so); - if (mlen >= (int)sizeof(r->method)) { - mlen = (int)sizeof(r->method) - 1; - } - memcpy(r->method, decorators[i] + match[1].rm_so, (size_t)mlen); - r->method[mlen] = '\0'; - /* Uppercase */ - for (int j = 0; r->method[j]; j++) { - r->method[j] = (char)toupper((unsigned char)r->method[j]); - } - - /* Path */ - int plen = (match[2].rm_eo - match[2].rm_so); - if (plen >= (int)sizeof(r->path)) { - plen = (int)sizeof(r->path) - 1; - } - memcpy(r->path, decorators[i] + match[2].rm_so, (size_t)plen); - r->path[plen] = '\0'; - - strncpy(r->function_name, name ? name : "", sizeof(r->function_name) - 1); - strncpy(r->qualified_name, qn ? qn : "", sizeof(r->qualified_name) - 1); + copy_match_upper(r->method, (int)sizeof(r->method), decorators[i], match[1].rm_so, + match[1].rm_eo); + copy_match(r->path, (int)sizeof(r->path), decorators[i], match[2].rm_so, + match[2].rm_eo); + fill_route_identity(r, name, qn); count++; } } @@ -836,7 +822,226 @@ int cbm_extract_python_routes(const char *name, const char *qn, const char **dec /* ── Route extraction: Go gin/chi ──────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Gin Group() binding: var → prefix. */ +typedef struct { + char var[64]; + char prefix[256]; +} go_gin_group_t; + +/* Chi Route() match position + prefix. */ +typedef struct { + int end_pos; + char prefix[256]; +} go_chi_match_t; + +/* Route regex match with byte positions. */ +typedef struct { + int start_pos, end_pos; + int method_so, method_eo; + int path_so, path_eo; + int handler_so, handler_eo; +} go_route_match_t; + +/* Collect gin Group() bindings from source. */ +static int collect_gin_groups(const char *source, const cbm_regex_t *re, go_gin_group_t *out, + int max) { + int count = 0; + const char *sp = source; + cbm_regmatch_t gm[4]; + while (cbm_regexec(re, sp, 4, gm, 0) == 0 && count < max) { + int vlen = (gm[1].rm_eo - gm[1].rm_so); + int plen = (gm[3].rm_eo - gm[3].rm_so); + if (vlen < 64 && plen < 256) { + memcpy(out[count].var, sp + gm[1].rm_so, (size_t)vlen); + out[count].var[vlen] = '\0'; + memcpy(out[count].prefix, sp + gm[3].rm_so, (size_t)plen); + out[count].prefix[plen] = '\0'; + count++; + } + sp += gm[0].rm_eo; + } + return count; +} + +/* Collect chi Route() matches with absolute byte positions. */ +static int collect_chi_matches(const char *source, const cbm_regex_t *re, go_chi_match_t *out, + int max) { + int count = 0; + const char *sp = source; + cbm_regmatch_t cm[2]; + while (cbm_regexec(re, sp, 2, cm, 0) == 0 && count < max) { + int abs_end = (int)((sp - source) + cm[0].rm_eo); + int plen = (cm[1].rm_eo - cm[1].rm_so); + if (plen < 256) { + out[count].end_pos = abs_end; + memcpy(out[count].prefix, sp + cm[1].rm_so, (size_t)plen); + out[count].prefix[plen] = '\0'; + count++; + } + sp += cm[0].rm_eo; + } + return count; +} + +/* Collect route regex matches with absolute byte positions. */ +static int collect_route_matches(const char *source, const cbm_regex_t *re, go_route_match_t *out, + int max) { + int count = 0; + const char *sp = source; + cbm_regmatch_t rm[4]; + while (cbm_regexec(re, sp, 4, rm, 0) == 0 && count < max) { + int base = (int)(sp - source); + out[count].start_pos = (base + rm[0].rm_so); + out[count].end_pos = (base + rm[0].rm_eo); + out[count].method_so = (base + rm[1].rm_so); + out[count].method_eo = (base + rm[1].rm_eo); + out[count].path_so = (base + rm[2].rm_so); + out[count].path_eo = (base + rm[2].rm_eo); + out[count].handler_so = (rm[3].rm_so >= 0) ? (base + rm[3].rm_so) : -1; + out[count].handler_eo = (rm[3].rm_eo >= 0) ? (base + rm[3].rm_eo) : -1; + count++; + sp += rm[0].rm_eo; + } + return count; +} + +/* Chi prefix stack entry. */ +typedef struct { + char prefix[256]; + int depth; +} chi_stack_entry_t; + +/* Concatenate all active chi prefixes into a route prefix. */ +static void build_chi_prefix(const chi_stack_entry_t *stack, int top, char *out, int out_sz) { + out[0] = '\0'; + for (int s = 0; s < top; s++) { + int cur = (int)strlen(out); + int pf = (int)strlen(stack[s].prefix); + if (cur + pf < out_sz - 1) { + strcat(out, stack[s].prefix); + } + } +} + +/* Handle brace tracking for chi prefix computation. */ +static void chi_handle_brace(char c, chi_stack_entry_t *stack, int *top, int *brace_depth, + bool *pending, const char *pending_prefix) { + if (c == '{') { + (*brace_depth)++; + if (*pending && *top < 32) { + strncpy(stack[*top].prefix, pending_prefix, sizeof(stack[*top].prefix) - 1); + stack[*top].prefix[sizeof(stack[*top].prefix) - 1] = '\0'; + stack[*top].depth = *brace_depth; + (*top)++; + *pending = false; + } + } else if (c == '}') { + while (*top > 0 && stack[*top - 1].depth == *brace_depth) { + (*top)--; + } + (*brace_depth)--; + } +} + +/* Compute chi prefix for each route via brace-depth tracking. */ +static void compute_chi_prefixes(const char *source, const go_chi_match_t *chi_matches, int nchi, + const go_route_match_t *route_matches, int nmatches, + char (*route_chi_prefix)[512]) { + if (nchi == 0 || nmatches == 0) { + return; + } + + chi_stack_entry_t chi_stack[32]; + int chi_top = 0; + int brace_depth = 0; + int next_chi = 0; + int next_route = 0; + bool pending = false; + char pending_prefix[256] = ""; + + int src_len = (int)strlen(source); + for (int i = 0; i <= src_len && next_route < nmatches; i++) { + while (next_chi < nchi && i >= chi_matches[next_chi].end_pos) { + pending = true; + strncpy(pending_prefix, chi_matches[next_chi].prefix, sizeof(pending_prefix) - 1); + pending_prefix[sizeof(pending_prefix) - 1] = '\0'; + next_chi++; + } + + if (i < src_len) { + chi_handle_brace(source[i], chi_stack, &chi_top, &brace_depth, &pending, + pending_prefix); + } + + while (next_route < nmatches && i == route_matches[next_route].start_pos) { + build_chi_prefix(chi_stack, chi_top, route_chi_prefix[next_route], 512); + next_route++; + } + } +} + +/* Extract handler ref from match, trimming whitespace and trailing chars. */ +static void extract_handler_ref(const char *source, const go_route_match_t *rm, + cbm_route_handler_t *r) { + if (rm->handler_so < 0) { + return; + } + int hlen = rm->handler_eo - rm->handler_so; + const char *hs = source + rm->handler_so; + while (hlen > 0 && (hs[0] == ' ' || hs[0] == '\t')) { + hs++; + hlen--; + } + while (hlen > 0 && (hs[hlen - 1] == ' ' || hs[hlen - 1] == '\t' || hs[hlen - 1] == ')' || + hs[hlen - 1] == '\n')) { + hlen--; + } + if (hlen > 0 && hlen < (int)sizeof(r->handler_ref)) { + memcpy(r->handler_ref, hs, (size_t)hlen); + r->handler_ref[hlen] = '\0'; + } +} + +/* Find receiver variable name before the dot in ".METHOD(" at start_pos. */ +static void find_receiver(const char *source, int start_pos, char *receiver, int max_len) { + receiver[0] = '\0'; + const char *dot_pos = source + start_pos; + if (dot_pos <= source) { + return; + } + const char *rp = dot_pos - 1; + while (rp >= source && (isalnum((unsigned char)*rp) || *rp == '_')) { + rp--; + } + rp++; + int rlen = (int)(dot_pos - rp); + if (rlen > 0 && rlen < max_len) { + memcpy(receiver, rp, (size_t)rlen); + receiver[rlen] = '\0'; + } +} + +/* Apply gin group or chi prefix to a route path. */ +static void apply_go_route_prefix(cbm_route_handler_t *r, const char *receiver, + const go_gin_group_t *gin_groups, int ngin, + const char *chi_prefix) { + for (int g = 0; g < ngin; g++) { + if (strcmp(receiver, gin_groups[g].var) == 0) { + char full_path[512]; + snprintf(full_path, sizeof(full_path), "%s%s", gin_groups[g].prefix, r->path); + strncpy(r->path, full_path, sizeof(r->path) - 1); + r->path[sizeof(r->path) - 1] = '\0'; + return; + } + } + if (chi_prefix && chi_prefix[0]) { + char full_path[512]; + snprintf(full_path, sizeof(full_path), "%s%s", chi_prefix, r->path); + strncpy(r->path, full_path, sizeof(r->path) - 1); + r->path[sizeof(r->path) - 1] = '\0'; + } +} + int cbm_extract_go_routes(const char *name, const char *qn, const char *source, cbm_route_handler_t *out, int max_out) { if (!source || !*source) { @@ -854,7 +1059,6 @@ int cbm_extract_go_routes(const char *name, const char *qn, const char *source, CBM_REG_EXTENDED) != 0) { return 0; } - if (cbm_regcomp( &go_group_re, "([a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*(:=|=)[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*\\." @@ -863,7 +1067,6 @@ int cbm_extract_go_routes(const char *name, const char *qn, const char *source, cbm_regfree(&go_route_re); return 0; } - if (cbm_regcomp(&go_chi_re, "\\.Route\\([[:space:]]*\"([^\"]+)\"[[:space:]]*,[[:space:]]*func", CBM_REG_EXTENDED) != 0) { cbm_regfree(&go_route_re); @@ -871,226 +1074,35 @@ int cbm_extract_go_routes(const char *name, const char *qn, const char *source, return 0; } - /* ── Step 1: Collect gin Group() bindings (var → prefix) ──── */ - typedef struct { - char var[64]; - char prefix[256]; - } gin_group_t; - gin_group_t gin_groups[32]; - int ngin = 0; + go_gin_group_t gin_groups[32]; + int ngin = collect_gin_groups(source, &go_group_re, gin_groups, 32); - const char *sp = source; - cbm_regmatch_t gm[4]; - while (cbm_regexec(&go_group_re, sp, 4, gm, 0) == 0 && ngin < 32) { - int vlen = (gm[1].rm_eo - gm[1].rm_so); - int plen = (gm[3].rm_eo - gm[3].rm_so); - if (vlen < 64 && plen < 256) { - memcpy(gin_groups[ngin].var, sp + gm[1].rm_so, (size_t)vlen); - gin_groups[ngin].var[vlen] = '\0'; - memcpy(gin_groups[ngin].prefix, sp + gm[3].rm_so, (size_t)plen); - gin_groups[ngin].prefix[plen] = '\0'; - ngin++; - } - sp += gm[0].rm_eo; - } + go_chi_match_t chi_matches[32]; + int nchi = collect_chi_matches(source, &go_chi_re, chi_matches, 32); - /* ── Step 2: Collect chi Route() matches with byte positions ── */ - typedef struct { - int end_pos; - char prefix[256]; - } chi_match_t; - chi_match_t chi_matches[32]; - int nchi = 0; + go_route_match_t route_matches[64]; + int nmatches = collect_route_matches(source, &go_route_re, route_matches, 64); - sp = source; - cbm_regmatch_t cm[2]; - while (cbm_regexec(&go_chi_re, sp, 2, cm, 0) == 0 && nchi < 32) { - int abs_end = (int)((sp - source) + cm[0].rm_eo); - int plen = (cm[1].rm_eo - cm[1].rm_so); - if (plen < 256) { - chi_matches[nchi].end_pos = abs_end; - memcpy(chi_matches[nchi].prefix, sp + cm[1].rm_so, (size_t)plen); - chi_matches[nchi].prefix[plen] = '\0'; - nchi++; - } - sp += cm[0].rm_eo; - } - - /* ── Step 3: Collect route matches with byte positions ─────── */ - typedef struct { - int start_pos, end_pos; - int method_so, method_eo; - int path_so, path_eo; - int handler_so, handler_eo; /* capture group 3: handler ref */ - } route_match_t; - route_match_t route_matches[64]; - int nmatches = 0; - - sp = source; - cbm_regmatch_t rm[4]; - while (cbm_regexec(&go_route_re, sp, 4, rm, 0) == 0 && nmatches < 64) { - int base = (int)(sp - source); - route_matches[nmatches].start_pos = (base + rm[0].rm_so); - route_matches[nmatches].end_pos = (base + rm[0].rm_eo); - route_matches[nmatches].method_so = (base + rm[1].rm_so); - route_matches[nmatches].method_eo = (base + rm[1].rm_eo); - route_matches[nmatches].path_so = (base + rm[2].rm_so); - route_matches[nmatches].path_eo = (base + rm[2].rm_eo); - route_matches[nmatches].handler_so = (rm[3].rm_so >= 0) ? (base + rm[3].rm_so) : -1; - route_matches[nmatches].handler_eo = (rm[3].rm_eo >= 0) ? (base + rm[3].rm_eo) : -1; - nmatches++; - sp += rm[0].rm_eo; - } - - /* ── Step 4: Compute chi prefix for each route via brace tracking ── */ char route_chi_prefix[64][512]; memset(route_chi_prefix, 0, sizeof(route_chi_prefix)); + compute_chi_prefixes(source, chi_matches, nchi, route_matches, nmatches, route_chi_prefix); - if (nchi > 0 && nmatches > 0) { - /* Single pass through source tracking brace depth + chi prefix stack. - * When we pass a chi Route() match end, mark it pending. - * When we see '{', if pending, push onto stack at current depth. - * When we see '}', pop stack entries at current depth, then decrement. - * At each route position, concat stack entries for the prefix. */ - - struct { - char prefix[256]; - int depth; - } chi_stack[32]; - int chi_top = 0; - int brace_depth = 0; - int next_chi = 0; - int next_route = 0; - bool pending = false; - char pending_prefix[256] = ""; - - int src_len = (int)strlen(source); - for (int i = 0; i <= src_len && next_route < nmatches; i++) { - /* Check if we've passed a chi Route() match end */ - while (next_chi < nchi && i >= chi_matches[next_chi].end_pos) { - pending = true; - strncpy(pending_prefix, chi_matches[next_chi].prefix, sizeof(pending_prefix) - 1); - pending_prefix[sizeof(pending_prefix) - 1] = '\0'; - next_chi++; - } - - if (i < src_len && source[i] == '{') { - brace_depth++; - if (pending && chi_top < 32) { - strncpy(chi_stack[chi_top].prefix, pending_prefix, - sizeof(chi_stack[chi_top].prefix) - 1); - chi_stack[chi_top].prefix[sizeof(chi_stack[chi_top].prefix) - 1] = '\0'; - chi_stack[chi_top].depth = brace_depth; - chi_top++; - pending = false; - } - } else if (i < src_len && source[i] == '}') { - while (chi_top > 0 && chi_stack[chi_top - 1].depth == brace_depth) { - chi_top--; - } - brace_depth--; - } - - /* Check if this position is a route match start */ - while (next_route < nmatches && i == route_matches[next_route].start_pos) { - /* Build prefix from chi stack */ - route_chi_prefix[next_route][0] = '\0'; - for (int s = 0; s < chi_top; s++) { - int cur = (int)strlen(route_chi_prefix[next_route]); - int pf = (int)strlen(chi_stack[s].prefix); - if (cur + pf < HALF_BUF_GUARD) { - strcat(route_chi_prefix[next_route], chi_stack[s].prefix); - } - } - next_route++; - } - } - } - - /* ── Step 5: Build output routes ───────────────────────────── */ int count = 0; for (int ri = 0; ri < nmatches && count < max_out; ri++) { cbm_route_handler_t *r = &out[count]; memset(r, 0, sizeof(*r)); - /* Method */ - int mlen = route_matches[ri].method_eo - route_matches[ri].method_so; - if (mlen >= (int)sizeof(r->method)) { - mlen = (int)sizeof(r->method) - 1; - } - memcpy(r->method, source + route_matches[ri].method_so, (size_t)mlen); - r->method[mlen] = '\0'; - for (int j = 0; r->method[j]; j++) { - r->method[j] = (char)toupper((unsigned char)r->method[j]); - } + copy_match_upper(r->method, (int)sizeof(r->method), source, route_matches[ri].method_so, + route_matches[ri].method_eo); + copy_match(r->path, (int)sizeof(r->path), source, route_matches[ri].path_so, + route_matches[ri].path_eo); - /* Path */ - int plen = route_matches[ri].path_eo - route_matches[ri].path_so; - if (plen >= (int)sizeof(r->path)) { - plen = (int)sizeof(r->path) - 1; - } - memcpy(r->path, source + route_matches[ri].path_so, (size_t)plen); - r->path[plen] = '\0'; - - /* Find the receiver: look back for "varName." before .METHOD */ - const char *dot_pos = source + route_matches[ri].start_pos; - char receiver[64] = ""; - if (dot_pos > source) { - const char *rp = dot_pos - 1; - while (rp >= source && (isalnum((unsigned char)*rp) || *rp == '_')) { - rp--; - } - rp++; - int rlen = (int)(dot_pos - rp); - if (rlen > 0 && rlen < 64) { - memcpy(receiver, rp, (size_t)rlen); - receiver[rlen] = '\0'; - } - } - - /* Apply gin group prefix if receiver matches a binding */ - bool gin_applied = false; - for (int g = 0; g < ngin; g++) { - if (strcmp(receiver, gin_groups[g].var) == 0) { - char full_path[512]; - snprintf(full_path, sizeof(full_path), "%s%s", gin_groups[g].prefix, r->path); - strncpy(r->path, full_path, sizeof(r->path) - 1); - r->path[sizeof(r->path) - 1] = '\0'; - gin_applied = true; - break; - } - } - - /* Apply chi prefix (only if gin group wasn't applied) */ - if (!gin_applied && route_chi_prefix[ri][0]) { - char full_path[512]; - snprintf(full_path, sizeof(full_path), "%s%s", route_chi_prefix[ri], r->path); - strncpy(r->path, full_path, sizeof(r->path) - 1); - r->path[sizeof(r->path) - 1] = '\0'; - } - - strncpy(r->function_name, name ? name : "", sizeof(r->function_name) - 1); - strncpy(r->qualified_name, qn ? qn : "", sizeof(r->qualified_name) - 1); - - /* Handler ref from capture group 3 (e.g., "h.CreateOrder") */ - if (route_matches[ri].handler_so >= 0) { - int hlen = route_matches[ri].handler_eo - route_matches[ri].handler_so; - /* Trim whitespace */ - const char *hs = source + route_matches[ri].handler_so; - while (hlen > 0 && (hs[0] == ' ' || hs[0] == '\t')) { - hs++; - hlen--; - } - while (hlen > 0 && (hs[hlen - 1] == ' ' || hs[hlen - 1] == '\t' || - hs[hlen - 1] == ')' || hs[hlen - 1] == '\n')) { - hlen--; - } - if (hlen > 0 && hlen < (int)sizeof(r->handler_ref)) { - memcpy(r->handler_ref, hs, (size_t)hlen); - r->handler_ref[hlen] = '\0'; - } - } + char receiver[64]; + find_receiver(source, route_matches[ri].start_pos, receiver, 64); + apply_go_route_prefix(r, receiver, gin_groups, ngin, route_chi_prefix[ri]); + fill_route_identity(r, name, qn); + extract_handler_ref(source, &route_matches[ri], r); count++; } @@ -1102,7 +1114,26 @@ int cbm_extract_go_routes(const char *name, const char *qn, const char *source, /* ── Route extraction: Java Spring ─────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Resolve Spring annotation name to HTTP method string. */ +static const char *spring_method_from_annotation(const char *ann_name) { + if (strcmp(ann_name, "Get") == 0) { + return "GET"; + } + if (strcmp(ann_name, "Post") == 0) { + return "POST"; + } + if (strcmp(ann_name, "Put") == 0) { + return "PUT"; + } + if (strcmp(ann_name, "Delete") == 0) { + return "DELETE"; + } + if (strcmp(ann_name, "Patch") == 0) { + return "PATCH"; + } + return "ANY"; +} + int cbm_extract_java_routes(const char *name, const char *qn, const char **decorators, int ndec, cbm_route_handler_t *out, int max_out) { if (!decorators || ndec <= 0) { @@ -1128,64 +1159,30 @@ int cbm_extract_java_routes(const char *name, const char *qn, const char **decor for (int i = 0; i < ndec && count < max_out; i++) { cbm_regmatch_t match[4]; - /* Try WebSocket first */ if (cbm_regexec(&spring_ws_re, decorators[i], 2, match, 0) == 0) { cbm_route_handler_t *r = &out[count]; memset(r, 0, sizeof(*r)); - - int plen = (match[1].rm_eo - match[1].rm_so); - if (plen >= (int)sizeof(r->path)) { - plen = (int)sizeof(r->path) - 1; - } - memcpy(r->path, decorators[i] + match[1].rm_so, (size_t)plen); - r->path[plen] = '\0'; - + copy_match(r->path, (int)sizeof(r->path), decorators[i], match[1].rm_so, + match[1].rm_eo); strncpy(r->method, "WS", sizeof(r->method) - 1); strncpy(r->protocol, "ws", sizeof(r->protocol) - 1); - strncpy(r->function_name, name ? name : "", sizeof(r->function_name) - 1); - strncpy(r->qualified_name, qn ? qn : "", sizeof(r->qualified_name) - 1); + fill_route_identity(r, name, qn); count++; continue; } - /* Try Spring mapping */ if (cbm_regexec(&spring_re, decorators[i], 4, match, 0) == 0) { cbm_route_handler_t *r = &out[count]; memset(r, 0, sizeof(*r)); - /* Method from annotation name */ - int mlen = (match[1].rm_eo - match[1].rm_so); char method_name[32]; - if (mlen >= (int)sizeof(method_name)) { - mlen = (int)sizeof(method_name) - 1; - } - memcpy(method_name, decorators[i] + match[1].rm_so, (size_t)mlen); - method_name[mlen] = '\0'; + copy_match(method_name, (int)sizeof(method_name), decorators[i], match[1].rm_so, + match[1].rm_eo); + strncpy(r->method, spring_method_from_annotation(method_name), sizeof(r->method) - 1); - if (strcmp(method_name, "Get") == 0) { - strncpy(r->method, "GET", sizeof(r->method) - 1); - } else if (strcmp(method_name, "Post") == 0) { - strncpy(r->method, "POST", sizeof(r->method) - 1); - } else if (strcmp(method_name, "Put") == 0) { - strncpy(r->method, "PUT", sizeof(r->method) - 1); - } else if (strcmp(method_name, "Delete") == 0) { - strncpy(r->method, "DELETE", sizeof(r->method) - 1); - } else if (strcmp(method_name, "Patch") == 0) { - strncpy(r->method, "PATCH", sizeof(r->method) - 1); - } else { - strncpy(r->method, "ANY", sizeof(r->method) - 1); - } - - /* Path */ - int plen = (match[3].rm_eo - match[3].rm_so); - if (plen >= (int)sizeof(r->path)) { - plen = (int)sizeof(r->path) - 1; - } - memcpy(r->path, decorators[i] + match[3].rm_so, (size_t)plen); - r->path[plen] = '\0'; - - strncpy(r->function_name, name ? name : "", sizeof(r->function_name) - 1); - strncpy(r->qualified_name, qn ? qn : "", sizeof(r->qualified_name) - 1); + copy_match(r->path, (int)sizeof(r->path), decorators[i], match[3].rm_so, + match[3].rm_eo); + fill_route_identity(r, name, qn); count++; } } @@ -1197,7 +1194,50 @@ int cbm_extract_java_routes(const char *name, const char *qn, const char **decor /* ── Route extraction: Kotlin Ktor ─────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Collect Ktor WebSocket routes. */ +static int collect_ktor_ws_routes(const char *source, const cbm_regex_t *re, const char *name, + const char *qn, cbm_route_handler_t *out, int max_out) { + int count = 0; + const char *p = source; + cbm_regmatch_t match[2]; + + while (count < max_out && cbm_regexec(re, p, 2, match, 0) == 0) { + cbm_route_handler_t *r = &out[count]; + memset(r, 0, sizeof(*r)); + copy_match(r->path, (int)sizeof(r->path), p, match[1].rm_so, match[1].rm_eo); + strncpy(r->method, "WS", sizeof(r->method) - 1); + strncpy(r->protocol, "ws", sizeof(r->protocol) - 1); + fill_route_identity(r, name, qn); + count++; + p += match[0].rm_eo; + } + return count; +} + +/* Collect Ktor HTTP routes, skipping webSocket false positives. */ +static int collect_ktor_http_routes(const char *source, const cbm_regex_t *re, const char *name, + const char *qn, cbm_route_handler_t *out, int max_out) { + int count = 0; + const char *p = source; + cbm_regmatch_t match[3]; + + while (count < max_out && cbm_regexec(re, p, 3, match, 0) == 0) { + const char *match_start = p + match[0].rm_so; + bool is_ws = (match_start >= source + 3 && strncmp(match_start - 3, "web", 3) == 0); + + if (!is_ws) { + cbm_route_handler_t *r = &out[count]; + memset(r, 0, sizeof(*r)); + copy_match_upper(r->method, (int)sizeof(r->method), p, match[1].rm_so, match[1].rm_eo); + copy_match(r->path, (int)sizeof(r->path), p, match[2].rm_so, match[2].rm_eo); + fill_route_identity(r, name, qn); + count++; + } + p += match[0].rm_eo; + } + return count; +} + int cbm_extract_ktor_routes(const char *name, const char *qn, const char *source, cbm_route_handler_t *out, int max_out) { if (!source || !*source) { @@ -1217,66 +1257,8 @@ int cbm_extract_ktor_routes(const char *name, const char *qn, const char *source return 0; } - int count = 0; - const char *p = source; - cbm_regmatch_t match[3]; - - /* WebSocket routes */ - while (count < max_out && cbm_regexec(&ktor_ws_re, p, 2, match, 0) == 0) { - cbm_route_handler_t *r = &out[count]; - memset(r, 0, sizeof(*r)); - - int plen = (match[1].rm_eo - match[1].rm_so); - if (plen >= (int)sizeof(r->path)) { - plen = (int)sizeof(r->path) - 1; - } - memcpy(r->path, p + match[1].rm_so, (size_t)plen); - r->path[plen] = '\0'; - - strncpy(r->method, "WS", sizeof(r->method) - 1); - strncpy(r->protocol, "ws", sizeof(r->protocol) - 1); - strncpy(r->function_name, name ? name : "", sizeof(r->function_name) - 1); - strncpy(r->qualified_name, qn ? qn : "", sizeof(r->qualified_name) - 1); - count++; - p += match[0].rm_eo; - } - - /* HTTP routes */ - p = source; - while (count < max_out && cbm_regexec(&ktor_re, p, 3, match, 0) == 0) { - /* Make sure this isn't the webSocket match (check if preceded by "web") */ - const char *match_start = p + match[0].rm_so; - bool is_ws = (match_start >= source + 3 && strncmp(match_start - 3, "web", 3) == 0); - - if (!is_ws) { - cbm_route_handler_t *r = &out[count]; - memset(r, 0, sizeof(*r)); - - /* Method */ - int mlen = (match[1].rm_eo - match[1].rm_so); - if (mlen >= (int)sizeof(r->method)) { - mlen = (int)sizeof(r->method) - 1; - } - memcpy(r->method, p + match[1].rm_so, (size_t)mlen); - r->method[mlen] = '\0'; - for (int j = 0; r->method[j]; j++) { - r->method[j] = (char)toupper((unsigned char)r->method[j]); - } - - /* Path */ - int plen = (match[2].rm_eo - match[2].rm_so); - if (plen >= (int)sizeof(r->path)) { - plen = (int)sizeof(r->path) - 1; - } - memcpy(r->path, p + match[2].rm_so, (size_t)plen); - r->path[plen] = '\0'; - - strncpy(r->function_name, name ? name : "", sizeof(r->function_name) - 1); - strncpy(r->qualified_name, qn ? qn : "", sizeof(r->qualified_name) - 1); - count++; - } - p += match[0].rm_eo; - } + int count = collect_ktor_ws_routes(source, &ktor_ws_re, name, qn, out, max_out); + count += collect_ktor_http_routes(source, &ktor_re, name, qn, out + count, max_out - count); cbm_regfree(&ktor_re); cbm_regfree(&ktor_ws_re); @@ -1296,7 +1278,6 @@ static bool is_express_receiver(const char *receiver) { return false; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) int cbm_extract_express_routes(const char *name, const char *qn, const char *source, cbm_route_handler_t *out, int max_out) { if (!source || !*source) { @@ -1317,40 +1298,15 @@ int cbm_extract_express_routes(const char *name, const char *qn, const char *sou cbm_regmatch_t match[4]; while (count < max_out && cbm_regexec(&express_re, p, 4, match, 0) == 0) { - /* Extract receiver */ - int rlen = (match[1].rm_eo - match[1].rm_so); char receiver[64]; - if (rlen >= 64) { - rlen = RECEIVER_MAX; - } - memcpy(receiver, p + match[1].rm_so, (size_t)rlen); - receiver[rlen] = '\0'; + copy_match(receiver, (int)sizeof(receiver), p, match[1].rm_so, match[1].rm_eo); if (is_express_receiver(receiver)) { cbm_route_handler_t *r = &out[count]; memset(r, 0, sizeof(*r)); - - /* Method */ - int mlen = (match[2].rm_eo - match[2].rm_so); - if (mlen >= (int)sizeof(r->method)) { - mlen = (int)sizeof(r->method) - 1; - } - memcpy(r->method, p + match[2].rm_so, (size_t)mlen); - r->method[mlen] = '\0'; - for (int j = 0; r->method[j]; j++) { - r->method[j] = (char)toupper((unsigned char)r->method[j]); - } - - /* Path */ - int plen = (match[3].rm_eo - match[3].rm_so); - if (plen >= (int)sizeof(r->path)) { - plen = (int)sizeof(r->path) - 1; - } - memcpy(r->path, p + match[3].rm_so, (size_t)plen); - r->path[plen] = '\0'; - - strncpy(r->function_name, name ? name : "", sizeof(r->function_name) - 1); - strncpy(r->qualified_name, qn ? qn : "", sizeof(r->qualified_name) - 1); + copy_match_upper(r->method, (int)sizeof(r->method), p, match[2].rm_so, match[2].rm_eo); + copy_match(r->path, (int)sizeof(r->path), p, match[3].rm_so, match[3].rm_eo); + fill_route_identity(r, name, qn); count++; } p += match[0].rm_eo; @@ -1480,7 +1436,36 @@ char *cbm_read_source_lines_disk(const char *root_dir, const char *rel_path, int /* ── Read source lines from cached buffer ──────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Append a line to a dynamically growing result buffer. */ +static void append_line_to_result(char **result, int *result_len, int *result_cap, const char *line, + int line_len) { + /* Strip trailing \r for CRLF files */ + int copy_len = line_len; + // cppcheck-suppress knownConditionTrueFalse + if (copy_len > 0 && line[copy_len - 1] == '\r') { + copy_len--; + } + + /* Add separator between lines */ + if (*result_len > 0) { + if (*result_len + 1 >= *result_cap) { + *result_cap = (*result_cap == 0) ? 1024 : *result_cap * 2; + *result = safe_realloc(*result, (size_t)*result_cap); + } + (*result)[(*result_len)++] = '\n'; + } + + /* Add line content */ + if (*result_len + copy_len >= *result_cap) { + *result_cap = *result_len + copy_len + 256; + *result = safe_realloc(*result, (size_t)*result_cap); + } + if (copy_len > 0 && *result) { + memcpy(*result + *result_len, line, (size_t)copy_len); + *result_len += copy_len; + } +} + char *cbm_read_source_lines_cached(const char *source, int source_len, int start_line, int end_line) { if (!source || source_len <= 0 || start_line <= 0 || end_line < start_line) { @@ -1496,36 +1481,11 @@ char *cbm_read_source_lines_cached(const char *source, int source_len, int start while (p < end) { line++; - /* Find end of this line */ const char *eol = memchr(p, '\n', (size_t)(end - p)); int llen = eol ? (int)(eol - p) : (int)(end - p); if (line >= start_line && line <= end_line) { - /* Strip trailing \r for CRLF files */ - int copy_len = llen; - // cppcheck-suppress knownConditionTrueFalse - if (copy_len > 0 && p[copy_len - 1] == '\r') { - copy_len--; - } - - /* Add separator between lines */ - if (result_len > 0) { - if (result_len + 1 >= result_cap) { - result_cap = (result_cap == 0) ? 1024 : result_cap * 2; - result = safe_realloc(result, (size_t)result_cap); - } - result[result_len++] = '\n'; - } - - /* Add line content */ - if (result_len + copy_len >= result_cap) { - result_cap = result_len + copy_len + 256; - result = safe_realloc(result, (size_t)result_cap); - } - if (copy_len > 0) { - memcpy(result + result_len, p, (size_t)copy_len); - result_len += copy_len; - } + append_line_to_result(&result, &result_len, &result_cap, p, llen); } if (line > end_line) { diff --git a/src/pipeline/pass_compile_commands.c b/src/pipeline/pass_compile_commands.c index b4842af96..e3acaf9b8 100644 --- a/src/pipeline/pass_compile_commands.c +++ b/src/pipeline/pass_compile_commands.c @@ -12,7 +12,16 @@ #include #include "yyjson/yyjson.h" -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Emit the current token if non-empty. Returns updated count. */ +static int emit_token(char *current, int *clen, char **out, int count, int max_out) { + if (*clen > 0 && count < max_out) { + current[*clen] = '\0'; + out[count++] = strdup(current); + *clen = 0; + } + return count; +} + int cbm_split_command(const char *cmd, char **out, int max_out) { if (!cmd || !out || max_out <= 0) { return 0; @@ -28,30 +37,18 @@ int cbm_split_command(const char *cmd, char **out, int max_out) { if (in_quote) { if (c == in_quote) { in_quote = 0; - } else { - if (clen < (int)sizeof(current) - 1) { - current[clen++] = c; - } + } else if (clen < (int)sizeof(current) - 1) { + current[clen++] = c; } } else if (c == '"' || c == '\'') { in_quote = c; } else if (c == ' ' || c == '\t') { - if (clen > 0 && count < max_out) { - current[clen] = '\0'; - out[count++] = strdup(current); - clen = 0; - } - } else { - if (clen < (int)sizeof(current) - 1) { - current[clen++] = c; - } + count = emit_token(current, &clen, out, count, max_out); + } else if (clen < (int)sizeof(current) - 1) { + current[clen++] = c; } } - if (clen > 0 && count < max_out) { - current[clen] = '\0'; - out[count++] = strdup(current); - } - return count; + return emit_token(current, &clen, out, count, max_out); } /* Resolve a path: if relative, join with directory. */ @@ -75,60 +72,65 @@ static char *resolve_path(const char *path, const char *directory) { return strdup(path); } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Try to consume a -I or -isystem include path flag. Returns true if consumed. */ +static bool try_include_flag(cbm_compile_flags_t *f, const char **args, int argc, int *i, + const char *directory) { + const char *arg = args[*i]; + if (arg[0] == '-' && arg[1] == 'I') { + const char *path = arg + 2; + if (*path == '\0' && *i + 1 < argc) { + (*i)++; + path = args[*i]; + } + if (path && *path) { + f->include_paths[f->include_count++] = resolve_path(path, directory); + } + return true; + } + if (strcmp(arg, "-isystem") == 0 && *i + 1 < argc) { + (*i)++; + f->include_paths[f->include_count++] = resolve_path(args[*i], directory); + return true; + } + return false; +} + +/* Try to consume a -D define flag. Returns true if consumed. */ +static bool try_define_flag(cbm_compile_flags_t *f, const char **args, int argc, int *i) { + const char *arg = args[*i]; + if (arg[0] != '-' || arg[1] != 'D') { + return false; + } + const char *define = arg + 2; + if (*define == '\0' && *i + 1 < argc) { + (*i)++; + define = args[*i]; + } + if (define && *define) { + f->defines[f->define_count++] = strdup(define); + } + return true; +} + cbm_compile_flags_t *cbm_extract_flags(const char **args, int argc, const char *directory) { cbm_compile_flags_t *f = calloc(1, sizeof(*f)); if (!f) { return NULL; } - - /* Pre-allocate arrays */ f->include_paths = calloc(argc, sizeof(char *)); f->defines = calloc(argc, sizeof(char *)); for (int i = 0; i < argc; i++) { - const char *arg = args[i]; - - /* -I or -I */ - if (arg[0] == '-' && arg[1] == 'I') { - const char *path = arg + 2; - if (*path == '\0' && i + 1 < argc) { - i++; - path = args[i]; - } - if (path && *path) { - f->include_paths[f->include_count++] = resolve_path(path, directory); - } + if (try_include_flag(f, args, argc, &i, directory)) { continue; } - - /* -isystem */ - if (strcmp(arg, "-isystem") == 0 && i + 1 < argc) { - i++; - f->include_paths[f->include_count++] = resolve_path(args[i], directory); + if (try_define_flag(f, args, argc, &i)) { continue; } - - /* -D or -D= */ - if (arg[0] == '-' && arg[1] == 'D') { - const char *define = arg + 2; - if (*define == '\0' && i + 1 < argc) { - i++; - define = args[i]; - } - if (define && *define) { - f->defines[f->define_count++] = strdup(define); - } - continue; - } - - /* -std= */ - if (strncmp(arg, "-std=", 5) == 0) { - snprintf(f->standard, sizeof(f->standard), "%s", arg + 5); - continue; + if (strncmp(args[i], "-std=", 5) == 0) { + snprintf(f->standard, sizeof(f->standard), "%s", args[i] + 5); } } - return f; } @@ -147,7 +149,87 @@ void cbm_compile_flags_free(cbm_compile_flags_t *f) { free(f); } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Extract compiler flag args from either "arguments" array or "command" string. */ +static int extract_flag_args(yyjson_val *args_val, yyjson_val *cmd_val, const char **flag_args, + char **split_args, int max_args) { + if (args_val && yyjson_is_arr(args_val)) { + int n = 0; + yyjson_val *a; + yyjson_arr_iter aiter; + yyjson_arr_iter_init(args_val, &aiter); + while ((a = yyjson_arr_iter_next(&aiter)) && n < max_args) { + const char *s = yyjson_get_str(a); + if (s) { + flag_args[n++] = s; + } + } + return n; + } + if (cmd_val && yyjson_is_str(cmd_val)) { + int n = cbm_split_command(yyjson_get_str(cmd_val), split_args, max_args); + for (int j = 0; j < n; j++) { + flag_args[j] = split_args[j]; + } + return n; + } + return 0; +} + +/* Process a single compile_commands.json entry. Returns 1 if added, 0 otherwise. */ +static int process_compile_entry(yyjson_val *entry, const char *repo_path, char **out_path, + cbm_compile_flags_t **out_flag) { + yyjson_val *dir_val = yyjson_obj_get(entry, "directory"); + yyjson_val *file_val = yyjson_obj_get(entry, "file"); + yyjson_val *cmd_val = yyjson_obj_get(entry, "command"); + yyjson_val *args_val = yyjson_obj_get(entry, "arguments"); + + if (!file_val) { + return 0; + } + const char *directory = dir_val ? yyjson_get_str(dir_val) : ""; + const char *file_path = yyjson_get_str(file_val); + if (!file_path) { + return 0; + } + + char *split_args[256]; + const char *flag_args[256]; + int flag_argc = extract_flag_args(args_val, cmd_val, flag_args, split_args, 256); + + if (flag_argc == 0) { + return 0; + } + + cbm_compile_flags_t *f = cbm_extract_flags(flag_args, flag_argc, directory); + + if (cmd_val && yyjson_is_str(cmd_val)) { + for (int j = 0; j < flag_argc; j++) { + free(split_args[j]); + } + } + + if (!f) { + return 0; + } + + char abs_path[4096]; + if (file_path[0] != '/' && directory && directory[0]) { + snprintf(abs_path, sizeof(abs_path), "%s/%s", directory, file_path); + } else { + snprintf(abs_path, sizeof(abs_path), "%s", file_path); + } + + size_t repo_len = strlen(repo_path); + if (strncmp(abs_path, repo_path, repo_len) != 0 || abs_path[repo_len] != '/') { + cbm_compile_flags_free(f); + return 0; + } + + *out_path = strdup(abs_path + repo_len + 1); + *out_flag = f; + return 1; +} + int cbm_parse_compile_commands(const char *json_data, const char *repo_path, char ***out_paths, cbm_compile_flags_t ***out_flags) { if (!json_data || !repo_path || !out_paths || !out_flags) { @@ -182,78 +264,13 @@ int cbm_parse_compile_commands(const char *json_data, const char *repo_path, cha yyjson_arr_iter_init(root, &iter); while ((entry = yyjson_arr_iter_next(&iter))) { - yyjson_val *dir_val = yyjson_obj_get(entry, "directory"); - yyjson_val *file_val = yyjson_obj_get(entry, "file"); - yyjson_val *cmd_val = yyjson_obj_get(entry, "command"); - yyjson_val *args_val = yyjson_obj_get(entry, "arguments"); - - if (!file_val) { - continue; + char *p = NULL; + cbm_compile_flags_t *f = NULL; + if (process_compile_entry(entry, repo_path, &p, &f)) { + paths[count] = p; + flags[count] = f; + count++; } - const char *directory = dir_val ? yyjson_get_str(dir_val) : ""; - const char *file_path = yyjson_get_str(file_val); - if (!file_path) { - continue; - } - - /* Build arguments array */ - char *split_args[256]; - const char *flag_args[256]; - int flag_argc = 0; - - if (args_val && yyjson_is_arr(args_val)) { - yyjson_val *a; - yyjson_arr_iter aiter; - yyjson_arr_iter_init(args_val, &aiter); - while ((a = yyjson_arr_iter_next(&aiter)) && flag_argc < 256) { - const char *s = yyjson_get_str(a); - if (s) { - flag_args[flag_argc++] = s; - } - } - } else if (cmd_val && yyjson_is_str(cmd_val)) { - int n = cbm_split_command(yyjson_get_str(cmd_val), split_args, 256); - for (int j = 0; j < n; j++) { - flag_args[j] = split_args[j]; - } - flag_argc = n; - } - - if (flag_argc == 0) { - continue; - } - - cbm_compile_flags_t *f = cbm_extract_flags(flag_args, flag_argc, directory); - - /* Free split_args if we used splitCommand */ - if (cmd_val && yyjson_is_str(cmd_val)) { - for (int j = 0; j < flag_argc; j++) { - free(split_args[j]); - } - } - - if (!f) { - continue; - } - - /* Compute relative path */ - char abs_path[4096]; - if (file_path[0] != '/' && directory && directory[0]) { - snprintf(abs_path, sizeof(abs_path), "%s/%s", directory, file_path); - } else { - snprintf(abs_path, sizeof(abs_path), "%s", file_path); - } - - /* Check if inside repo */ - size_t repo_len = strlen(repo_path); - if (strncmp(abs_path, repo_path, repo_len) != 0 || abs_path[repo_len] != '/') { - cbm_compile_flags_free(f); - continue; - } - - paths[count] = strdup(abs_path + repo_len + 1); - flags[count] = f; - count++; } yyjson_doc_free(doc); diff --git a/src/pipeline/pass_configlink.c b/src/pipeline/pass_configlink.c index 1501cf248..5baa811e5 100644 --- a/src/pipeline/pass_configlink.c +++ b/src/pipeline/pass_configlink.c @@ -206,7 +206,36 @@ static const char *path_basename(const char *path) { return slash ? slash + 1 : path; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Check if a Cargo.toml QN contains a dependency section in any dotted part. */ +static bool is_cargo_dep_section(const char *qn) { + char qn_copy[512]; + snprintf(qn_copy, sizeof(qn_copy), "%s", qn); + char *saveptr = NULL; + char *part = strtok_r(qn_copy, ".", &saveptr); + while (part) { + char lower[128]; + size_t plen = strlen(part); + if (plen >= sizeof(lower)) { + plen = sizeof(lower) - 1; + } + for (size_t j = 0; j < plen; j++) { + lower[j] = (char)tolower((unsigned char)part[j]); + } + lower[plen] = '\0'; + + static const char *dep_secs[] = {"dependencies", "devdependencies", + "peerdependencies", "dev-dependencies", + "build-dependencies", NULL}; + for (int k = 0; dep_secs[k]; k++) { + if (strcmp(lower, dep_secs[k]) == 0) { + return true; + } + } + part = strtok_r(NULL, ".", &saveptr); + } + return false; +} + static int collect_manifest_deps(const cbm_gbuf_node_t *const *vars, int var_count, dep_entry_t *out, int max_out) { int n = 0; @@ -216,44 +245,10 @@ static int collect_manifest_deps(const cbm_gbuf_node_t *const *vars, int var_cou continue; } - /* Check if QN contains a dependency section name */ - bool is_dep = false; - if (vars[i]->qualified_name) { - is_dep = is_dep_section(vars[i]->qualified_name); - } + bool is_dep = vars[i]->qualified_name && is_dep_section(vars[i]->qualified_name); - /* Cargo.toml special case: check QN parts */ if (!is_dep && strcmp(base, "Cargo.toml") == 0 && vars[i]->qualified_name) { - /* Check if any dotted part is a dep section */ - char qn_copy[512]; - snprintf(qn_copy, sizeof(qn_copy), "%s", vars[i]->qualified_name); - char *saveptr = NULL; - char *part = strtok_r(qn_copy, ".", &saveptr); - while (part) { - char lower[128]; - size_t plen = strlen(part); - if (plen >= sizeof(lower)) { - plen = sizeof(lower) - 1; - } - for (size_t j = 0; j < plen; j++) { - lower[j] = (char)tolower((unsigned char)part[j]); - } - lower[plen] = '\0'; - - static const char *dep_secs[] = {"dependencies", "devdependencies", - "peerdependencies", "dev-dependencies", - "build-dependencies", NULL}; - for (int k = 0; dep_secs[k]; k++) { - if (strcmp(lower, dep_secs[k]) == 0) { - is_dep = true; - break; - } - } - if (is_dep) { - break; - } - part = strtok_r(NULL, ".", &saveptr); - } + is_dep = is_cargo_dep_section(vars[i]->qualified_name); } if (is_dep) { @@ -265,7 +260,34 @@ static int collect_manifest_deps(const cbm_gbuf_node_t *const *vars, int var_cou return n; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Lowercase a string into buf. */ +static void lowercase_into(char *buf, size_t bufsize, const char *src) { + size_t len = src ? strlen(src) : 0; + for (size_t j = 0; j < len && j < bufsize - 1; j++) { + buf[j] = (char)tolower((unsigned char)src[j]); + } + buf[len < bufsize ? len : bufsize - 1] = '\0'; +} + +/* Match a dep name (lowercased) against an import target node. + * Returns confidence > 0 on match, 0 on no match. */ +static double match_dep_to_import(const cbm_gbuf_node_t *target, const char *dep_lower) { + char target_lower[256]; + lowercase_into(target_lower, sizeof(target_lower), target->name); + + if (strcmp(target_lower, dep_lower) == 0) { + return CONF_DEP_EXACT; + } + if (target->qualified_name) { + char qn_lower[512]; + lowercase_into(qn_lower, sizeof(qn_lower), target->qualified_name); + if (strstr(qn_lower, dep_lower) != NULL) { + return CONF_DEP_QN_SUBSTR; + } + } + return 0.0; +} + static int strategy_dep_imports(cbm_gbuf_t *gb) { const cbm_gbuf_node_t **vars = NULL; int var_count = 0; @@ -291,14 +313,9 @@ static int strategy_dep_imports(cbm_gbuf_t *gb) { for (int di = 0; di < dep_count; di++) { char dep_lower[256]; - size_t dlen = strlen(deps[di].name); - for (size_t j = 0; j < dlen && j < sizeof(dep_lower) - 1; j++) { - dep_lower[j] = (char)tolower((unsigned char)deps[di].name[j]); - } - dep_lower[dlen < sizeof(dep_lower) ? dlen : sizeof(dep_lower) - 1] = '\0'; + lowercase_into(dep_lower, sizeof(dep_lower), deps[di].name); for (int ii = 0; ii < import_count; ii++) { - /* Resolve target and source nodes from gbuf */ const cbm_gbuf_node_t *target = cbm_gbuf_find_by_id(gb, imports[ii]->target_id); if (!target) { continue; @@ -309,30 +326,7 @@ static int strategy_dep_imports(cbm_gbuf_t *gb) { continue; } - /* Compare dep name to import target */ - double confidence = 0.0; - char target_lower[256]; - size_t tlen = target->name ? strlen(target->name) : 0; - for (size_t j = 0; j < tlen && j < sizeof(target_lower) - 1; j++) { - target_lower[j] = (char)tolower((unsigned char)target->name[j]); - } - target_lower[tlen < sizeof(target_lower) ? tlen : sizeof(target_lower) - 1] = '\0'; - - if (strcmp(target_lower, dep_lower) == 0) { - confidence = CONF_DEP_EXACT; - } else if (target->qualified_name) { - char qn_lower[512]; - size_t qlen = strlen(target->qualified_name); - for (size_t j = 0; j < qlen && j < sizeof(qn_lower) - 1; j++) { - qn_lower[j] = (char)tolower((unsigned char)target->qualified_name[j]); - } - qn_lower[qlen < sizeof(qn_lower) ? qlen : sizeof(qn_lower) - 1] = '\0'; - - if (strstr(qn_lower, dep_lower) != NULL) { - confidence = CONF_DEP_QN_SUBSTR; - } - } - + double confidence = match_dep_to_import(target, dep_lower); if (confidence > 0.0) { char props[512]; snprintf( @@ -352,8 +346,109 @@ static int strategy_dep_imports(cbm_gbuf_t *gb) { /* ── Strategy 3: Config File Path → Code String Reference ───────── */ +typedef struct { + const char *key; + int64_t node_id; +} path_map_t; + +/* Match a ref_path against config module maps. Returns target node_id (0 = no match). */ +static int64_t match_ref_to_config(const char *ref_path, double *out_confidence, + const path_map_t *full_map, int full_count, + const path_map_t *base_map, int base_count) { + /* Try full path match */ + for (int fi = 0; fi < full_count; fi++) { + if (strcmp(full_map[fi].key, ref_path) == 0) { + *out_confidence = CONF_FILE_FULLPATH; + return full_map[fi].node_id; + } + } + + /* Try basename match */ + const char *ref_base = path_basename(ref_path); + for (int bi = 0; bi < base_count; bi++) { + if (strcmp(base_map[bi].key, ref_base) == 0) { + *out_confidence = CONF_FILE_BASENAME; + return base_map[bi].node_id; + } + } + + return 0; +} + +/* Scan one source file for config file references and create CONFIGURES edges. */ +static int scan_source_for_config_refs(cbm_gbuf_t *gb, const char *project, const char *repo_path, + const char *file_path, const cbm_regex_t *re, + const path_map_t *full_map, int full_count, + const path_map_t *base_map, int base_count) { + char abs_path[1024]; + snprintf(abs_path, sizeof(abs_path), "%s/%s", repo_path, file_path); + + FILE *f = fopen(abs_path, "r"); + if (!f) { + return 0; + } + + (void)fseek(f, 0, SEEK_END); + long fsize = ftell(f); + (void)fseek(f, 0, SEEK_SET); + if (fsize <= 0 || fsize > (long)10 * 1024 * 1024) { + (void)fclose(f); + return 0; + } + + char *source = malloc((size_t)fsize + 1); + if (!source) { + (void)fclose(f); + return 0; + } + size_t nread = fread(source, 1, (size_t)fsize, f); + source[nread] = '\0'; + (void)fclose(f); + + int edge_count = 0; + cbm_regmatch_t match[3]; + const char *cursor = source; + while (cbm_regexec(re, cursor, 3, match, 0) == 0) { + int start = match[1].rm_so; + int end = match[1].rm_eo; + int ref_len = end - start; + char ref_path[512]; + if (ref_len >= (int)sizeof(ref_path)) { + ref_len = (int)sizeof(ref_path) - 1; + } + memcpy(ref_path, cursor + start, (size_t)ref_len); + ref_path[ref_len] = '\0'; + + double confidence = 0.0; + int64_t target_id = + match_ref_to_config(ref_path, &confidence, full_map, full_count, base_map, base_count); + + if (target_id != 0) { + char *module_qn = cbm_pipeline_fqn_module(project, file_path); + if (module_qn) { + const cbm_gbuf_node_t *src_node = cbm_gbuf_find_by_qn(gb, module_qn); + if (src_node) { + char props[512]; + snprintf(props, sizeof(props), + "{\"strategy\":\"file_reference\",\"confidence\":%.2f,\"ref_" + "path\":\"%s\"}", + confidence, ref_path); + + cbm_gbuf_insert_edge(gb, src_node->id, target_id, "CONFIGURES", props); + edge_count++; + } + free(module_qn); + } + } + + cursor += match[0].rm_eo; + } + + free(source); + return edge_count; +} + /* Disk-based fallback: reads source from disk (sequential pipeline path). */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) static int strategy_file_refs_disk(cbm_gbuf_t *gb, const char *project, const char *repo_path) { /* Collect config Module nodes */ const cbm_gbuf_node_t **modules = NULL; @@ -363,11 +458,6 @@ static int strategy_file_refs_disk(cbm_gbuf_t *gb, const char *project, const ch } /* Build basename → Module and fullpath → Module maps */ - typedef struct { - const char *key; - int64_t node_id; - } path_map_t; - path_map_t *base_map = calloc((size_t)mod_count, sizeof(path_map_t)); path_map_t *full_map = calloc((size_t)mod_count, sizeof(path_map_t)); int base_count = 0; @@ -412,97 +502,8 @@ static int strategy_file_refs_disk(cbm_gbuf_t *gb, const char *project, const ch for (int si = 0; si < scan_count; si++) { int idx = scan_indices[si]; - const char *file_path = modules[idx]->file_path; - - /* Read source file from disk */ - char abs_path[1024]; - snprintf(abs_path, sizeof(abs_path), "%s/%s", repo_path, file_path); - - FILE *f = fopen(abs_path, "r"); - if (!f) { - continue; - } - - (void)fseek(f, 0, SEEK_END); - long fsize = ftell(f); - (void)fseek(f, 0, SEEK_SET); - if (fsize <= 0 || fsize > (long)10 * 1024 * 1024) { - (void)fclose(f); - continue; - } - - char *source = malloc((size_t)fsize + 1); - if (!source) { - (void)fclose(f); - continue; - } - size_t nread = fread(source, 1, (size_t)fsize, f); - source[nread] = '\0'; - (void)fclose(f); - - /* Find all config file references via regex */ - cbm_regmatch_t match[3]; - const char *cursor = source; - while (cbm_regexec(&re, cursor, 3, match, 0) == 0) { - /* Extract the captured path (group 1) */ - int start = match[1].rm_so; - int end = match[1].rm_eo; - int ref_len = end - start; - char ref_path[512]; - if (ref_len >= (int)sizeof(ref_path)) { - ref_len = (int)sizeof(ref_path) - 1; - } - memcpy(ref_path, cursor + start, (size_t)ref_len); - ref_path[ref_len] = '\0'; - - /* Match against config modules */ - int64_t target_id = 0; - double confidence = 0.0; - - /* Try full path match */ - for (int fi = 0; fi < full_count; fi++) { - if (strcmp(full_map[fi].key, ref_path) == 0) { - target_id = full_map[fi].node_id; - confidence = CONF_FILE_FULLPATH; - break; - } - } - - /* Try basename match */ - if (target_id == 0) { - const char *ref_base = path_basename(ref_path); - for (int bi = 0; bi < base_count; bi++) { - if (strcmp(base_map[bi].key, ref_base) == 0) { - target_id = base_map[bi].node_id; - confidence = CONF_FILE_BASENAME; - break; - } - } - } - - if (target_id != 0) { - /* Resolve source Module QN */ - char *module_qn = cbm_pipeline_fqn_module(project, file_path); - if (module_qn) { - const cbm_gbuf_node_t *src_node = cbm_gbuf_find_by_qn(gb, module_qn); - if (src_node) { - char props[512]; - snprintf(props, sizeof(props), - "{\"strategy\":\"file_reference\",\"confidence\":%.2f,\"ref_" - "path\":\"%s\"}", - confidence, ref_path); - - cbm_gbuf_insert_edge(gb, src_node->id, target_id, "CONFIGURES", props); - edge_count++; - } - free(module_qn); - } - } - - cursor += match[0].rm_eo; - } - - free(source); + edge_count += scan_source_for_config_refs(gb, project, repo_path, modules[idx]->file_path, + &re, full_map, full_count, base_map, base_count); } cbm_regfree(&re); diff --git a/src/pipeline/pass_configures.c b/src/pipeline/pass_configures.c index c711789cf..53b3cf3f9 100644 --- a/src/pipeline/pass_configures.c +++ b/src/pipeline/pass_configures.c @@ -33,17 +33,39 @@ bool cbm_is_env_var_name(const char *s) { return has_upper; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Emit a camelCase-split word (lowercased) into the output buffer. + * Prepends '_' if out_pos > 0. Returns updated out_pos and token_count. */ +static void emit_camel_words(const char *part, size_t plen, char *norm_out, size_t norm_sz, + size_t *out_pos, int *token_count) { + size_t start = 0; + for (size_t i = 1; i < plen; i++) { + if (part[i] >= 'A' && part[i] <= 'Z' && part[i - 1] >= 'a' && part[i - 1] <= 'z') { + if (*out_pos > 0 && *out_pos < norm_sz - 1) { + norm_out[(*out_pos)++] = '_'; + } + for (size_t j = start; j < i && *out_pos < norm_sz - 1; j++) { + norm_out[(*out_pos)++] = (char)tolower((unsigned char)part[j]); + } + (*token_count)++; + start = i; + } + } + /* Emit remaining */ + if (*out_pos > 0 && *out_pos < norm_sz - 1) { + norm_out[(*out_pos)++] = '_'; + } + for (size_t j = start; j < plen && *out_pos < norm_sz - 1; j++) { + norm_out[(*out_pos)++] = (char)tolower((unsigned char)part[j]); + } + (*token_count)++; +} + int cbm_normalize_config_key(const char *key, char *norm_out, size_t norm_sz) { if (!key || !norm_out || norm_sz == 0) { return 0; } norm_out[0] = '\0'; - /* Split on delimiters: _, -, . */ - /* Then split each part on camelCase transitions */ - /* Collect all words, lowercase them, join with _ */ - char buf[512]; size_t klen = strlen(key); if (klen >= sizeof(buf)) { @@ -52,46 +74,20 @@ int cbm_normalize_config_key(const char *key, char *norm_out, size_t norm_sz) { memcpy(buf, key, klen); buf[klen] = '\0'; - /* First pass: replace delimiters with spaces */ + /* Replace delimiters with spaces */ for (size_t i = 0; i < klen; i++) { if (buf[i] == '_' || buf[i] == '-' || buf[i] == '.') { buf[i] = ' '; } } - /* Extract parts (space-separated) */ int token_count = 0; size_t out_pos = 0; char *saveptr = NULL; char *part = strtok_r(buf, " ", &saveptr); while (part) { - /* Split camelCase within this part */ - size_t plen = strlen(part); - size_t start = 0; - - for (size_t i = 1; i < plen; i++) { - if (part[i] >= 'A' && part[i] <= 'Z' && part[i - 1] >= 'a' && part[i - 1] <= 'z') { - /* Found transition — emit word */ - if (out_pos > 0 && out_pos < norm_sz - 1) { - norm_out[out_pos++] = '_'; - } - for (size_t j = start; j < i && out_pos < norm_sz - 1; j++) { - norm_out[out_pos++] = (char)tolower((unsigned char)part[j]); - } - token_count++; - start = i; - } - } - /* Emit remaining */ - if (out_pos > 0 && out_pos < norm_sz - 1) { - norm_out[out_pos++] = '_'; - } - for (size_t j = start; j < plen && out_pos < norm_sz - 1; j++) { - norm_out[out_pos++] = (char)tolower((unsigned char)part[j]); - } - token_count++; - + emit_camel_words(part, strlen(part), norm_out, norm_sz, &out_pos, &token_count); part = strtok_r(NULL, " ", &saveptr); } diff --git a/src/pipeline/pass_enrichment.c b/src/pipeline/pass_enrichment.c index c9d6c2bd0..3c2e94cb5 100644 --- a/src/pipeline/pass_enrichment.c +++ b/src/pipeline/pass_enrichment.c @@ -60,13 +60,37 @@ int cbm_split_camel_case(const char *s, char **out, int max_out) { return count; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Strip decorator syntax: leading @, #[...], and arguments (...). + * Operates in-place on buf. Returns pointer to the cleaned token start. */ +static char *strip_decorator_syntax(char *buf) { + char *p = buf; + if (*p == '@') { + p++; + } + if (p[0] == '#' && p[1] == '[') { + p += 2; + size_t plen = strlen(p); + if (plen > 0 && p[plen - 1] == ']') { + p[plen - 1] = '\0'; + } + } + char *paren = strchr(p, '('); + if (paren) { + *paren = '\0'; + } + for (char *c = p; *c; c++) { + if (*c == '.' || *c == '_' || *c == '-' || *c == ':' || *c == '/') { + *c = ' '; + } + } + return p; +} + int cbm_tokenize_decorator(const char *dec, char **out, int max_out) { if (!dec || !out || max_out <= 0) { return 0; } - /* Work on a copy */ char buf[256]; size_t len = strlen(dec); if (len >= sizeof(buf)) { @@ -75,58 +99,26 @@ int cbm_tokenize_decorator(const char *dec, char **out, int max_out) { memcpy(buf, dec, len); buf[len] = '\0'; - char *p = buf; + char *p = strip_decorator_syntax(buf); - /* Strip leading @ */ - if (*p == '@') { - p++; - } - /* Strip leading #[ and trailing ] */ - if (p[0] == '#' && p[1] == '[') { - p += 2; - size_t plen = strlen(p); - if (plen > 0 && p[plen - 1] == ']') { - p[plen - 1] = '\0'; - } - } - - /* Strip arguments: everything from first ( onwards */ - char *paren = strchr(p, '('); - if (paren) { - *paren = '\0'; - } - - /* Replace delimiters with spaces: . _ - : / */ - for (char *c = p; *c; c++) { - if (*c == '.' || *c == '_' || *c == '-' || *c == ':' || *c == '/') { - *c = ' '; - } - } - - /* Process each part: split camelCase, lowercase, filter stopwords */ int count = 0; char *saveptr = NULL; char *part = strtok_r(p, " ", &saveptr); while (part && count < max_out) { - /* Split camelCase */ char *camel_parts[16]; int camel_count = cbm_split_camel_case(part, camel_parts, 16); for (int i = 0; i < camel_count && count < max_out; i++) { - /* Lowercase */ for (char *c = camel_parts[i]; *c; c++) { *c = (char)tolower((unsigned char)*c); } - - /* Filter: must be >= 2 chars and not a stopword */ if (strlen(camel_parts[i]) >= 2 && !is_decorator_stopword(camel_parts[i])) { out[count++] = camel_parts[i]; } else { free(camel_parts[i]); } } - part = strtok_r(NULL, " ", &saveptr); } @@ -274,20 +266,26 @@ static int cmp_str(const void *a, const void *b) { return strcmp(*(const char **)a, *(const char **)b); } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) -int cbm_pipeline_pass_decorator_tags(cbm_gbuf_t *gbuf, const char *project) { - if (!gbuf || !project) { - return 0; +/* Free tagged_node_t array. */ +static void free_tagged_nodes(tagged_node_t *nodes, int count) { + for (int n = 0; n < count; n++) { + free(nodes[n].qualified_name); + for (int w = 0; w < nodes[n].word_count; w++) { + free(nodes[n].words[w]); + } + free(nodes[n].words); } + free(nodes); +} +/* Phase 1: Collect decorated nodes and count word frequency. */ +static int collect_decorated_nodes(cbm_gbuf_t *gbuf, tagged_node_t **out_nodes, + CBMHashTable *word_counts) { static const char *labels[] = {"Function", "Method", "Class"}; static const int nlabels = 3; - - /* Phase 1: Collect decorated nodes and count word frequency */ tagged_node_t *nodes = NULL; int node_count = 0; int node_cap = 0; - CBMHashTable *word_counts = cbm_ht_create(128); for (int l = 0; l < nlabels; l++) { const cbm_gbuf_node_t **found = NULL; @@ -303,28 +301,68 @@ int cbm_pipeline_pass_decorator_tags(cbm_gbuf_t *gbuf, const char *project) { if (wc <= 0) { continue; } - - /* Grow array if needed */ if (node_count >= node_cap) { node_cap = node_cap ? node_cap * 2 : 64; nodes = safe_realloc(nodes, sizeof(tagged_node_t) * node_cap); } - tagged_node_t *tn = &nodes[node_count++]; tn->node_id = found[i]->id; tn->qualified_name = strdup(found[i]->qualified_name); tn->words = words; tn->word_count = wc; - - /* Update word counts */ for (int w = 0; w < wc; w++) { intptr_t cnt = (intptr_t)cbm_ht_get(word_counts, words[w]); cbm_ht_set(word_counts, words[w], (void *)(cnt + 1)); } } - /* gbuf data is borrowed — no free needed */ } + *out_nodes = nodes; + return node_count; +} + +/* Phase 3: Apply candidate tags to nodes in the gbuf. */ +static int apply_decorator_tags(cbm_gbuf_t *gbuf, tagged_node_t *nodes, int node_count, + CBMHashTable *candidates) { + int tagged = 0; + for (int n = 0; n < node_count; n++) { + char *tag_words[256]; + int tag_count = 0; + for (int w = 0; w < nodes[n].word_count; w++) { + if (cbm_ht_get(candidates, nodes[n].words[w]) && tag_count < 256) { + tag_words[tag_count++] = nodes[n].words[w]; + } + } + if (tag_count == 0) { + continue; + } + qsort(tag_words, tag_count, sizeof(char *), cmp_str); + + const cbm_gbuf_node_t *gn = cbm_gbuf_find_by_qn(gbuf, nodes[n].qualified_name); + if (!gn) { + continue; + } + + const char *props = gn->properties_json ? gn->properties_json : "{}"; + char *new_props = inject_decorator_tags(props, tag_words, tag_count); + if (new_props) { + cbm_gbuf_upsert_node(gbuf, gn->label, gn->name, gn->qualified_name, gn->file_path, + gn->start_line, gn->end_line, new_props); + free(new_props); + tagged++; + } + } + return tagged; +} + +int cbm_pipeline_pass_decorator_tags(cbm_gbuf_t *gbuf, const char *project) { + if (!gbuf || !project) { + return 0; + } + + CBMHashTable *word_counts = cbm_ht_create(128); + tagged_node_t *nodes = NULL; + int node_count = collect_decorated_nodes(gbuf, &nodes, word_counts); if (node_count == 0) { cbm_ht_free(word_counts); free(nodes); @@ -334,7 +372,6 @@ int cbm_pipeline_pass_decorator_tags(cbm_gbuf_t *gbuf, const char *project) { /* Phase 2: Determine candidates (words on 2+ nodes) */ CBMHashTable *candidates = cbm_ht_create(64); int candidate_count = 0; - for (int n = 0; n < node_count; n++) { for (int w = 0; w < nodes[n].word_count; w++) { const char *word = nodes[n].words[w]; @@ -346,65 +383,12 @@ int cbm_pipeline_pass_decorator_tags(cbm_gbuf_t *gbuf, const char *project) { } } - if (candidate_count == 0) { - for (int n = 0; n < node_count; n++) { - free(nodes[n].qualified_name); - for (int w = 0; w < nodes[n].word_count; w++) { - free(nodes[n].words[w]); - } - free(nodes[n].words); - } - free(nodes); - cbm_ht_free(word_counts); - cbm_ht_free(candidates); - return 0; - } - - /* Phase 3: Tag each node with its candidate words */ int tagged = 0; - for (int n = 0; n < node_count; n++) { - char *tag_words[256]; - int tag_count = 0; - for (int w = 0; w < nodes[n].word_count; w++) { - if (cbm_ht_get(candidates, nodes[n].words[w]) && tag_count < 256) { - tag_words[tag_count++] = nodes[n].words[w]; - } - } - - if (tag_count == 0) { - continue; - } - - /* Sort tags alphabetically */ - qsort(tag_words, tag_count, sizeof(char *), cmp_str); - - /* Look up node in gbuf (borrowed pointer) */ - const cbm_gbuf_node_t *gn = cbm_gbuf_find_by_qn(gbuf, nodes[n].qualified_name); - if (!gn) { - continue; - } - - /* Inject decorator_tags into properties_json */ - const char *props = gn->properties_json ? gn->properties_json : "{}"; - char *new_props = inject_decorator_tags(props, tag_words, tag_count); - if (new_props) { - /* Re-upsert with updated properties — gbuf frees old, copies new */ - cbm_gbuf_upsert_node(gbuf, gn->label, gn->name, gn->qualified_name, gn->file_path, - gn->start_line, gn->end_line, new_props); - free(new_props); - tagged++; - } + if (candidate_count > 0) { + tagged = apply_decorator_tags(gbuf, nodes, node_count, candidates); } - /* Cleanup */ - for (int n = 0; n < node_count; n++) { - free(nodes[n].qualified_name); - for (int w = 0; w < nodes[n].word_count; w++) { - free(nodes[n].words[w]); - } - free(nodes[n].words); - } - free(nodes); + free_tagged_nodes(nodes, node_count); cbm_ht_free(word_counts); cbm_ht_free(candidates); diff --git a/src/pipeline/pass_envscan.c b/src/pipeline/pass_envscan.c index ca808fffc..747af1c96 100644 --- a/src/pipeline/pass_envscan.c +++ b/src/pipeline/pass_envscan.c @@ -190,160 +190,202 @@ static file_type_t detect_file_type(const char *name) { /* ── Line scanner ──────────────────────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Extract key/value from a regex match with two capture groups. + * Returns 1 on success, 0 if groups are empty or too large. */ +static int extract_kv_groups(const char *trimmed, const cbm_regmatch_t *m, int key_grp, int val_grp, + char *key_out, size_t key_sz, char *val_out, size_t val_sz) { + int klen = (m[key_grp].rm_eo - m[key_grp].rm_so); + int vlen = (m[val_grp].rm_eo - m[val_grp].rm_so); + if (klen <= 0 || klen >= (int)key_sz || vlen <= 0 || vlen >= (int)val_sz) { + return 0; + } + memcpy(key_out, trimmed + m[key_grp].rm_so, klen); + key_out[klen] = '\0'; + memcpy(val_out, trimmed + m[val_grp].rm_so, vlen); + val_out[vlen] = '\0'; + return 1; +} + +/* Try to scan a Dockerfile line. */ +static int scan_dockerfile_line(const char *line, char *key, size_t ksz, char *val, size_t vsz) { + cbm_regmatch_t m[5]; + if (cbm_regexec(&dockerfile_re, line, 4, m, 0) != 0) { + return 0; + } + if (!extract_kv_groups(line, m, 2, 3, key, ksz, val, vsz)) { + return 0; + } + size_t vl = strlen(val); + while (vl > 0 && (val[vl - 1] == '"' || val[vl - 1] == '\'')) { + val[--vl] = '\0'; + } + return 1; +} + +/* Try to scan a YAML line. */ +static int scan_yaml_line(const char *line, char *key, size_t ksz, char *val, size_t vsz) { + cbm_regmatch_t m[5]; + if (cbm_regexec(&yaml_kv_re, line, 3, m, 0) == 0 && + extract_kv_groups(line, m, 1, 2, key, ksz, val, vsz)) { + return 1; + } + if (cbm_regexec(&yaml_setenv_re, line, 3, m, 0) == 0 && + extract_kv_groups(line, m, 1, 2, key, ksz, val, vsz)) { + return 1; + } + return 0; +} + +/* Try to scan a Terraform line. */ +static int scan_terraform_line(const char *line, char *key, size_t ksz, char *val, size_t vsz) { + cbm_regmatch_t m[5]; + if (cbm_regexec(&terraform_re, line, 3, m, 0) != 0) { + return 0; + } + int vlen = (m[2].rm_eo - m[2].rm_so); + if (vlen <= 0 || vlen >= (int)vsz) { + return 0; + } + strncpy(key, "_tf_default", ksz - 1); + key[ksz - 1] = '\0'; + memcpy(val, line + m[2].rm_so, vlen); + val[vlen] = '\0'; + return 1; +} + +/* Try single-regex scan (shell, envfile, toml, properties). */ +static int scan_regex_line(cbm_regex_t *re, const char *line, int kg, int vg, char *key, size_t ksz, + char *val, size_t vsz) { + cbm_regmatch_t m[5]; + if (cbm_regexec(re, line, 5, m, 0) == 0 && + extract_kv_groups(line, m, kg, vg, key, ksz, val, vsz)) { + return 1; + } + return 0; +} + static int scan_line(const char *line, file_type_t ft, char *key_out, size_t key_sz, char *val_out, size_t val_sz) { - cbm_regmatch_t m[5]; const char *trimmed = line; while (*trimmed == ' ' || *trimmed == '\t') { trimmed++; } - - /* Skip comments */ if (*trimmed == '#' || (trimmed[0] == '/' && trimmed[1] == '/')) { return 0; } switch (ft) { case FT_DOCKERFILE: - if (cbm_regexec(&dockerfile_re, trimmed, 4, m, 0) == 0) { - /* group 2 = key, group 3 = value */ - int klen = (m[2].rm_eo - m[2].rm_so); - int vlen = (m[3].rm_eo - m[3].rm_so); - if (klen > 0 && klen < (int)key_sz && vlen > 0 && vlen < (int)val_sz) { - memcpy(key_out, trimmed + m[2].rm_so, klen); - key_out[klen] = '\0'; - memcpy(val_out, trimmed + m[3].rm_so, vlen); - val_out[vlen] = '\0'; - /* Trim quotes from value */ - size_t vl = strlen(val_out); - while (vl > 0 && (val_out[vl - 1] == '"' || val_out[vl - 1] == '\'')) { - val_out[--vl] = '\0'; - } - return 1; - } - } - break; - + return scan_dockerfile_line(trimmed, key_out, key_sz, val_out, val_sz); case FT_YAML: - if (cbm_regexec(&yaml_kv_re, trimmed, 3, m, 0) == 0) { - int klen = (m[1].rm_eo - m[1].rm_so); - int vlen = (m[2].rm_eo - m[2].rm_so); - if (klen > 0 && klen < (int)key_sz && vlen > 0 && vlen < (int)val_sz) { - memcpy(key_out, trimmed + m[1].rm_so, klen); - key_out[klen] = '\0'; - memcpy(val_out, trimmed + m[2].rm_so, vlen); - val_out[vlen] = '\0'; - return 1; - } - } - if (cbm_regexec(&yaml_setenv_re, trimmed, 3, m, 0) == 0) { - int klen = (m[1].rm_eo - m[1].rm_so); - int vlen = (m[2].rm_eo - m[2].rm_so); - if (klen > 0 && klen < (int)key_sz && vlen > 0 && vlen < (int)val_sz) { - memcpy(key_out, trimmed + m[1].rm_so, klen); - key_out[klen] = '\0'; - memcpy(val_out, trimmed + m[2].rm_so, vlen); - val_out[vlen] = '\0'; - return 1; - } - } - break; - + return scan_yaml_line(trimmed, key_out, key_sz, val_out, val_sz); case FT_TERRAFORM: - if (cbm_regexec(&terraform_re, trimmed, 3, m, 0) == 0) { - int vlen = (m[2].rm_eo - m[2].rm_so); - if (vlen > 0 && vlen < (int)val_sz) { - strncpy(key_out, "_tf_default", key_sz - 1); - key_out[key_sz - 1] = '\0'; - memcpy(val_out, trimmed + m[2].rm_so, vlen); - val_out[vlen] = '\0'; - return 1; - } - } - break; - + return scan_terraform_line(trimmed, key_out, key_sz, val_out, val_sz); case FT_SHELL: - if (cbm_regexec(&shell_re, trimmed, 4, m, 0) == 0) { - /* group 2 = key, group 3 = value */ - int klen = (m[2].rm_eo - m[2].rm_so); - int vlen = (m[3].rm_eo - m[3].rm_so); - if (klen > 0 && klen < (int)key_sz && vlen > 0 && vlen < (int)val_sz) { - memcpy(key_out, trimmed + m[2].rm_so, klen); - key_out[klen] = '\0'; - memcpy(val_out, trimmed + m[3].rm_so, vlen); - val_out[vlen] = '\0'; - return 1; - } - } - break; - + return scan_regex_line(&shell_re, trimmed, 2, 3, key_out, key_sz, val_out, val_sz); case FT_ENVFILE: - if (cbm_regexec(&envfile_re, trimmed, 3, m, 0) == 0) { - int klen = (m[1].rm_eo - m[1].rm_so); - int vlen = (m[2].rm_eo - m[2].rm_so); - if (klen > 0 && klen < (int)key_sz && vlen > 0 && vlen < (int)val_sz) { - memcpy(key_out, trimmed + m[1].rm_so, klen); - key_out[klen] = '\0'; - memcpy(val_out, trimmed + m[2].rm_so, vlen); - val_out[vlen] = '\0'; - return 1; - } - } - break; - + return scan_regex_line(&envfile_re, trimmed, 1, 2, key_out, key_sz, val_out, val_sz); case FT_TOML: - if (cbm_regexec(&toml_re, trimmed, 3, m, 0) == 0) { - int klen = (m[1].rm_eo - m[1].rm_so); - int vlen = (m[2].rm_eo - m[2].rm_so); - if (klen > 0 && klen < (int)key_sz && vlen > 0 && vlen < (int)val_sz) { - memcpy(key_out, trimmed + m[1].rm_so, klen); - key_out[klen] = '\0'; - memcpy(val_out, trimmed + m[2].rm_so, vlen); - val_out[vlen] = '\0'; - return 1; - } - } - break; - + return scan_regex_line(&toml_re, trimmed, 1, 2, key_out, key_sz, val_out, val_sz); case FT_PROPERTIES: - if (cbm_regexec(&properties_re, trimmed, 3, m, 0) == 0) { - int klen = (m[1].rm_eo - m[1].rm_so); - int vlen = (m[2].rm_eo - m[2].rm_so); - if (klen > 0 && klen < (int)key_sz && vlen > 0 && vlen < (int)val_sz) { - memcpy(key_out, trimmed + m[1].rm_so, klen); - key_out[klen] = '\0'; - memcpy(val_out, trimmed + m[2].rm_so, vlen); - val_out[vlen] = '\0'; - return 1; - } - } - break; - + return scan_regex_line(&properties_re, trimmed, 1, 2, key_out, key_sz, val_out, val_sz); default: - break; + return 0; } - return 0; } /* ── Public API ────────────────────────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Scan a single file for env URL bindings. Returns number of bindings added. */ +static int scan_env_file(const char *full_path, const char *rel, file_type_t ft, + cbm_env_binding_t *out, int max_out) { + FILE *f = fopen(full_path, "r"); + if (!f) { + return 0; + } + + struct stat fst; + if (fstat(fileno(f), &fst) != 0 || fst.st_size > (long)1024 * 1024) { + fclose(f); + return 0; + } + + int count = 0; + char line[2048]; + while (fgets(line, sizeof(line), f) && count < max_out) { + size_t ll = strlen(line); + while (ll > 0 && (line[ll - 1] == '\n' || line[ll - 1] == '\r')) { + line[--ll] = '\0'; + } + + char key[128]; + char value[512]; + if (!scan_line(line, ft, key, sizeof(key), value, sizeof(value))) { + continue; + } + if (strncmp(value, "http://", 7) != 0 && strncmp(value, "https://", 8) != 0) { + continue; + } + if (cbm_is_secret_binding(key, value) || cbm_is_secret_value(value)) { + continue; + } + + strncpy(out[count].key, key, sizeof(out[count].key) - 1); + out[count].key[sizeof(out[count].key) - 1] = '\0'; + strncpy(out[count].value, value, sizeof(out[count].value) - 1); + out[count].value[sizeof(out[count].value) - 1] = '\0'; + strncpy(out[count].file_path, rel, sizeof(out[count].file_path) - 1); + out[count].file_path[sizeof(out[count].file_path) - 1] = '\0'; + count++; + } + (void)fclose(f); + return count; +} + +/* Process a single directory entry for env scanning. Returns bindings added. */ +static int process_env_entry(cbm_dirent_t *ent, const char *dir_path, const char *root_path, + cbm_env_binding_t *out, int max_out, char path_stack[][512], + int *stack_top) { + char full_path[512]; + snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, ent->name); + + struct stat st; + if (stat(full_path, &st) != 0) { + return 0; + } + if (S_ISDIR(st.st_mode)) { + if (!is_ignored_dir(ent->name) && *stack_top < 256) { + strncpy(path_stack[*stack_top], full_path, sizeof(path_stack[0]) - 1); + path_stack[*stack_top][sizeof(path_stack[0]) - 1] = '\0'; + (*stack_top)++; + } + return 0; + } + if (is_secret_file(ent->name)) { + return 0; + } + file_type_t ft = detect_file_type(ent->name); + if (ft == FT_UNKNOWN) { + return 0; + } + const char *rel = full_path + strlen(root_path); + while (*rel == '/') { + rel++; + } + return scan_env_file(full_path, rel, ft, out, max_out); +} + int cbm_scan_project_env_urls(const char *root_path, cbm_env_binding_t *out, int max_out) { if (!root_path || !out || max_out <= 0) { return 0; } - compile_patterns(); int count = 0; - - /* Recursive directory walk using a stack */ char path_stack[256][512]; - int stack_top = 0; + int stack_top = 1; strncpy(path_stack[0], root_path, sizeof(path_stack[0]) - 1); path_stack[0][sizeof(path_stack[0]) - 1] = '\0'; - stack_top = 1; while (stack_top > 0 && count < max_out) { stack_top--; @@ -355,96 +397,12 @@ int cbm_scan_project_env_urls(const char *root_path, cbm_env_binding_t *out, int if (!d) { continue; } - cbm_dirent_t *ent; while ((ent = cbm_readdir(d)) && count < max_out) { - char full_path[512]; - snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, ent->name); - - struct stat st; - if (stat(full_path, &st) != 0) { - continue; - } - - if (S_ISDIR(st.st_mode)) { - if (is_ignored_dir(ent->name)) { - continue; - } - if (stack_top < 256) { - strncpy(path_stack[stack_top], full_path, sizeof(path_stack[0]) - 1); - path_stack[stack_top][sizeof(path_stack[0]) - 1] = '\0'; - stack_top++; - } - continue; - } - - /* Skip secret files */ - if (is_secret_file(ent->name)) { - continue; - } - - /* Determine file type */ - file_type_t ft = detect_file_type(ent->name); - if (ft == FT_UNKNOWN) { - continue; - } - - /* Compute relative path */ - const char *rel = full_path + strlen(root_path); - while (*rel == '/') { - rel++; - } - - /* Open first, then fstat on fd to avoid TOCTOU race */ - FILE *f = fopen(full_path, "r"); - if (!f) { - continue; - } - - /* Recheck size on the open fd (not the path) */ - struct stat fst; - if (fstat(fileno(f), &fst) != 0 || fst.st_size > (long)1024 * 1024) { - fclose(f); - continue; - } - - char line[2048]; - while (fgets(line, sizeof(line), f) && count < max_out) { - /* Strip trailing newline */ - size_t ll = strlen(line); - while (ll > 0 && (line[ll - 1] == '\n' || line[ll - 1] == '\r')) { - line[--ll] = '\0'; - } - - char key[128]; - char value[512]; - if (scan_line(line, ft, key, sizeof(key), value, sizeof(value))) { - /* Must be a URL */ - if (strncmp(value, "http://", 7) != 0 && strncmp(value, "https://", 8) != 0) { - continue; - } - /* Secret exclusion */ - if (cbm_is_secret_binding(key, value)) { - continue; - } - if (cbm_is_secret_value(value)) { - continue; - } - - /* Add binding */ - strncpy(out[count].key, key, sizeof(out[count].key) - 1); - out[count].key[sizeof(out[count].key) - 1] = '\0'; - strncpy(out[count].value, value, sizeof(out[count].value) - 1); - out[count].value[sizeof(out[count].value) - 1] = '\0'; - strncpy(out[count].file_path, rel, sizeof(out[count].file_path) - 1); - out[count].file_path[sizeof(out[count].file_path) - 1] = '\0'; - count++; - } - } - (void)fclose(f); + count += process_env_entry(ent, dir_path, root_path, out + count, max_out - count, + path_stack, &stack_top); } cbm_closedir(d); } - return count; } diff --git a/src/pipeline/pass_gitdiff.c b/src/pipeline/pass_gitdiff.c index a37b3fd20..75179e96d 100644 --- a/src/pipeline/pass_gitdiff.c +++ b/src/pipeline/pass_gitdiff.c @@ -35,7 +35,50 @@ void cbm_parse_range(const char *s, int *out_start, int *out_count) { *out_count = (int)strtol(comma + 1, NULL, 10); } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +#define HUNK_LINE_BUF 1536 + +/* Parse a single name-status line into a cbm_changed_file_t. + * Returns true if a valid entry was produced. */ +static bool parse_one_name_status(const char *line, size_t line_len, cbm_changed_file_t *out_f) { + char tmp[HUNK_LINE_BUF]; + if (line_len >= sizeof(tmp)) { + line_len = sizeof(tmp) - 1; + } + memcpy(tmp, line, line_len); + tmp[line_len] = '\0'; + + char *status_str = tmp; + char *tab1 = strchr(tmp, '\t'); + if (!tab1) { + return false; + } + *tab1 = '\0'; + char *path1 = tab1 + 1; + + char *tab2 = strchr(path1, '\t'); + char *path2 = NULL; + if (tab2) { + *tab2 = '\0'; + path2 = tab2 + 1; + } + + memset(out_f, 0, sizeof(*out_f)); + + if (status_str[0] == 'R') { + out_f->status[0] = 'R'; + out_f->status[1] = '\0'; + snprintf(out_f->old_path, sizeof(out_f->old_path), "%s", path1); + snprintf(out_f->path, sizeof(out_f->path), "%s", path2 ? path2 : path1); + } else { + out_f->status[0] = status_str[0]; + out_f->status[1] = '\0'; + snprintf(out_f->path, sizeof(out_f->path), "%s", path1); + out_f->old_path[0] = '\0'; + } + + return cbm_is_trackable_file(out_f->path); +} + int cbm_parse_name_status(const char *output, cbm_changed_file_t *out, int max_out) { if (!output || !out || max_out <= 0) { return 0; @@ -45,64 +88,14 @@ int cbm_parse_name_status(const char *output, cbm_changed_file_t *out, int max_o const char *line = output; while (*line && count < max_out) { - /* Find end of line */ const char *eol = strchr(line, '\n'); size_t line_len = eol ? (size_t)(eol - line) : strlen(line); - if (line_len == 0) { - line = eol ? eol + 1 : line + line_len; - continue; - } - - /* Copy line to temp buffer for parsing */ -#define HUNK_LINE_BUF 1536 - char tmp[HUNK_LINE_BUF]; - if (line_len >= sizeof(tmp)) { - line_len = sizeof(tmp) - 1; - } - memcpy(tmp, line, line_len); - tmp[line_len] = '\0'; - - /* Split on tabs */ - char *status_str = tmp; - char *tab1 = strchr(tmp, '\t'); - if (!tab1) { - line = eol ? eol + 1 : line + line_len; - continue; - } - *tab1 = '\0'; - char *path1 = tab1 + 1; - - char *tab2 = strchr(path1, '\t'); - char *path2 = NULL; - if (tab2) { - *tab2 = '\0'; - path2 = tab2 + 1; - } - - cbm_changed_file_t f; - memset(&f, 0, sizeof(f)); - - if (status_str[0] == 'R') { - /* Rename: R100\told_path\tnew_path */ - f.status[0] = 'R'; - f.status[1] = '\0'; - snprintf(f.old_path, sizeof(f.old_path), "%s", path1); - if (path2) { - snprintf(f.path, sizeof(f.path), "%s", path2); - } else { - snprintf(f.path, sizeof(f.path), "%s", path1); + if (line_len > 0) { + cbm_changed_file_t f; + if (parse_one_name_status(line, line_len, &f)) { + out[count++] = f; } - } else { - f.status[0] = status_str[0]; - f.status[1] = '\0'; - snprintf(f.path, sizeof(f.path), "%s", path1); - f.old_path[0] = '\0'; - } - - /* Filter untrackable files */ - if (cbm_is_trackable_file(f.path)) { - out[count++] = f; } line = eol ? eol + 1 : line + line_len; @@ -110,7 +103,49 @@ int cbm_parse_name_status(const char *output, cbm_changed_file_t *out, int max_o return count; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Parse a single @@ hunk header line and emit a hunk entry if valid. + * Returns true if a hunk was added. */ +static bool parse_hunk_line(const char *line, size_t line_len, const char *current_file, + cbm_changed_hunk_t *out_h) { + const char *plus = memchr(line, '+', line_len); + if (!plus || plus <= line) { + return false; + } + + const char *end_at = strstr(plus, " @@"); + size_t range_len; + if (end_at) { + range_len = (size_t)(end_at - plus - 1); + } else { + range_len = (size_t)(line + line_len - plus - 1); + } + + char range_str[64]; + if (range_len >= sizeof(range_str)) { + range_len = sizeof(range_str) - 1; + } + memcpy(range_str, plus + 1, range_len); + range_str[range_len] = '\0'; + + int start; + int cnt; + cbm_parse_range(range_str, &start, &cnt); + + if (start <= 0 || !cbm_is_trackable_file(current_file)) { + return false; + } + + int end = start + cnt - 1; + if (end < start) { + end = start; + } + + snprintf(out_h->path, sizeof(out_h->path), "%s", current_file); + out_h->start_line = start; + out_h->end_line = end; + return true; +} + int cbm_parse_hunks(const char *output, cbm_changed_hunk_t *out, int max_out) { if (!output || !out || max_out <= 0) { return 0; @@ -129,7 +164,6 @@ int cbm_parse_hunks(const char *output, cbm_changed_hunk_t *out, int max_out) { continue; } - /* Track current file from "+++ b/" header */ if (line_len > 6 && strncmp(line, "+++ b/", 6) == 0) { size_t flen = line_len - 6; if (flen >= sizeof(current_file)) { @@ -137,53 +171,10 @@ int cbm_parse_hunks(const char *output, cbm_changed_hunk_t *out, int max_out) { } memcpy(current_file, line + 6, flen); current_file[flen] = '\0'; - line = eol ? eol + 1 : line + line_len; - continue; - } - - /* Skip binary files */ - if (line_len >= 12 && strncmp(line, "Binary files", 12) == 0) { - line = eol ? eol + 1 : line + line_len; - continue; - } - - /* Parse @@ hunk headers */ - if (line_len >= 2 && line[0] == '@' && line[1] == '@' && current_file[0]) { - /* Find +start,count */ - const char *plus = memchr(line, '+', line_len); - if (plus && plus > line) { - /* Find end " @@" */ - const char *end_at = strstr(plus, " @@"); - size_t range_len; - if (end_at) { - range_len = (size_t)(end_at - plus - 1); - } else { - range_len = (size_t)(line + line_len - plus - 1); - } - - char range_str[64]; - if (range_len >= sizeof(range_str)) { - range_len = sizeof(range_str) - 1; - } - memcpy(range_str, plus + 1, range_len); - range_str[range_len] = '\0'; - - int start; - int cnt; - cbm_parse_range(range_str, &start, &cnt); - - if (start > 0 && cbm_is_trackable_file(current_file)) { - int end = start + cnt - 1; - if (end < start) { - end = start; - } - - cbm_changed_hunk_t h; - snprintf(h.path, sizeof(h.path), "%s", current_file); - h.start_line = start; - h.end_line = end; - out[count++] = h; - } + } else if (line_len >= 2 && line[0] == '@' && line[1] == '@' && current_file[0]) { + cbm_changed_hunk_t h; + if (parse_hunk_line(line, line_len, current_file, &h)) { + out[count++] = h; } } diff --git a/src/pipeline/pass_httplinks.c b/src/pipeline/pass_httplinks.c index 56ce6c92c..3a5e8d138 100644 --- a/src/pipeline/pass_httplinks.c +++ b/src/pipeline/pass_httplinks.c @@ -331,10 +331,118 @@ static int discover_module_routes(const cbm_gbuf_node_t *mod, const cbm_pipeline /* ── Prefix resolution ─────────────────────────────────────────── */ +/* Import map entry for prefix resolution (shared between FastAPI/Express). */ +typedef struct { + char var[128]; + char module[256]; +} hl_import_entry_t; + +/* Build Python import map from "from X import Y" statements. */ +static int fastapi_build_import_map(const char *source, const cbm_regex_t *import_re, + hl_import_entry_t *imports, int max_imports) { + int count = 0; + const char *p = source; + cbm_regmatch_t pm[3]; + while (count < max_imports && cbm_regexec(import_re, p, 3, pm, 0) == 0) { + int mlen = (pm[1].rm_eo - pm[1].rm_so); + int vlen = (pm[2].rm_eo - pm[2].rm_so); + if (mlen < 256 && vlen < 128) { + snprintf(imports[count].module, 256, "%.*s", mlen, p + pm[1].rm_so); + snprintf(imports[count].var, 128, "%.*s", vlen, p + pm[2].rm_so); + count++; + } + p += pm[0].rm_eo; + } + return count; +} + +/* Apply a resolved prefix to all routes matching a module path. */ +static void apply_module_prefix(cbm_route_handler_t *routes, int route_count, + const char *module_path, const char *file_frag, char *prefix) { + size_t pfx_len = strlen(prefix); + while (pfx_len > 0 && prefix[pfx_len - 1] == '/') { + prefix[--pfx_len] = '\0'; + } + + for (int r = 0; r < route_count; r++) { + if (strncmp(routes[r].path, prefix, pfx_len) == 0) { + continue; + } + if (strstr(routes[r].qualified_name, module_path) || + (routes[r].function_name[0] && strstr(routes[r].qualified_name, file_frag))) { + char new_path[256]; + const char *old_path = routes[r].path; + while (*old_path == '/') { + old_path++; + } + snprintf(new_path, sizeof(new_path), "%s/%s", prefix, old_path); + snprintf(routes[r].path, sizeof(routes[r].path), "%s", new_path); + } + } +} + +/* Look up a variable name in the import map. */ +static const char *hl_import_lookup(const hl_import_entry_t *imports, int count, const char *var) { + for (int i = 0; i < count; i++) { + if (strcmp(imports[i].var, var) == 0) { + return imports[i].module; + } + } + return NULL; +} + +/* Process one Python module for include_router calls. */ +static void fastapi_process_module(cbm_pipeline_ctx_t *ctx, const cbm_gbuf_node_t *mod, + const cbm_regex_t *include_re, const cbm_regex_t *import_re, + cbm_route_handler_t *routes, int route_count) { + char *source = read_full_source(ctx, mod->file_path); + if (!source) { + return; + } + + hl_import_entry_t imports[64]; + memset(imports, 0, sizeof(imports)); + int import_count = fastapi_build_import_map(source, import_re, imports, 64); + + const char *p = source; + cbm_regmatch_t pm[3]; + while (cbm_regexec(include_re, p, 3, pm, 0) == 0) { + char var_name[128] = {0}; + char prefix[256] = {0}; + int vlen = (pm[1].rm_eo - pm[1].rm_so); + int plen = (pm[2].rm_eo - pm[2].rm_so); + if (vlen < 128) { + snprintf(var_name, 128, "%.*s", vlen, p + pm[1].rm_so); + } + if (plen < 256) { + snprintf(prefix, 256, "%.*s", plen, p + pm[2].rm_so); + } + p += pm[0].rm_eo; + + /* Resolve var → module path */ + const char *module_path = hl_import_lookup(imports, import_count, var_name); + if (!module_path) { + continue; + } + + /* Convert dotted module path to file fragment */ + char file_frag[256]; + snprintf(file_frag, sizeof(file_frag), "%s", module_path); + for (char *c = file_frag; *c; c++) { + if (*c == '.') { + *c = '/'; + } + } + + apply_module_prefix(routes, route_count, module_path, file_frag, prefix); + } + + free(source); +} + /* Resolve FastAPI include_router prefixes. * Scans Python Module nodes for: app.include_router(var, prefix="/prefix") * and from ... import var. Prepends prefix to routes from matching modules. */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) static void resolve_fastapi_prefixes(cbm_pipeline_ctx_t *ctx, cbm_route_handler_t *routes, int route_count) { const cbm_gbuf_node_t **modules = NULL; @@ -364,110 +472,136 @@ static void resolve_fastapi_prefixes(cbm_pipeline_ctx_t *ctx, cbm_route_handler_ if (!mod->file_path || !has_suffix(mod->file_path, ".py")) { continue; } - - /* Read full module source (from cache or disk) */ - char *source = read_full_source(ctx, mod->file_path); - if (!source) { - continue; - } - - /* Build import map: var_name → dotted.module.path */ - typedef struct { - char var[128]; - char module[256]; - } import_entry_t; - import_entry_t imports[64]; - memset(imports, 0, sizeof(imports)); - int import_count = 0; - - const char *p = source; - cbm_regmatch_t pm[3]; - while (import_count < 64 && cbm_regexec(&import_re, p, 3, pm, 0) == 0) { - int mlen = (pm[1].rm_eo - pm[1].rm_so); - int vlen = (pm[2].rm_eo - pm[2].rm_so); - if (mlen < 256 && vlen < 128) { - snprintf(imports[import_count].module, 256, "%.*s", mlen, p + pm[1].rm_so); - snprintf(imports[import_count].var, 128, "%.*s", vlen, p + pm[2].rm_so); - import_count++; - } - p += pm[0].rm_eo; - } - - /* Find include_router calls */ - p = source; - while (cbm_regexec(&include_re, p, 3, pm, 0) == 0) { - char var_name[128] = {0}; - char prefix[256] = {0}; - int vlen = (pm[1].rm_eo - pm[1].rm_so); - int plen = (pm[2].rm_eo - pm[2].rm_so); - if (vlen < 128) { - snprintf(var_name, 128, "%.*s", vlen, p + pm[1].rm_so); - } - if (plen < 256) { - snprintf(prefix, 256, "%.*s", plen, p + pm[2].rm_so); - } - p += pm[0].rm_eo; - - /* Find which module this var was imported from */ - const char *module_path = NULL; - for (int i = 0; i < import_count; i++) { - if (strcmp(imports[i].var, var_name) == 0) { - module_path = imports[i].module; - break; - } - } - if (!module_path) { - continue; - } - - /* Convert dotted module path to file fragment */ - char file_frag[256]; - snprintf(file_frag, sizeof(file_frag), "%s", module_path); - for (char *c = file_frag; *c; c++) { - if (*c == '.') { - *c = '/'; - } - } - - /* Strip trailing slash from prefix */ - size_t pfx_len = strlen(prefix); - while (pfx_len > 0 && prefix[pfx_len - 1] == '/') { - prefix[--pfx_len] = '\0'; - } - - /* Apply prefix to matching routes */ - for (int r = 0; r < route_count; r++) { - /* Skip routes that already have this prefix */ - if (strncmp(routes[r].path, prefix, pfx_len) == 0) { - continue; - } - - /* Match routes whose QN contains the imported module path. - * QN uses dots (project.orders.routes.func), so match both: - * - dotted module path ("orders.routes") against QN - * - slash-based file fragment ("orders/routes") against file_path */ - if (strstr(routes[r].qualified_name, module_path) || - (routes[r].function_name[0] && strstr(routes[r].qualified_name, file_frag))) { - char new_path[256]; - const char *old_path = routes[r].path; - while (*old_path == '/') { - old_path++; - } - snprintf(new_path, sizeof(new_path), "%s/%s", prefix, old_path); - snprintf(routes[r].path, sizeof(routes[r].path), "%s", new_path); - } - } - } - - free(source); + fastapi_process_module(ctx, mod, &include_re, &import_re, routes, route_count); } cbm_regfree(&include_re); cbm_regfree(&import_re); } +/* Build JS/TS import map from require() and ES import statements. */ +static int express_build_import_map(const char *source, const cbm_regex_t *require_re, + const cbm_regex_t *esimport_re, hl_import_entry_t *imports, + int max_imports) { + int count = 0; + cbm_regmatch_t pm[4]; + + const char *p = source; + while (count < max_imports && cbm_regexec(require_re, p, 4, pm, 0) == 0) { + int vlen = (pm[2].rm_eo - pm[2].rm_so); + int mlen = (pm[3].rm_eo - pm[3].rm_so); + if (vlen < 128 && mlen < 256) { + snprintf(imports[count].var, 128, "%.*s", vlen, p + pm[2].rm_so); + snprintf(imports[count].module, 256, "%.*s", mlen, p + pm[3].rm_so); + count++; + } + p += pm[0].rm_eo; + } + p = source; + while (count < max_imports && cbm_regexec(esimport_re, p, 3, pm, 0) == 0) { + int vlen = (pm[1].rm_eo - pm[1].rm_so); + int mlen = (pm[2].rm_eo - pm[2].rm_so); + if (vlen < 128 && mlen < 256) { + snprintf(imports[count].var, 128, "%.*s", vlen, p + pm[1].rm_so); + snprintf(imports[count].module, 256, "%.*s", mlen, p + pm[2].rm_so); + count++; + } + p += pm[0].rm_eo; + } + return count; +} + +/* Apply Express .use() prefix for one matched var to routes. */ +static void express_apply_use_prefix(cbm_route_handler_t *routes, int route_count, + const char *module_path, char *prefix) { + /* Strip leading ./ and ../ from relative import */ + const char *file_frag = module_path; + if (strncmp(file_frag, "./", 2) == 0) { + file_frag += 2; + } + if (strncmp(file_frag, "../", 3) == 0) { + file_frag += 3; + } + + /* Strip trailing slash from prefix */ + size_t pfx_len = strlen(prefix); + while (pfx_len > 0 && prefix[pfx_len - 1] == '/') { + prefix[--pfx_len] = '\0'; + } + + /* Convert slash-based path to dots for QN matching */ + char dotted_frag[DOTTED_FRAG_BUF]; + snprintf(dotted_frag, sizeof(dotted_frag), "%s", file_frag); + for (char *c = dotted_frag; *c; c++) { + if (*c == '/') { + *c = '.'; + } + } + + for (int r = 0; r < route_count; r++) { + if (strncmp(routes[r].path, prefix, pfx_len) == 0) { + continue; + } + if (strstr(routes[r].qualified_name, dotted_frag) || + strstr(routes[r].qualified_name, file_frag)) { + char new_path[256]; + const char *old_path = routes[r].path; + while (*old_path == '/') { + old_path++; + } + snprintf(new_path, sizeof(new_path), "%s/%s", prefix, old_path); + snprintf(routes[r].path, sizeof(routes[r].path), "%s", new_path); + } + } +} + +/* Process one JS/TS module for Express .use() prefix resolution. */ +static void express_process_module(cbm_pipeline_ctx_t *ctx, const cbm_gbuf_node_t *mod, + const cbm_regex_t *use_re, const cbm_regex_t *require_re, + const cbm_regex_t *esimport_re, cbm_route_handler_t *routes, + int route_count) { + char *source = read_full_source(ctx, mod->file_path); + if (!source) { + return; + } + + hl_import_entry_t imports[64]; + memset(imports, 0, sizeof(imports)); + int import_count = express_build_import_map(source, require_re, esimport_re, imports, 64); + + const char *p = source; + cbm_regmatch_t pm[3]; + while (cbm_regexec(use_re, p, 3, pm, 0) == 0) { + char prefix[256] = {0}; + char var_name[128] = {0}; + int plen = (pm[1].rm_eo - pm[1].rm_so); + int vlen = (pm[2].rm_eo - pm[2].rm_so); + if (plen < 256) { + snprintf(prefix, 256, "%.*s", plen, p + pm[1].rm_so); + } + if (vlen < 128) { + snprintf(var_name, 128, "%.*s", vlen, p + pm[2].rm_so); + } + p += pm[0].rm_eo; + + const char *module_path = NULL; + for (int i = 0; i < import_count; i++) { + if (strcmp(imports[i].var, var_name) == 0) { + module_path = imports[i].module; + break; + } + } + if (!module_path) { + continue; + } + + express_apply_use_prefix(routes, route_count, module_path, prefix); + } + + free(source); +} + /* Resolve Express app.use("/prefix", routerVar) prefixes. */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) static void resolve_express_prefixes(cbm_pipeline_ctx_t *ctx, cbm_route_handler_t *routes, int route_count) { const cbm_gbuf_node_t **modules = NULL; @@ -507,117 +641,7 @@ static void resolve_express_prefixes(cbm_pipeline_ctx_t *ctx, cbm_route_handler_ if (!mod->file_path || !is_jsts_file(mod->file_path)) { continue; } - - /* Read full module source (from cache or disk) */ - char *source = read_full_source(ctx, mod->file_path); - if (!source) { - continue; - } - - /* Build import map: var_name → module_path */ - typedef struct { - char var[128]; - char module[256]; - } import_entry_t; - import_entry_t imports[64]; - memset(imports, 0, sizeof(imports)); - int import_count = 0; - - const char *p = source; - cbm_regmatch_t pm[4]; - while (import_count < 64 && cbm_regexec(&require_re, p, 4, pm, 0) == 0) { - int vlen = (pm[2].rm_eo - pm[2].rm_so); - int mlen = (pm[3].rm_eo - pm[3].rm_so); - if (vlen < 128 && mlen < 256) { - snprintf(imports[import_count].var, 128, "%.*s", vlen, p + pm[2].rm_so); - snprintf(imports[import_count].module, 256, "%.*s", mlen, p + pm[3].rm_so); - import_count++; - } - p += pm[0].rm_eo; - } - p = source; - while (import_count < 64 && cbm_regexec(&esimport_re, p, 3, pm, 0) == 0) { - int vlen = (pm[1].rm_eo - pm[1].rm_so); - int mlen = (pm[2].rm_eo - pm[2].rm_so); - if (vlen < 128 && mlen < 256) { - snprintf(imports[import_count].var, 128, "%.*s", vlen, p + pm[1].rm_so); - snprintf(imports[import_count].module, 256, "%.*s", mlen, p + pm[2].rm_so); - import_count++; - } - p += pm[0].rm_eo; - } - - /* Find .use("/prefix", var) calls */ - p = source; - while (cbm_regexec(&use_re, p, 3, pm, 0) == 0) { - char prefix[256] = {0}; - char var_name[128] = {0}; - int plen = (pm[1].rm_eo - pm[1].rm_so); - int vlen = (pm[2].rm_eo - pm[2].rm_so); - if (plen < 256) { - snprintf(prefix, 256, "%.*s", plen, p + pm[1].rm_so); - } - if (vlen < 128) { - snprintf(var_name, 128, "%.*s", vlen, p + pm[2].rm_so); - } - p += pm[0].rm_eo; - - /* Resolve var → module path */ - const char *module_path = NULL; - for (int i = 0; i < import_count; i++) { - if (strcmp(imports[i].var, var_name) == 0) { - module_path = imports[i].module; - break; - } - } - if (!module_path) { - continue; - } - - /* Strip leading ./ and ../ from relative import */ - const char *file_frag = module_path; - if (strncmp(file_frag, "./", 2) == 0) { - file_frag += 2; - } - if (strncmp(file_frag, "../", 3) == 0) { - file_frag += 3; - } - - /* Strip trailing slash from prefix */ - size_t pfx_len = strlen(prefix); - while (pfx_len > 0 && prefix[pfx_len - 1] == '/') { - prefix[--pfx_len] = '\0'; - } - - /* Apply prefix to matching routes */ - for (int r = 0; r < route_count; r++) { - if (strncmp(routes[r].path, prefix, pfx_len) == 0) { - continue; - } - - /* Convert slash-based path to dots for QN matching */ - char dotted_frag[DOTTED_FRAG_BUF]; - snprintf(dotted_frag, sizeof(dotted_frag), "%s", file_frag); - for (char *c = dotted_frag; *c; c++) { - if (*c == '/') { - *c = '.'; - } - } - - if (strstr(routes[r].qualified_name, dotted_frag) || - strstr(routes[r].qualified_name, file_frag)) { - char new_path[256]; - const char *old_path = routes[r].path; - while (*old_path == '/') { - old_path++; - } - snprintf(new_path, sizeof(new_path), "%s/%s", prefix, old_path); - snprintf(routes[r].path, sizeof(routes[r].path), "%s", new_path); - } - } - } - - free(source); + express_process_module(ctx, mod, &use_re, &require_re, &esimport_re, routes, route_count); } cbm_regfree(&use_re); @@ -651,23 +675,18 @@ static void apply_prefix_to_func_routes(cbm_route_handler_t *routes, int route_c } } -/* Resolve Go gin cross-file Group() prefixes. - * Pattern: v1 := r.Group("/api"); RegisterRoutes(v1) */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) -static void resolve_cross_file_group_prefixes(cbm_pipeline_ctx_t *ctx, cbm_route_handler_t *routes, - int route_count) { - /* Build routesByFunc index: funcQN → (start_index, count) in routes array */ - typedef struct { - const char *qn; - int start; - int count; - } func_routes_t; - func_routes_t func_map[1024]; - memset(func_map, 0, sizeof(func_map)); - int func_map_count = 0; +/* Index of routes grouped by function QN. */ +typedef struct { + const char *qn; + int start; + int count; +} hl_func_routes_t; - for (int i = 0; i < route_count && func_map_count < 1024; i++) { - /* Find or create entry */ +/* Build routesByFunc index: funcQN → (start_index, count) in routes array. */ +static int build_func_route_index(const cbm_route_handler_t *routes, int route_count, + hl_func_routes_t *func_map, int max_funcs) { + int func_map_count = 0; + for (int i = 0; i < route_count && func_map_count < max_funcs; i++) { int found = -1; for (int j = 0; j < func_map_count; j++) { if (strcmp(func_map[j].qn, routes[i].qualified_name) == 0) { @@ -683,6 +702,115 @@ static void resolve_cross_file_group_prefixes(cbm_pipeline_ctx_t *ctx, cbm_route func_map_count++; } } + return func_map_count; +} + +/* Group prefix var→prefix mapping. */ +typedef struct { + char var[128]; + char prefix[256]; +} hl_var_prefix_t; + +/* Try direct Group() pattern: FuncName(router.Group("/prefix")) */ +static bool try_direct_group_prefix(const char *source, const cbm_regex_t *re, const char *name, + cbm_route_handler_t *routes, int route_count, + const char *func_qn) { + cbm_regmatch_t pm[3]; + const char *p = source; + while (cbm_regexec(re, p, 3, pm, 0) == 0) { + char called_name[128] = {0}; + char prefix[256] = {0}; + int nlen = (pm[1].rm_eo - pm[1].rm_so); + int plen = (pm[2].rm_eo - pm[2].rm_so); + if (nlen < 128) { + snprintf(called_name, 128, "%.*s", nlen, p + pm[1].rm_so); + } + if (plen < 256) { + snprintf(prefix, 256, "%.*s", plen, p + pm[2].rm_so); + } + p += pm[0].rm_eo; + if (strcmp(called_name, name) == 0) { + apply_prefix_to_func_routes(routes, route_count, func_qn, prefix); + return true; + } + } + return false; +} + +/* Collect var := r.Group("/prefix") assignments from source. */ +static int collect_var_group_prefixes(const char *source, const cbm_regex_t *re, + hl_var_prefix_t *out, int max_out) { + cbm_regmatch_t pm[3]; + int count = 0; + const char *p = source; + while (count < max_out && cbm_regexec(re, p, 3, pm, 0) == 0) { + int vlen = (pm[1].rm_eo - pm[1].rm_so); + int plen = (pm[2].rm_eo - pm[2].rm_so); + if (vlen < 128 && plen < 256) { + snprintf(out[count].var, 128, "%.*s", vlen, p + pm[1].rm_so); + snprintf(out[count].prefix, 256, "%.*s", plen, p + pm[2].rm_so); + count++; + } + p += pm[0].rm_eo; + } + return count; +} + +/* Try indirect Group() pattern: v1 := r.Group("/api"); FuncName(v1) */ +static void try_indirect_group_prefix(const char *source, const cbm_gbuf_node_t *func_node, + const hl_var_prefix_t *vars, int var_count, + cbm_route_handler_t *routes, int route_count, + const char *func_qn) { + char call_pat[256]; + snprintf(call_pat, sizeof(call_pat), "%s[[:space:]]*\\([[:space:]]*([[:alnum:]_]+)", + func_node->name); + cbm_regex_t call_re; + if (cbm_regcomp(&call_re, call_pat, CBM_REG_EXTENDED) != 0) { + return; + } + cbm_regmatch_t pm[3]; + const char *p = source; + while (cbm_regexec(&call_re, p, 2, pm, 0) == 0) { + char arg_name[128] = {0}; + int alen = (pm[1].rm_eo - pm[1].rm_so); + if (alen < 128) { + snprintf(arg_name, 128, "%.*s", alen, p + pm[1].rm_so); + } + p += pm[0].rm_eo; + for (int v = 0; v < var_count; v++) { + if (strcmp(vars[v].var, arg_name) == 0) { + apply_prefix_to_func_routes(routes, route_count, func_qn, vars[v].prefix); + break; + } + } + } + cbm_regfree(&call_re); +} + +static void resolve_group_from_caller(const char *caller_source, const cbm_gbuf_node_t *func_node, + const cbm_regex_t *group_direct_re, + const cbm_regex_t *group_var_re, cbm_route_handler_t *routes, + int route_count, const char *func_qn) { + if (try_direct_group_prefix(caller_source, group_direct_re, func_node->name, routes, + route_count, func_qn)) { + return; + } + hl_var_prefix_t var_pfx[16]; + memset(var_pfx, 0, sizeof(var_pfx)); + int var_count = collect_var_group_prefixes(caller_source, group_var_re, var_pfx, 16); + if (var_count > 0) { + try_indirect_group_prefix(caller_source, func_node, var_pfx, var_count, routes, route_count, + func_qn); + } +} + +/* Resolve Go gin cross-file Group() prefixes. + * Pattern: v1 := r.Group("/api"); RegisterRoutes(v1) */ +static void resolve_cross_file_group_prefixes(cbm_pipeline_ctx_t *ctx, cbm_route_handler_t *routes, + int route_count) { + hl_func_routes_t func_map[1024]; + memset(func_map, 0, sizeof(func_map)); + int func_map_count = build_func_route_index(routes, route_count, func_map, 1024); cbm_regex_t group_direct_re; cbm_regex_t group_var_re; @@ -703,21 +831,16 @@ static void resolve_cross_file_group_prefixes(cbm_pipeline_ctx_t *ctx, cbm_route for (int fi = 0; fi < func_map_count; fi++) { const char *func_qn = func_map[fi].qn; - - /* Find this function node in gbuf */ const cbm_gbuf_node_t *func_node = cbm_gbuf_find_by_qn(ctx->gbuf, func_qn); if (!func_node) { continue; } - /* Find CALLS edges targeting this function */ const cbm_gbuf_edge_t **caller_edges = NULL; int caller_count = 0; if (cbm_gbuf_find_edges_by_target_type(ctx->gbuf, func_node->id, "CALLS", &caller_edges, - &caller_count) != 0) { - continue; - } - if (caller_count == 0) { + &caller_count) != 0 || + caller_count == 0) { continue; } @@ -734,77 +857,8 @@ static void resolve_cross_file_group_prefixes(cbm_pipeline_ctx_t *ctx, cbm_route continue; } - /* Pattern 1: RegisterRoutes(router.Group("/api")) */ - cbm_regmatch_t pm[3]; - const char *p = caller_source; - while (cbm_regexec(&group_direct_re, p, 3, pm, 0) == 0) { - char called_name[128] = {0}; - char prefix[256] = {0}; - int nlen = (pm[1].rm_eo - pm[1].rm_so); - int plen = (pm[2].rm_eo - pm[2].rm_so); - if (nlen < 128) { - snprintf(called_name, 128, "%.*s", nlen, p + pm[1].rm_so); - } - if (plen < 256) { - snprintf(prefix, 256, "%.*s", plen, p + pm[2].rm_so); - } - p += pm[0].rm_eo; - - if (strcmp(called_name, func_node->name) == 0) { - apply_prefix_to_func_routes(routes, route_count, func_qn, prefix); - break; - } - } - - /* Pattern 2: v1 := r.Group("/api"); RegisterRoutes(v1) */ - typedef struct { - char var[128]; - char prefix[256]; - } var_prefix_t; - var_prefix_t var_pfx[16]; - memset(var_pfx, 0, sizeof(var_pfx)); - int var_count = 0; - - p = caller_source; - while (var_count < 16 && cbm_regexec(&group_var_re, p, 3, pm, 0) == 0) { - int vlen = (pm[1].rm_eo - pm[1].rm_so); - int plen = (pm[2].rm_eo - pm[2].rm_so); - if (vlen < 128 && plen < 256) { - snprintf(var_pfx[var_count].var, 128, "%.*s", vlen, p + pm[1].rm_so); - snprintf(var_pfx[var_count].prefix, 256, "%.*s", plen, p + pm[2].rm_so); - var_count++; - } - p += pm[0].rm_eo; - } - - if (var_count > 0) { - /* Build regex: funcName\s*\(\s*(\w+) */ - char call_pat[256]; - snprintf(call_pat, sizeof(call_pat), "%s[[:space:]]*\\([[:space:]]*([[:alnum:]_]+)", - func_node->name); - cbm_regex_t call_re; - if (cbm_regcomp(&call_re, call_pat, CBM_REG_EXTENDED) == 0) { - p = caller_source; - while (cbm_regexec(&call_re, p, 2, pm, 0) == 0) { - char arg_name[128] = {0}; - int alen = (pm[1].rm_eo - pm[1].rm_so); - if (alen < 128) { - snprintf(arg_name, 128, "%.*s", alen, p + pm[1].rm_so); - } - p += pm[0].rm_eo; - - for (int v = 0; v < var_count; v++) { - if (strcmp(var_pfx[v].var, arg_name) == 0) { - apply_prefix_to_func_routes(routes, route_count, func_qn, - var_pfx[v].prefix); - break; - } - } - } - cbm_regfree(&call_re); - } - } - + resolve_group_from_caller(caller_source, func_node, &group_direct_re, &group_var_re, + routes, route_count, func_qn); free(caller_source); } } @@ -864,122 +918,129 @@ static int create_registration_call_edges(cbm_pipeline_ctx_t *ctx, cbm_route_han /* ── Route node insertion ──────────────────────────────────────── */ +/* Snapshot of handler node fields (gbuf pointers go stale after upsert). */ +typedef struct { + char file[512]; + char label[32]; + char name[256]; + char qn[512]; + char props_json[2048]; + int start; + int end; + int64_t id; +} hl_handler_snapshot_t; + +/* Copy handler node fields into a stable snapshot. */ +static void snapshot_handler_node(const cbm_gbuf_node_t *handler_node, hl_handler_snapshot_t *h) { + memset(h, 0, sizeof(*h)); + snprintf(h->props_json, sizeof(h->props_json), "{}"); + if (!handler_node) { + return; + } + if (handler_node->file_path) { + snprintf(h->file, sizeof(h->file), "%s", handler_node->file_path); + } + if (handler_node->label) { + snprintf(h->label, sizeof(h->label), "%s", handler_node->label); + } + if (handler_node->name) { + snprintf(h->name, sizeof(h->name), "%s", handler_node->name); + } + if (handler_node->qualified_name) { + snprintf(h->qn, sizeof(h->qn), "%s", handler_node->qualified_name); + } + if (handler_node->properties_json) { + snprintf(h->props_json, sizeof(h->props_json), "%s", handler_node->properties_json); + } + h->start = handler_node->start_line; + h->end = handler_node->end_line; + h->id = handler_node->id; +} + +/* Build Route node properties JSON with optional protocol detection. */ +static void build_route_props(char *props, size_t props_size, const cbm_route_handler_t *rh, + const char *handler_qn, const hl_handler_snapshot_t *h, + const cbm_pipeline_ctx_t *ctx) { + int n = snprintf(props, props_size, "{\"method\":\"%s\",\"path\":\"%s\",\"handler\":\"%s\"", + rh->method, rh->path, handler_qn); + if (rh->protocol[0]) { + n += snprintf(props + n, props_size - (size_t)n, ",\"protocol\":\"%s\"", rh->protocol); + } else if (h->id > 0 && h->file[0] && h->start > 0) { + char *hsource = read_source_lines(ctx, h->file, h->start, h->end); + if (hsource) { + const char *proto = cbm_detect_protocol(hsource); + if (proto[0]) { + n += snprintf(props + n, props_size - (size_t)n, ",\"protocol\":\"%s\"", proto); + } + free(hsource); + } + } + snprintf(props + n, props_size - (size_t)n, "}"); +} + +/* Build normalized Route QN from method and path. */ +static void build_route_qn(char *route_qn, size_t qn_size, const cbm_route_handler_t *rh) { + char normal_method[16]; + snprintf(normal_method, sizeof(normal_method), "%s", rh->method[0] ? rh->method : "ANY"); + + char normal_path[256]; + snprintf(normal_path, sizeof(normal_path), "%s", rh->path); + for (char *c = normal_path; *c; c++) { + if (*c == '/') { + *c = '_'; + } + } + char *np = normal_path; + while (*np == '_') { + np++; + } + size_t nplen = strlen(np); + while (nplen > 0 && np[nplen - 1] == '_') { + np[--nplen] = '\0'; + } + + snprintf(route_qn, qn_size, "%s.route.%s.%s", rh->qualified_name, normal_method, np); +} + /* Insert Route nodes and HANDLES edges for discovered routes. * Uses ResolvedHandlerQN if set (from createRegistrationCallEdges). */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) static int insert_route_nodes(cbm_pipeline_ctx_t *ctx, cbm_route_handler_t *routes, int route_count) { int count = 0; for (int i = 0; i < route_count; i++) { cbm_route_handler_t *rh = &routes[i]; - /* Build Route QN and name */ + char route_qn[1024]; + build_route_qn(route_qn, sizeof(route_qn), rh); + char normal_method[16]; snprintf(normal_method, sizeof(normal_method), "%s", rh->method[0] ? rh->method : "ANY"); - - /* Normalize path for QN: replace / with _ */ - char normal_path[256]; - snprintf(normal_path, sizeof(normal_path), "%s", rh->path); - for (char *c = normal_path; *c; c++) { - if (*c == '/') { - *c = '_'; - } - } - /* Trim leading/trailing underscores */ - char *np = normal_path; - while (*np == '_') { - np++; - } - size_t nplen = strlen(np); - while (nplen > 0 && np[nplen - 1] == '_') { - np[--nplen] = '\0'; - } - - char route_qn[1024]; - snprintf(route_qn, sizeof(route_qn), "%s.route.%s.%s", rh->qualified_name, normal_method, - np); - char route_name[256]; snprintf(route_name, sizeof(route_name), "%s %s", normal_method, rh->path); - /* Use resolved handler QN if available */ const char *handler_qn = rh->qualified_name; if (rh->resolved_handler_qn[0]) { handler_qn = rh->resolved_handler_qn; } - /* Look up handler node for file_path and line range. - * Copy all needed fields — gbuf pointers go stale after upsert. */ - const cbm_gbuf_node_t *handler_node = cbm_gbuf_find_by_qn(ctx->gbuf, handler_qn); - char h_file[512] = ""; - char h_label[32] = ""; - char h_name[256] = ""; - char h_qn[512] = ""; - char h_props_json[2048] = "{}"; - int h_start = 0; - int h_end = 0; - int64_t h_id = 0; - if (handler_node) { - if (handler_node->file_path) { - snprintf(h_file, sizeof(h_file), "%s", handler_node->file_path); - } - if (handler_node->label) { - snprintf(h_label, sizeof(h_label), "%s", handler_node->label); - } - if (handler_node->name) { - snprintf(h_name, sizeof(h_name), "%s", handler_node->name); - } - if (handler_node->qualified_name) { - snprintf(h_qn, sizeof(h_qn), "%s", handler_node->qualified_name); - } - if (handler_node->properties_json) { - snprintf(h_props_json, sizeof(h_props_json), "%s", handler_node->properties_json); - } - h_start = handler_node->start_line; - h_end = handler_node->end_line; - h_id = handler_node->id; - } - /* handler_node pointer is NOT used below — only the copies above */ + hl_handler_snapshot_t h; + snapshot_handler_node(cbm_gbuf_find_by_qn(ctx->gbuf, handler_qn), &h); - /* Build properties JSON */ char props[512]; - int n = - snprintf(props, sizeof(props), "{\"method\":\"%s\",\"path\":\"%s\",\"handler\":\"%s\"", - rh->method, rh->path, handler_qn); - if (rh->protocol[0]) { - n += snprintf(props + n, sizeof(props) - (size_t)n, ",\"protocol\":\"%s\"", - rh->protocol); - } else if (h_id > 0 && h_file[0] && h_start > 0) { - /* Detect protocol from handler source */ - char *hsource = read_source_lines(ctx, h_file, h_start, h_end); - if (hsource) { - const char *proto = cbm_detect_protocol(hsource); - if (proto[0]) { - n += snprintf(props + n, sizeof(props) - (size_t)n, ",\"protocol\":\"%s\"", - proto); - } - free(hsource); - } - } - snprintf(props + n, sizeof(props) - (size_t)n, "}"); + build_route_props(props, sizeof(props), rh, handler_qn, &h, ctx); - /* Create Route node */ - int64_t route_id = cbm_gbuf_upsert_node(ctx->gbuf, "Route", route_name, route_qn, h_file, - h_start, h_end, props); + int64_t route_id = cbm_gbuf_upsert_node(ctx->gbuf, "Route", route_name, route_qn, h.file, + h.start, h.end, props); if (route_id <= 0) { continue; } - /* Create HANDLES edge: handler → Route */ - if (h_id > 0) { - int64_t source = h_id; - int64_t target = route_id; - cbm_gbuf_insert_edge(ctx->gbuf, source, target, "HANDLES", "{}"); + if (h.id > 0) { + cbm_gbuf_insert_edge(ctx->gbuf, h.id, route_id, "HANDLES", "{}"); - /* Mark handler as entry point */ - char *new_props = set_entry_point(h_props_json); + char *new_props = set_entry_point(h.props_json); if (new_props) { - cbm_gbuf_upsert_node(ctx->gbuf, h_label, h_name, h_qn, h_file, h_start, h_end, + cbm_gbuf_upsert_node(ctx->gbuf, h.label, h.name, h.qn, h.file, h.start, h.end, new_props); free(new_props); } @@ -992,74 +1053,68 @@ static int insert_route_nodes(cbm_pipeline_ctx_t *ctx, cbm_route_handler_t *rout /* ── Match and link ────────────────────────────────────────────── */ +/* Match one call site against all routes and create edges. Returns number of links created. */ +static int match_site_to_routes(cbm_pipeline_ctx_t *ctx, const cbm_http_call_site_t *cs, + const cbm_gbuf_node_t *caller, const cbm_route_handler_t *routes, + int route_count) { + int links = 0; + for (int ri = 0; ri < route_count; ri++) { + const cbm_route_handler_t *rh = &routes[ri]; + + if (cbm_same_service(cs->source_qn, rh->qualified_name)) { + continue; + } + if (cbm_is_path_excluded(rh->path, cbm_default_exclude_paths, + cbm_default_exclude_paths_count)) { + continue; + } + + double score = cbm_path_match_score(cs->path, rh->path); + if (score < MIN_PATH_CONFIDENCE) { + continue; + } + + double weight = (strcmp(cs->source_label, "Module") == 0) ? MODULE_WEIGHT : 1.0; + score *= weight; + if (score > 1.0) { + score = 1.0; + } + + const char *handler_qn = rh->qualified_name; + if (rh->resolved_handler_qn[0]) { + handler_qn = rh->resolved_handler_qn; + } + const cbm_gbuf_node_t *handler = cbm_gbuf_find_by_qn(ctx->gbuf, handler_qn); + if (!handler) { + continue; + } + + const char *edge_type = cs->is_async ? "ASYNC_CALLS" : "HTTP_CALLS"; + const char *band = cbm_confidence_band(score); + + char edge_props[256]; + snprintf(edge_props, sizeof(edge_props), + "{\"url_path\":\"%s\",\"confidence\":%.3f,\"confidence_band\":\"%s\"}", cs->path, + score, band); + + cbm_gbuf_insert_edge(ctx->gbuf, caller->id, handler->id, edge_type, edge_props); + links++; + } + return links; +} + /* Match call sites to routes and create HTTP_CALLS/ASYNC_CALLS edges. */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) static int match_and_link(cbm_pipeline_ctx_t *ctx, cbm_route_handler_t *routes, int route_count, cbm_http_call_site_t *sites, int site_count) { int link_count = 0; for (int si = 0; si < site_count; si++) { const cbm_http_call_site_t *cs = &sites[si]; - - /* Find caller node */ const cbm_gbuf_node_t *caller = cbm_gbuf_find_by_qn(ctx->gbuf, cs->source_qn); if (!caller) { continue; } - - for (int ri = 0; ri < route_count; ri++) { - const cbm_route_handler_t *rh = &routes[ri]; - - /* Skip same-service matches */ - if (cbm_same_service(cs->source_qn, rh->qualified_name)) { - continue; - } - - /* Skip excluded paths */ - if (cbm_is_path_excluded(rh->path, cbm_default_exclude_paths, - cbm_default_exclude_paths_count)) { - continue; - } - - /* Score path match */ - double score = cbm_path_match_score(cs->path, rh->path); - if (score < MIN_PATH_CONFIDENCE) { - continue; /* minimum confidence threshold */ - } - - /* Apply source weight */ - double weight = 1.0; - if (strcmp(cs->source_label, "Module") == 0) { - weight = MODULE_WEIGHT; - } - score *= weight; - - if (score > 1.0) { - score = 1.0; - } - - /* Find handler node */ - const char *handler_qn = rh->qualified_name; - if (rh->resolved_handler_qn[0]) { - handler_qn = rh->resolved_handler_qn; - } - const cbm_gbuf_node_t *handler = cbm_gbuf_find_by_qn(ctx->gbuf, handler_qn); - if (!handler) { - continue; - } - - /* Create edge */ - const char *edge_type = cs->is_async ? "ASYNC_CALLS" : "HTTP_CALLS"; - const char *band = cbm_confidence_band(score); - - char edge_props[256]; - snprintf(edge_props, sizeof(edge_props), - "{\"url_path\":\"%s\",\"confidence\":%.3f,\"confidence_band\":\"%s\"}", - cs->path, score, band); - - cbm_gbuf_insert_edge(ctx->gbuf, caller->id, handler->id, edge_type, edge_props); - link_count++; - } + link_count += match_site_to_routes(ctx, cs, caller, routes, route_count); } return link_count; @@ -1145,7 +1200,59 @@ typedef struct { _Atomic int *cancelled; } hl_site_ctx_t; -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Check if source contains HTTP client or async dispatch keywords. + * Returns: 0 = neither, 1 = http only, 2 = async only, 3 = both. */ +static int check_http_async_keywords(const char *source) { + bool has_http = false; + for (int k = 0; k < cbm_http_client_keywords_count; k++) { + if (strstr(source, cbm_http_client_keywords[k])) { + has_http = true; + break; + } + } + bool has_async = false; + for (int k = 0; k < cbm_async_dispatch_keywords_count; k++) { + if (strstr(source, cbm_async_dispatch_keywords[k])) { + has_async = true; + break; + } + } + return (has_http ? 1 : 0) | (has_async ? 2 : 0); +} + +/* Return true if name looks like a Python dunder method (__xxx__). */ +static bool is_dunder_method(const char *name) { + if (!name) { + return false; + } + size_t len = strlen(name); + return len > 4 && name[0] == '_' && name[1] == '_' && name[len - 1] == '_' && + name[len - 2] == '_'; +} + +/* Store extracted URL paths as call sites in the worker buffer. */ +static void store_site_paths(hl_site_buf_t *buf, const char *source, const cbm_gbuf_node_t *n, + const char *label, bool is_async) { + char *paths[64]; + int path_count = cbm_extract_url_paths(source, paths, 64); + + int space = HL_SITES_PER_WORKER - buf->count; + for (int p = 0; p < path_count && space > 0; p++) { + cbm_http_call_site_t *site = &buf->sites[buf->count]; + snprintf(site->path, sizeof(site->path), "%s", paths[p]); + site->method[0] = '\0'; + snprintf(site->source_name, sizeof(site->source_name), "%s", n->name); + snprintf(site->source_qn, sizeof(site->source_qn), "%s", n->qualified_name); + snprintf(site->source_label, sizeof(site->source_label), "%s", label); + site->is_async = is_async; + buf->count++; + space--; + } + for (int p = 0; p < path_count; p++) { + free(paths[p]); + } +} + static void hl_site_worker(int worker_id, void *arg) { hl_site_ctx_t *sc = arg; hl_site_buf_t *buf = &sc->worker_bufs[worker_id]; @@ -1163,10 +1270,7 @@ static void hl_site_worker(int worker_id, void *arg) { if (!n->file_path || n->start_line <= 0 || n->end_line <= 0) { continue; } - - /* Skip Python dunder methods */ - if (n->name && strlen(n->name) > 4 && n->name[0] == '_' && n->name[1] == '_' && - n->name[strlen(n->name) - 1] == '_' && n->name[strlen(n->name) - 2] == '_') { + if (is_dunder_method(n->name)) { continue; } @@ -1175,57 +1279,145 @@ static void hl_site_worker(int worker_id, void *arg) { continue; } - /* Require at least one HTTP client or async dispatch keyword */ - bool has_http = false; - for (int k = 0; k < cbm_http_client_keywords_count; k++) { - if (strstr(source, cbm_http_client_keywords[k])) { - has_http = true; - break; - } - } - bool has_async = false; - for (int k = 0; k < cbm_async_dispatch_keywords_count; k++) { - if (strstr(source, cbm_async_dispatch_keywords[k])) { - has_async = true; - break; - } - } - - if (!has_http && !has_async) { + int kw = check_http_async_keywords(source); + if (kw == 0) { free(source); continue; } - bool is_async = has_async && !has_http; - - /* Extract URL paths */ - char *paths[64]; - int path_count = cbm_extract_url_paths(source, paths, 64); - - int space = HL_SITES_PER_WORKER - buf->count; - for (int p = 0; p < path_count && space > 0; p++) { - cbm_http_call_site_t *site = &buf->sites[buf->count]; - snprintf(site->path, sizeof(site->path), "%s", paths[p]); - site->method[0] = '\0'; - snprintf(site->source_name, sizeof(site->source_name), "%s", n->name); - snprintf(site->source_qn, sizeof(site->source_qn), "%s", n->qualified_name); - snprintf(site->source_label, sizeof(site->source_label), "%s", sc->labels[idx]); - site->is_async = is_async; - buf->count++; - space--; - } - /* Free any remaining paths we couldn't store */ - for (int p = 0; p < path_count; p++) { - free(paths[p]); - } - + bool is_async = (kw & 2) && !(kw & 1); + store_site_paths(buf, source, n, sc->labels[idx], is_async); free(source); } } /* ── Main pass entry point ─────────────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Collect routes from Function/Method/Module nodes via parallel workers. */ +static int collect_routes_parallel(cbm_pipeline_ctx_t *ctx, const cbm_gbuf_node_t **label_nodes[3], + const int label_counts[3], int worker_count, + cbm_route_handler_t *routes) { + int total_route_nodes = 0; + for (int li = 0; li < 3; li++) { + total_route_nodes += label_counts[li]; + } + + hl_work_item_t *work_items = NULL; + if (total_route_nodes > 0) { + work_items = malloc((size_t)total_route_nodes * sizeof(hl_work_item_t)); + } + + int wi = 0; + if (work_items) { + for (int li = 0; li < 3; li++) { + for (int i = 0; i < label_counts[li]; i++) { + work_items[wi].node = label_nodes[li][i]; + work_items[wi].is_module = (li == 2); + wi++; + } + } + } + + int route_count = 0; + hl_route_buf_t *route_bufs = calloc((size_t)worker_count, sizeof(hl_route_buf_t)); + + if (work_items && route_bufs && wi > 0) { + hl_route_ctx_t rc = { + .items = work_items, + .item_count = wi, + .ctx = ctx, + .worker_bufs = route_bufs, + .worker_count = worker_count, + .cancelled = ctx->cancelled, + }; + atomic_init(&rc.next_idx, 0); + + cbm_parallel_for_opts_t opts = {.max_workers = worker_count, .force_pthreads = false}; + cbm_parallel_for(worker_count, hl_route_worker, &rc, opts); + + for (int w = 0; w < worker_count; w++) { + int to_copy = route_bufs[w].count; + if (to_copy > MAX_ROUTES - route_count) { + to_copy = MAX_ROUTES - route_count; + } + if (to_copy > 0) { + memcpy(routes + route_count, route_bufs[w].routes, + (size_t)to_copy * sizeof(cbm_route_handler_t)); + route_count += to_copy; + } + } + } + free(work_items); + free(route_bufs); + return route_count; +} + +/* Collect HTTP/async call sites from Function/Method nodes via parallel workers. */ +static int collect_sites_parallel(const cbm_pipeline_ctx_t *ctx, + const cbm_gbuf_node_t **label_nodes[3], const int label_counts[3], + int worker_count, cbm_http_call_site_t *sites) { + const char *site_labels[] = {"Function", "Method"}; + int total_site_nodes = 0; + for (int li = 0; li < 2; li++) { + total_site_nodes += label_counts[li]; + } + if (total_site_nodes == 0) { + return 0; + } + + const cbm_gbuf_node_t **all_site_nodes = + malloc((size_t)total_site_nodes * sizeof(cbm_gbuf_node_t *)); + const char **all_site_labels = malloc((size_t)total_site_nodes * sizeof(const char *)); + if (!all_site_nodes || !all_site_labels) { + free(all_site_nodes); + free(all_site_labels); + return 0; + } + + int si2 = 0; + for (int li = 0; li < 2; li++) { + for (int i = 0; i < label_counts[li]; i++) { + all_site_nodes[si2] = label_nodes[li][i]; + all_site_labels[si2] = site_labels[li]; + si2++; + } + } + + int site_count = 0; + hl_site_buf_t *site_bufs = calloc((size_t)worker_count, sizeof(hl_site_buf_t)); + if (site_bufs && si2 > 0) { + hl_site_ctx_t sc = { + .nodes = all_site_nodes, + .labels = all_site_labels, + .node_count = si2, + .ctx = ctx, + .worker_bufs = site_bufs, + .worker_count = worker_count, + .cancelled = ctx->cancelled, + }; + atomic_init(&sc.next_idx, 0); + + cbm_parallel_for_opts_t opts = {.max_workers = worker_count, .force_pthreads = false}; + cbm_parallel_for(worker_count, hl_site_worker, &sc, opts); + + for (int w = 0; w < worker_count; w++) { + int to_copy = site_bufs[w].count; + if (to_copy > MAX_CALL_SITES - site_count) { + to_copy = MAX_CALL_SITES - site_count; + } + if (to_copy > 0) { + memcpy(sites + site_count, site_bufs[w].sites, + (size_t)to_copy * sizeof(cbm_http_call_site_t)); + site_count += to_copy; + } + } + } + free(site_bufs); + free(all_site_nodes); + free(all_site_labels); + return site_count; +} + int cbm_pipeline_pass_httplinks(cbm_pipeline_ctx_t *ctx) { cbm_log_info("pass.start", "pass", "httplinks"); @@ -1239,8 +1431,6 @@ int cbm_pipeline_pass_httplinks(cbm_pipeline_ctx_t *ctx) { } /* ── Phase 1: Route collection from decorator properties + disk ── */ - - /* Need label_counts for fallback path */ const char *route_labels[] = {"Function", "Method", "Module"}; const cbm_gbuf_node_t **label_nodes[3] = {NULL, NULL, NULL}; int label_counts[3] = {0, 0, 0}; @@ -1253,66 +1443,7 @@ int cbm_pipeline_pass_httplinks(cbm_pipeline_ctx_t *ctx) { if (!routes) { return -1; } - int route_count = 0; - - { - /* Route discovery: decorator-based (from gbuf properties) + disk-based - * (Go/Express/Laravel/Ktor source reading). AST-based route registration - * detection (CBM_SVC_ROUTE_REG) creates Route+HANDLES during resolution, - * so this path is primarily for decorator-discovered routes now. */ - int total_route_nodes = 0; - for (int li = 0; li < 3; li++) { - total_route_nodes += label_counts[li]; - } - - hl_work_item_t *work_items = NULL; - if (total_route_nodes > 0) { - work_items = malloc((size_t)total_route_nodes * sizeof(hl_work_item_t)); - } - - int wi = 0; - if (work_items) { - for (int li = 0; li < 3; li++) { - for (int i = 0; i < label_counts[li]; i++) { - work_items[wi].node = label_nodes[li][i]; - work_items[wi].is_module = (li == 2); - wi++; - } - } - } - - hl_route_buf_t *route_bufs = calloc((size_t)worker_count, sizeof(hl_route_buf_t)); - - if (work_items && route_bufs && wi > 0) { - hl_route_ctx_t rc = { - .items = work_items, - .item_count = wi, - .ctx = ctx, - .worker_bufs = route_bufs, - .worker_count = worker_count, - .cancelled = ctx->cancelled, - }; - atomic_init(&rc.next_idx, 0); - - cbm_parallel_for_opts_t opts = {.max_workers = worker_count, .force_pthreads = false}; - cbm_parallel_for(worker_count, hl_route_worker, &rc, opts); - - for (int w = 0; w < worker_count; w++) { - int to_copy = route_bufs[w].count; - if (to_copy > MAX_ROUTES - route_count) { - to_copy = MAX_ROUTES - route_count; - } - if (to_copy > 0) { - memcpy(routes + route_count, route_bufs[w].routes, - (size_t)to_copy * sizeof(cbm_route_handler_t)); - route_count += to_copy; - } - } - } - free(work_items); - free(route_bufs); - } - + int route_count = collect_routes_parallel(ctx, label_nodes, label_counts, worker_count, routes); cbm_log_info("httplink.routes", "count", itoa_hl(route_count)); /* ── Phase 2: Resolve cross-file prefixes (serial) ────────── */ @@ -1330,74 +1461,11 @@ int cbm_pipeline_pass_httplinks(cbm_pipeline_ctx_t *ctx) { int route_nodes = insert_route_nodes(ctx, routes, route_count); /* ── Phase 5: Call site collection ──────────────────────────── */ - cbm_http_call_site_t *sites = calloc(MAX_CALL_SITES, sizeof(cbm_http_call_site_t)); int site_count = 0; - if (sites) { - /* Call site collection: parallel disk-reading approach. - * Note: HTTP_CALLS/ASYNC_CALLS edges are now also created during - * resolution via service_patterns. This path provides additional - * URL-based matching for call sites not caught by QN matching. */ - const char *site_labels[] = {"Function", "Method"}; - const cbm_gbuf_node_t **all_site_nodes = NULL; - const char **all_site_labels = NULL; - int total_site_nodes = 0; - - for (int li = 0; li < 2; li++) { - total_site_nodes += label_counts[li]; - } - - if (total_site_nodes > 0) { - all_site_nodes = malloc((size_t)total_site_nodes * sizeof(cbm_gbuf_node_t *)); - all_site_labels = malloc((size_t)total_site_nodes * sizeof(const char *)); - } - - if (all_site_nodes && all_site_labels) { - int si2 = 0; - for (int li = 0; li < 2; li++) { - for (int i = 0; i < label_counts[li]; i++) { - all_site_nodes[si2] = label_nodes[li][i]; - all_site_labels[si2] = site_labels[li]; - si2++; - } - } - - hl_site_buf_t *site_bufs = calloc((size_t)worker_count, sizeof(hl_site_buf_t)); - if (site_bufs && si2 > 0) { - hl_site_ctx_t sc = { - .nodes = all_site_nodes, - .labels = all_site_labels, - .node_count = si2, - .ctx = ctx, - .worker_bufs = site_bufs, - .worker_count = worker_count, - .cancelled = ctx->cancelled, - }; - atomic_init(&sc.next_idx, 0); - - cbm_parallel_for_opts_t opts = {.max_workers = worker_count, - .force_pthreads = false}; - cbm_parallel_for(worker_count, hl_site_worker, &sc, opts); - - for (int w = 0; w < worker_count; w++) { - int to_copy = site_bufs[w].count; - if (to_copy > MAX_CALL_SITES - site_count) { - to_copy = MAX_CALL_SITES - site_count; - } - if (to_copy > 0) { - memcpy(sites + site_count, site_bufs[w].sites, - (size_t)to_copy * sizeof(cbm_http_call_site_t)); - site_count += to_copy; - } - } - } - free(site_bufs); - } - free(all_site_nodes); - free(all_site_labels); + site_count = collect_sites_parallel(ctx, label_nodes, label_counts, worker_count, sites); } - cbm_log_info("httplink.callsites", "count", itoa_hl(site_count)); /* ── Phase 6: Match and link (serial) ─────────────────────── */ diff --git a/src/pipeline/pass_infrascan.c b/src/pipeline/pass_infrascan.c index 5fd8945c3..5dcd21984 100644 --- a/src/pipeline/pass_infrascan.c +++ b/src/pipeline/pass_infrascan.c @@ -297,7 +297,34 @@ bool cbm_is_secret_binding(const char *key, const char *value) { /* ── Clean JSON brackets ────────────────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Strip JSON array brackets, quotes, and normalize whitespace. */ +static void clean_json_array_inner(const char *s, size_t len, char *out, size_t out_sz) { + size_t pos = 0; + bool in_space = false; + for (size_t i = 1; i < len - 1 && pos < out_sz - 1; i++) { + char c = s[i]; + if (c == '"') { + continue; + } + if (c == ',') { + c = ' '; + } + if (c == ' ' || c == '\t') { + if (!in_space && pos > 0) { + out[pos++] = ' '; + in_space = true; + } + } else { + out[pos++] = c; + in_space = false; + } + } + while (pos > 0 && out[pos - 1] == ' ') { + pos--; + } + out[pos] = '\0'; +} + void cbm_clean_json_brackets(const char *s, char *out, size_t out_sz) { if (!s || !out || out_sz == 0) { return; @@ -305,31 +332,7 @@ void cbm_clean_json_brackets(const char *s, char *out, size_t out_sz) { size_t len = strlen(s); if (len >= 2 && s[0] == '[' && s[len - 1] == ']') { - size_t pos = 0; - bool in_space = false; - for (size_t i = 1; i < len - 1 && pos < out_sz - 1; i++) { - char c = s[i]; - if (c == '"') { - continue; /* strip quotes */ - } - if (c == ',') { - c = ' '; /* commas → spaces */ - } - if (c == ' ' || c == '\t') { - if (!in_space && pos > 0) { - out[pos++] = ' '; - in_space = true; - } - } else { - out[pos++] = c; - in_space = false; - } - } - /* Trim trailing space */ - while (pos > 0 && out[pos - 1] == ' ') { - pos--; - } - out[pos] = '\0'; + clean_json_array_inner(s, len, out, out_sz); } else { snprintf(out, out_sz, "%s", s); } @@ -467,46 +470,40 @@ static void df_parse_arg(const char *line, cbm_dockerfile_result_t *r) { } } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Parse a Dockerfile CMD/ENTRYPOINT value: strip brackets, trim. */ +static void df_parse_json_cmd(const char *line, int prefix_len, char *out, size_t out_sz) { + const char *p = skip_ws(line + prefix_len); + char raw[512]; + snprintf(raw, sizeof(raw), "%s", p); + rtrim(raw); + cbm_clean_json_brackets(raw, out, out_sz); +} + static void df_parse_directives(const char *line, cbm_dockerfile_result_t *r) { - /* WORKDIR */ if (strncasecmp(line, "WORKDIR", 7) == 0 && (line[7] == ' ' || line[7] == '\t')) { const char *p = skip_ws(line + 8); snprintf(r->workdir, sizeof(r->workdir), "%s", p); rtrim(r->workdir); return; } - /* CMD */ if (strncasecmp(line, "CMD", 3) == 0 && (line[3] == ' ' || line[3] == '\t')) { - const char *p = skip_ws(line + 4); - char raw[512]; - snprintf(raw, sizeof(raw), "%s", p); - rtrim(raw); - cbm_clean_json_brackets(raw, r->cmd, sizeof(r->cmd)); + df_parse_json_cmd(line, 4, r->cmd, sizeof(r->cmd)); return; } - /* ENTRYPOINT */ if (strncasecmp(line, "ENTRYPOINT", 10) == 0 && (line[10] == ' ' || line[10] == '\t')) { - const char *p = skip_ws(line + LEN_HEALTHCHECK); - char raw[512]; - snprintf(raw, sizeof(raw), "%s", p); - rtrim(raw); - cbm_clean_json_brackets(raw, r->entrypoint, sizeof(r->entrypoint)); + df_parse_json_cmd(line, LEN_HEALTHCHECK, r->entrypoint, sizeof(r->entrypoint)); return; } - /* USER */ if (strncasecmp(line, "USER", 4) == 0 && (line[4] == ' ' || line[4] == '\t')) { const char *p = skip_ws(line + 5); extract_word(p, r->user, sizeof(r->user)); return; } - /* HEALTHCHECK ... CMD */ if (strncasecmp(line, "HEALTHCHECK", LEN_HEALTHCHECK) == 0 && (line[LEN_HEALTHCHECK] == ' ' || line[LEN_HEALTHCHECK] == '\t')) { const char *cmd_pos = ci_strstr(line + LEN_HEALTHCHECK, "CMD"); if (cmd_pos) { - cmd_pos += 3; - cmd_pos = skip_ws(cmd_pos); + cmd_pos = skip_ws(cmd_pos + 3); snprintf(r->healthcheck, sizeof(r->healthcheck), "%s", cmd_pos); rtrim(r->healthcheck); } @@ -570,7 +567,41 @@ int cbm_parse_dockerfile_source(const char *source, cbm_dockerfile_result_t *out /* ── Dotenv parser ──────────────────────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Strip matching surrounding quotes from a value string in-place. */ +static void strip_surrounding_quotes(char *value) { + size_t vlen = strlen(value); + if (vlen >= 2 && ((value[0] == '"' && value[vlen - 1] == '"') || + (value[0] == '\'' && value[vlen - 1] == '\''))) { + memmove(value, value + 1, vlen - 2); + value[vlen - 2] = '\0'; + } +} + +/* Parse a single dotenv line: KEY=VALUE. Returns 1 if env var added. */ +static int parse_dotenv_line(const char *trimmed, cbm_dotenv_result_t *out) { + char key[128]; + int klen = 0; + if (isalpha((unsigned char)trimmed[0]) || trimmed[0] == '_') { + klen = extract_word(trimmed, key, sizeof(key)); + } + if (klen == 0 || trimmed[klen] != '=') { + return 0; + } + + char value[512]; + snprintf(value, sizeof(value), "%s", trimmed + klen + 1); + rtrim(value); + strip_surrounding_quotes(value); + + if (cbm_is_secret_binding(key, value) || out->env_count >= 64) { + return 0; + } + snprintf(out->env_vars[out->env_count].key, sizeof(out->env_vars[0].key), "%s", key); + snprintf(out->env_vars[out->env_count].value, sizeof(out->env_vars[0].value), "%s", value); + out->env_count++; + return 1; +} + int cbm_parse_dotenv_source(const char *source, cbm_dotenv_result_t *out) { if (!source || !out) { return -1; @@ -600,41 +631,7 @@ int cbm_parse_dotenv_source(const char *source, cbm_dotenv_result_t *out) { continue; } - /* Match: KEY=VALUE where KEY starts with [A-Za-z_] */ - char key[128]; - int klen = 0; - if (isalpha((unsigned char)trimmed[0]) || trimmed[0] == '_') { - klen = extract_word(trimmed, key, sizeof(key)); - } - if (klen == 0 || trimmed[klen] != '=') { - continue; - } - - /* Value: strip surrounding quotes */ - const char *val_start = trimmed + klen + 1; - char value[512]; - snprintf(value, sizeof(value), "%s", val_start); - rtrim(value); - - /* Strip matching quotes */ - size_t vlen = strlen(value); - if (vlen >= 2 && ((value[0] == '"' && value[vlen - 1] == '"') || - (value[0] == '\'' && value[vlen - 1] == '\''))) { - memmove(value, value + 1, vlen - 2); - value[vlen - 2] = '\0'; - } - - /* Filter secrets */ - if (cbm_is_secret_binding(key, value)) { - continue; - } - - if (out->env_count >= 64) { - continue; - } - snprintf(out->env_vars[out->env_count].key, sizeof(out->env_vars[0].key), "%s", key); - snprintf(out->env_vars[out->env_count].value, sizeof(out->env_vars[0].value), "%s", value); - out->env_count++; + parse_dotenv_line(trimmed, out); } return (out->env_count > 0) ? 0 : -1; @@ -782,7 +779,41 @@ static void shell_parse_docker(const char *line, cbm_shell_result_t *r) { } } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Check for shebang on first non-empty line. Returns true if line was consumed. */ +static bool shell_try_shebang(const char *trimmed, cbm_shell_result_t *out, bool *shebang_checked) { + if (*shebang_checked) { + return false; + } + if (trimmed[0] == '#' && trimmed[1] == '!') { + const char *sb = trimmed + 2; + while (*sb == ' ') { + sb++; + } + snprintf(out->shebang, sizeof(out->shebang), "%s", sb); + rtrim(out->shebang); + *shebang_checked = true; + return true; + } + *shebang_checked = true; + return false; +} + +/* Dispatch a single trimmed shell line to the appropriate parser. */ +static void shell_dispatch_line(const char *trimmed, cbm_shell_result_t *out) { + if (strncmp(trimmed, "export", 6) == 0) { + shell_parse_export(trimmed, out); + return; + } + if (strncmp(trimmed, "source", 6) == 0 || (trimmed[0] == '.' && trimmed[1] == ' ')) { + shell_parse_source(trimmed, out); + return; + } + if (strncmp(trimmed, "docker", 6) == 0) { + shell_parse_docker(trimmed, out); + } + shell_parse_plain_var(trimmed, out); +} + int cbm_parse_shell_source(const char *source, cbm_shell_result_t *out) { if (!source || !out) { return -1; @@ -813,50 +844,17 @@ int cbm_parse_shell_source(const char *source, cbm_shell_result_t *out) { continue; } - /* Check shebang (first non-empty line starting with #!) */ - if (!shebang_checked && out->shebang[0] == '\0') { - if (trimmed[0] == '#' && trimmed[1] == '!') { - const char *sb = trimmed + 2; - while (*sb == ' ') { - sb++; - } - snprintf(out->shebang, sizeof(out->shebang), "%s", sb); - rtrim(out->shebang); - shebang_checked = true; - continue; - } - shebang_checked = true; + if (shell_try_shebang(trimmed, out, &shebang_checked)) { + continue; } - /* Skip comments */ if (*trimmed == '#') { continue; } - /* Try export first */ - if (strncmp(trimmed, "export", 6) == 0) { - shell_parse_export(trimmed, out); - continue; - } - - /* Try source/dot */ - if (strncmp(trimmed, "source", 6) == 0 || (trimmed[0] == '.' && trimmed[1] == ' ')) { - shell_parse_source(trimmed, out); - continue; - } - - /* Try docker commands */ - if (strncmp(trimmed, "docker", 6) == 0) { - shell_parse_docker(trimmed, out); - } - - /* Try plain var (only if no spaces) */ - shell_parse_plain_var(trimmed, out); + shell_dispatch_line(trimmed, out); } - /* Result is non-empty if we have more than just infra_type. - * In Go: len(props) > 1, where props always has infra_type. - * Here: check if we have shebang OR env_vars OR sources OR docker_cmds. */ bool has_content = out->shebang[0] != '\0' || out->env_count > 0 || out->source_count > 0 || out->docker_cmd_count > 0; return has_content ? 0 : -1; @@ -937,7 +935,203 @@ static int tf_extract_ident(const char *p, const char *keyword, char *out, size_ return (int)(end - p + 1); } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Parse a second quoted string after the first ident (for resource/data blocks). */ +static void tf_extract_second_quoted(const char *trimmed, int consumed, char *out, size_t outsz) { + const char *rest = skip_ws(trimmed + consumed); + out[0] = '\0'; + if (*rest == '"') { + rest++; + const char *end = strchr(rest, '"'); + if (end) { + size_t len = (size_t)(end - rest); + if (len >= outsz) { + len = outsz - 1; + } + memcpy(out, rest, len); + out[len] = '\0'; + } + } +} + +/* Try to parse a simple "keyword ident" block (variable, output, provider). */ +static bool tf_try_simple_block(const char *trimmed, const char *keyword, char *names, int name_sz, + int *count, int max_count) { + char ident[128]; + int consumed = tf_extract_ident(trimmed, keyword, ident, sizeof(ident)); + if (consumed <= 0 || *count >= max_count) { + return false; + } + snprintf(names + ((size_t)(*count) * (size_t)name_sz), name_sz, "%s", ident); + (*count)++; + return true; +} + +/* Try to match terraform/locals keyword blocks. */ +static bool tf_try_keyword_block(const char *trimmed, tf_block_kind_t *cur_block, + cbm_terraform_result_t *out) { + if (strncmp(trimmed, "terraform", 9) == 0 && strstr(trimmed, "{")) { + *cur_block = TF_BLOCK_TERRAFORM; + return true; + } + if (strncmp(trimmed, "locals", 6) == 0 && strstr(trimmed, "{")) { + out->has_locals = true; + return true; + } + return false; +} + +/* Detect a top-level block header. Returns true if line was consumed as a header. */ +static bool tf_detect_block_header(const char *trimmed, cbm_terraform_result_t *out, + tf_block_kind_t *cur_block, int *cur_var_idx, int *cur_mod_idx) { + char ident1[128]; + + /* resource "type" "name" */ + { + int rc = tf_extract_ident(trimmed, "resource", ident1, sizeof(ident1)); + if (rc > 0 && out->resource_count < 32) { + char ident2[128]; + tf_extract_second_quoted(trimmed, rc, ident2, sizeof(ident2)); + snprintf(out->resources[out->resource_count].type, sizeof(out->resources[0].type), "%s", + ident1); + snprintf(out->resources[out->resource_count].name, sizeof(out->resources[0].name), "%s", + ident2); + out->resource_count++; + return true; + } + } + + /* variable "name" */ + int consumed = tf_extract_ident(trimmed, "variable", ident1, sizeof(ident1)); + if (consumed > 0 && out->variable_count < 32) { + *cur_block = TF_BLOCK_VARIABLE; + *cur_var_idx = out->variable_count; + snprintf(out->variables[*cur_var_idx].name, sizeof(out->variables[0].name), "%s", ident1); + out->variable_count++; + return true; + } + + /* output "name" */ + if (tf_try_simple_block(trimmed, "output", out->outputs[0], (int)sizeof(out->outputs[0]), + &out->output_count, 32)) { + return true; + } + + /* provider "name" */ + if (tf_try_simple_block(trimmed, "provider", out->providers[0], (int)sizeof(out->providers[0]), + &out->provider_count, 16)) { + return true; + } + + /* module "name" */ + consumed = tf_extract_ident(trimmed, "module", ident1, sizeof(ident1)); + if (consumed > 0 && out->module_count < 16) { + *cur_block = TF_BLOCK_MODULE; + *cur_mod_idx = out->module_count; + snprintf(out->modules[*cur_mod_idx].tf_name, sizeof(out->modules[0].tf_name), "%s", ident1); + out->module_count++; + return true; + } + + /* data "type" "name" */ + { + int dc = tf_extract_ident(trimmed, "data", ident1, sizeof(ident1)); + if (dc > 0 && out->data_source_count < 16) { + char ident2[128]; + tf_extract_second_quoted(trimmed, dc, ident2, sizeof(ident2)); + snprintf(out->data_sources[out->data_source_count].type, + sizeof(out->data_sources[0].type), "%s", ident1); + snprintf(out->data_sources[out->data_source_count].name, + sizeof(out->data_sources[0].name), "%s", ident2); + out->data_source_count++; + return true; + } + } + + return tf_try_keyword_block(trimmed, cur_block, out); +} + +/* Extract attributes from a line inside a variable block. */ +static void tf_parse_variable_attrs(const char *trimmed, cbm_tf_variable_t *v) { + char def_val[256] = {0}; + tf_extract_quoted(trimmed, "default", def_val, sizeof(def_val)); + if (def_val[0] && !cbm_is_secret_binding(v->name, def_val)) { + snprintf(v->default_val, sizeof(v->default_val), "%s", def_val); + } + char type_val[64] = {0}; + tf_extract_quoted(trimmed, "type", type_val, sizeof(type_val)); + if (type_val[0]) { + snprintf(v->type, sizeof(v->type), "%s", type_val); + } + char desc_val[256] = {0}; + tf_extract_quoted(trimmed, "description", desc_val, sizeof(desc_val)); + if (desc_val[0]) { + snprintf(v->description, sizeof(v->description), "%s", desc_val); + } +} + +/* Extract attributes inside a block (variable/module/terraform). */ +static void tf_parse_block_attrs(const char *trimmed, cbm_terraform_result_t *out, + tf_block_kind_t cur_block, int cur_var_idx, int cur_mod_idx) { + switch (cur_block) { + case TF_BLOCK_VARIABLE: + if (cur_var_idx >= 0) { + tf_parse_variable_attrs(trimmed, &out->variables[cur_var_idx]); + } + break; + case TF_BLOCK_MODULE: + if (cur_mod_idx >= 0) { + char src_val[256] = {0}; + tf_extract_quoted(trimmed, "source", src_val, sizeof(src_val)); + if (src_val[0]) { + snprintf(out->modules[cur_mod_idx].source, sizeof(out->modules[0].source), "%s", + src_val); + } + } + break; + case TF_BLOCK_TERRAFORM: { + const char *bp = skip_ws(trimmed); + char backend_name[128] = {0}; + tf_extract_ident(bp, "backend", backend_name, sizeof(backend_name)); + if (backend_name[0]) { + snprintf(out->backend, sizeof(out->backend), "%s", backend_name); + } + break; + } + default: + break; + } +} + +/* Check if any terraform content was parsed. */ +static bool tf_result_has_content(const cbm_terraform_result_t *out) { + return out->resource_count > 0 || out->variable_count > 0 || out->output_count > 0 || + out->provider_count > 0 || out->module_count > 0 || out->data_source_count > 0 || + out->backend[0] != '\0' || out->has_locals; +} + +/* Read next non-empty, non-comment line. Advances *pp. Returns trimmed or NULL. */ +static char *tf_next_line(const char **pp, char *line, size_t line_sz) { + const char *p = *pp; + const char *eol = strchr(p, '\n'); + size_t len = eol ? (size_t)(eol - p) : strlen(p); + if (len >= line_sz) { + len = line_sz - 1; + } + memcpy(line, p, len); + line[len] = '\0'; + *pp = eol ? eol + 1 : p + len; + + char *trimmed = line; + while (*trimmed == ' ' || *trimmed == '\t') { + trimmed++; + } + rtrim(trimmed); + if (*trimmed == '\0' || *trimmed == '#' || (trimmed[0] == '/' && trimmed[1] == '/')) { + return NULL; + } + return trimmed; +} + int cbm_parse_terraform_source(const char *source, cbm_terraform_result_t *out) { if (!source || !out) { return -1; @@ -948,198 +1142,27 @@ int cbm_parse_terraform_source(const char *source, cbm_terraform_result_t *out) char line[4096]; int brace_depth = 0; tf_block_kind_t cur_block = TF_BLOCK_NONE; - - /* Current block data indices (for variable/module accumulation) */ int cur_var_idx = -1; int cur_mod_idx = -1; while (*p) { - const char *eol = strchr(p, '\n'); - size_t line_len = eol ? (size_t)(eol - p) : strlen(p); - if (line_len >= sizeof(line)) { - line_len = sizeof(line) - 1; - } - memcpy(line, p, line_len); - line[line_len] = '\0'; - p = eol ? eol + 1 : p + line_len; - - char *trimmed = line; - while (*trimmed == ' ' || *trimmed == '\t') { - trimmed++; - } - rtrim(trimmed); - - if (*trimmed == '\0' || *trimmed == '#' || (trimmed[0] == '/' && trimmed[1] == '/')) { + char *trimmed = tf_next_line(&p, line, sizeof(line)); + if (!trimmed) { continue; } - /* Track brace depth */ brace_depth += count_char(trimmed, '{') - count_char(trimmed, '}'); - /* At top level (depth <= 1), detect block headers */ if (brace_depth <= 1 && cur_block == TF_BLOCK_NONE) { - char ident1[128]; - char ident2[128]; - - /* resource "type" "name" */ - int consumed = tf_extract_ident(trimmed, "resource", ident1, sizeof(ident1)); - if (consumed > 0) { - const char *rest = skip_ws(trimmed + consumed); - tf_extract_ident(rest, "", ident2, sizeof(ident2)); - /* Actually need to parse second quoted string directly */ - if (*rest == '"') { - rest++; - const char *end = strchr(rest, '"'); - if (end) { - size_t len = (size_t)(end - rest); - if (len >= sizeof(ident2)) { - len = sizeof(ident2) - 1; - } - memcpy(ident2, rest, len); - ident2[len] = '\0'; - } - } - if (out->resource_count < 32) { - snprintf(out->resources[out->resource_count].type, - sizeof(out->resources[0].type), "%s", ident1); - snprintf(out->resources[out->resource_count].name, - sizeof(out->resources[0].name), "%s", ident2); - out->resource_count++; - } - continue; - } - - /* variable "name" */ - consumed = tf_extract_ident(trimmed, "variable", ident1, sizeof(ident1)); - if (consumed > 0 && out->variable_count < 32) { - cur_block = TF_BLOCK_VARIABLE; - cur_var_idx = out->variable_count; - snprintf(out->variables[cur_var_idx].name, sizeof(out->variables[0].name), "%s", - ident1); - out->variable_count++; - continue; - } - - /* output "name" */ - consumed = tf_extract_ident(trimmed, "output", ident1, sizeof(ident1)); - if (consumed > 0 && out->output_count < 32) { - snprintf(out->outputs[out->output_count], sizeof(out->outputs[0]), "%s", ident1); - out->output_count++; - continue; - } - - /* provider "name" */ - consumed = tf_extract_ident(trimmed, "provider", ident1, sizeof(ident1)); - if (consumed > 0 && out->provider_count < 16) { - snprintf(out->providers[out->provider_count], sizeof(out->providers[0]), "%s", - ident1); - out->provider_count++; - continue; - } - - /* module "name" */ - consumed = tf_extract_ident(trimmed, "module", ident1, sizeof(ident1)); - if (consumed > 0 && out->module_count < 16) { - cur_block = TF_BLOCK_MODULE; - cur_mod_idx = out->module_count; - snprintf(out->modules[cur_mod_idx].tf_name, sizeof(out->modules[0].tf_name), "%s", - ident1); - out->module_count++; - continue; - } - - /* data "type" "name" */ - consumed = tf_extract_ident(trimmed, "data", ident1, sizeof(ident1)); - if (consumed > 0) { - const char *rest = skip_ws(trimmed + consumed); - ident2[0] = '\0'; - if (*rest == '"') { - rest++; - const char *end = strchr(rest, '"'); - if (end) { - size_t len = (size_t)(end - rest); - if (len >= sizeof(ident2)) { - len = sizeof(ident2) - 1; - } - memcpy(ident2, rest, len); - ident2[len] = '\0'; - } - } - if (out->data_source_count < 16) { - snprintf(out->data_sources[out->data_source_count].type, - sizeof(out->data_sources[0].type), "%s", ident1); - snprintf(out->data_sources[out->data_source_count].name, - sizeof(out->data_sources[0].name), "%s", ident2); - out->data_source_count++; - } - continue; - } - - /* terraform { */ - if (strncmp(trimmed, "terraform", 9) == 0 && strstr(trimmed, "{")) { - cur_block = TF_BLOCK_TERRAFORM; - continue; - } - - /* locals { */ - if (strncmp(trimmed, "locals", 6) == 0 && strstr(trimmed, "{")) { - out->has_locals = true; + if (tf_detect_block_header(trimmed, out, &cur_block, &cur_var_idx, &cur_mod_idx)) { continue; } } - /* Inside a block, extract attributes (depth 1-2) */ if (cur_block != TF_BLOCK_NONE && brace_depth >= 1 && brace_depth <= 2) { - switch (cur_block) { - case TF_BLOCK_VARIABLE: - if (cur_var_idx >= 0) { - cbm_tf_variable_t *v = &out->variables[cur_var_idx]; - /* default = "value" */ - char def_val[256] = {0}; - tf_extract_quoted(trimmed, "default", def_val, sizeof(def_val)); - if (def_val[0] && !cbm_is_secret_binding(v->name, def_val)) { - snprintf(v->default_val, sizeof(v->default_val), "%s", def_val); - } - /* type = */ - char type_val[64] = {0}; - tf_extract_quoted(trimmed, "type", type_val, sizeof(type_val)); - if (type_val[0]) { - snprintf(v->type, sizeof(v->type), "%s", type_val); - } - /* description = "desc" */ - char desc_val[256] = {0}; - tf_extract_quoted(trimmed, "description", desc_val, sizeof(desc_val)); - if (desc_val[0]) { - snprintf(v->description, sizeof(v->description), "%s", desc_val); - } - } - break; - case TF_BLOCK_MODULE: - if (cur_mod_idx >= 0) { - char src_val[256] = {0}; - tf_extract_quoted(trimmed, "source", src_val, sizeof(src_val)); - if (src_val[0]) { - snprintf(out->modules[cur_mod_idx].source, sizeof(out->modules[0].source), - "%s", src_val); - } - } - break; - case TF_BLOCK_TERRAFORM: { - /* backend "name" */ - const char *bp = skip_ws(trimmed); - char backend_name[128] = {0}; - tf_extract_ident(bp, "backend", backend_name, sizeof(backend_name)); - if (backend_name[0]) { - snprintf(out->backend, sizeof(out->backend), "%s", backend_name); - } - break; - } - default: - break; - } + tf_parse_block_attrs(trimmed, out, cur_block, cur_var_idx, cur_mod_idx); } - /* Block closed */ if (brace_depth == 0 && cur_block != TF_BLOCK_NONE) { cur_block = TF_BLOCK_NONE; cur_var_idx = -1; @@ -1147,11 +1170,7 @@ int cbm_parse_terraform_source(const char *source, cbm_terraform_result_t *out) } } - /* Check if we extracted anything useful */ - bool has_content = out->resource_count > 0 || out->variable_count > 0 || - out->output_count > 0 || out->provider_count > 0 || out->module_count > 0 || - out->data_source_count > 0 || out->backend[0] != '\0' || out->has_locals; - return has_content ? 0 : -1; + return tf_result_has_content(out) ? 0 : -1; } /* ── Infra QN helper ────────────────────────────────────────────── */ diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index df6762623..8fcf9706b 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -497,7 +497,7 @@ static void extract_worker(int worker_id, void *ctx_ptr) { cbm_slab_destroy_thread(); } -int cbm_parallel_extract(cbm_pipeline_ctx_t *ctx, cbm_file_info_t *files, int file_count, +int cbm_parallel_extract(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, int file_count, CBMFileResult **result_cache, _Atomic int64_t *shared_ids, int worker_count) { if (file_count == 0) { diff --git a/src/pipeline/pass_route_nodes.c b/src/pipeline/pass_route_nodes.c index 9fd16b833..79ed21680 100644 --- a/src/pipeline/pass_route_nodes.c +++ b/src/pipeline/pass_route_nodes.c @@ -121,9 +121,41 @@ static const char *url_path(const char *url) { return path ? path : "/"; } +/* Check if a trailing dash-segment is a hash/project-number (strippable). + * Returns true if the segment is short alphanumeric but not a meaningful word. */ +static bool is_hash_segment(const char *seg, size_t slen) { + if (slen > 12 || slen == 0) { + return false; + } + int has_letter = 0; + for (size_t si = 0; si < slen; si++) { + if (!((seg[si] >= '0' && seg[si] <= '9') || (seg[si] >= 'a' && seg[si] <= 'z'))) { + return false; + } + if (seg[si] >= 'a' && seg[si] <= 'z') { + has_letter = 1; + } + } + /* Don't strip meaningful words (>=3 chars with letters like "api", "endpoint") */ + return !(has_letter && slen >= 3); +} + +/* Strip up to 2 trailing hash/project-number segments from a Cloud Run hostname. */ +static void strip_cloud_run_suffixes(char *hostname) { + for (int strip = 0; strip < 2; strip++) { + char *last_dash = strrchr(hostname, '-'); + if (!last_dash) { + break; + } + if (!is_hash_segment(last_dash + 1, strlen(last_dash + 1))) { + break; + } + *last_dash = '\0'; + } +} + /* Extract service name from Cloud Run URL hostname. * "my-svc-ab12cd34ef-uc.a.run.app/path" → "my-svc" */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) static const char *extract_service_name(const char *url, char *buf, int bufsz) { if (!url) { return NULL; @@ -133,22 +165,15 @@ static const char *extract_service_name(const char *url, char *buf, int bufsz) { return NULL; } const char *host_start = scheme_end + 3; - /* Service name is everything before the revision hash. - * Pattern: service-name-HASH-HASH.region.run.app - * Heuristic: find the longest prefix of dash-separated words - * before a segment that looks like a hash (short alphanumeric). */ const char *end = host_start; while (*end && *end != '.' && *end != '/') { end++; } - /* Copy full hostname part before first dot */ int hlen = (int)(end - host_start); if (hlen <= 0 || hlen >= bufsz) { return NULL; } - /* Walk backward from end, stripping hash-like segments. - * Cloud Run format: name-REVHASH-LOCHASH.region.run.app */ char tmp[256]; if (hlen >= (int)sizeof(tmp)) { return NULL; @@ -156,51 +181,76 @@ static const char *extract_service_name(const char *url, char *buf, int bufsz) { memcpy(tmp, host_start, (size_t)hlen); tmp[hlen] = '\0'; - /* Cloud Run hostname formats: - * Old: service-name-REVHASH-LOCHASH.region.run.app (strip 2 segments) - * New: service-name-PROJECTNUM.region.run.app (strip 1 segment) - * Hash/project-num segments: short alphanumeric or all-numeric. - * Never strip a segment that looks like a meaningful word (>3 chars, has letters). */ - for (int strip = 0; strip < 2; strip++) { - char *last_dash = strrchr(tmp, '-'); - if (!last_dash) { - break; - } - const char *seg = last_dash + 1; - size_t slen = strlen(seg); - if (slen > 12 || slen == 0) { - break; - } - /* Check if segment is all-numeric (GCP project number) or short alphanumeric (hash) */ - int all_alnum = 1; - int has_letter = 0; - for (size_t si = 0; si < slen; si++) { - if (!((seg[si] >= '0' && seg[si] <= '9') || (seg[si] >= 'a' && seg[si] <= 'z'))) { - all_alnum = 0; - break; - } - if (seg[si] >= 'a' && seg[si] <= 'z') { - has_letter = 1; - } - } - if (!all_alnum) { - break; - } - /* Don't strip meaningful words (>=3 chars with letters like "api", "endpoint") */ - if (has_letter && slen >= 3) { - break; - } - *last_dash = '\0'; - } + strip_cloud_run_suffixes(tmp); snprintf(buf, (size_t)bufsz, "%s", tmp); return buf; } /* Phase 2: Match infra Route URLs to handler Route nodes by URL path + service name. */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Check if a Route QN indicates an infra/async/broker Route (not a handler). */ +static bool is_broker_route(const char *qn) { + static const char *const prefixes[] = { + "__route__infra__", "__route__pubsub__", "__route__cloud_tasks__", + "__route__async__", "__route__cloud_scheduler__", "__route__kafka__", + "__route__sqs__"}; + if (!qn) { + return false; + } + for (int i = 0; i < (int)(sizeof(prefixes) / sizeof(prefixes[0])); i++) { + if (strstr(qn, prefixes[i]) == qn) { + return true; + } + } + return false; +} + +/* Try to match a single infra Route to a handler Route and create HANDLES bridge. + * Returns 1 if matched, 0 otherwise. */ +static int match_one_infra_route(cbm_gbuf_t *gb, const cbm_gbuf_node_t *infra, + const char *infra_path, const char *svc_name, + const cbm_gbuf_node_t **all_routes, int route_count) { + for (int j = 0; j < route_count; j++) { + const cbm_gbuf_node_t *handler_route = all_routes[j]; + if (is_broker_route(handler_route->qualified_name)) { + continue; + } + int file_matches = (handler_route->file_path != NULL && + strstr(handler_route->file_path, svc_name) != NULL); + int is_prefix_route = (handler_route->qualified_name != NULL && + strncmp(handler_route->qualified_name, "__route__ANY__", 14) == 0); + if (!file_matches && !is_prefix_route) { + continue; + } + const char *handler_name = handler_route->name; + if (!handler_name) { + continue; + } + const char *handler_path = strchr(handler_name, '/'); + if (!handler_path) { + continue; + } + + int path_match = (strlen(handler_path) > 1 && (strstr(infra_path, handler_path) != NULL || + strstr(handler_path, infra_path) != NULL)); + int root_svc_match = (strcmp(handler_path, "/") == 0); + if (path_match || root_svc_match) { + const cbm_gbuf_edge_t **fn_handles = NULL; + int fn_hcount = 0; + cbm_gbuf_find_edges_by_target_type(gb, handler_route->id, "HANDLES", &fn_handles, + &fn_hcount); + for (int fh = 0; fh < fn_hcount; fh++) { + cbm_gbuf_insert_edge(gb, fn_handles[fh]->source_id, infra->id, "HANDLES", + "{\"source\":\"infra_match\"}"); + } + return 1; + } + } + return 0; +} + +/* Phase 2: Match infra Route URLs to handler Route nodes by URL path + service name. */ static void match_infra_routes(cbm_gbuf_t *gb) { - /* Collect infra Routes (from YAML) and handler Routes (from Python decorators) */ const cbm_gbuf_node_t **all_routes = NULL; int route_count = 0; if (cbm_gbuf_find_by_label(gb, "Route", &all_routes, &route_count) != 0 || route_count == 0) { @@ -211,7 +261,6 @@ static void match_infra_routes(cbm_gbuf_t *gb) { for (int i = 0; i < route_count; i++) { const cbm_gbuf_node_t *infra = all_routes[i]; - /* Only process infra routes (full URLs) */ if (!infra->qualified_name || strncmp(infra->qualified_name, "__route__infra__", 16) != 0) { continue; } @@ -226,67 +275,7 @@ static void match_infra_routes(cbm_gbuf_t *gb) { continue; } - /* Find handler Routes whose file_path contains the service name - * and whose route path matches the infra URL path */ - for (int j = 0; j < route_count; j++) { - const cbm_gbuf_node_t *handler_route = all_routes[j]; - /* Skip infra/async/broker Route nodes — match only handler/prefix Routes */ - const char *hqn = handler_route->qualified_name; - if (hqn && - (strstr(hqn, "__route__infra__") == hqn || - strstr(hqn, "__route__pubsub__") == hqn || - strstr(hqn, "__route__cloud_tasks__") == hqn || - strstr(hqn, "__route__async__") == hqn || - strstr(hqn, "__route__cloud_scheduler__") == hqn || - strstr(hqn, "__route__kafka__") == hqn || strstr(hqn, "__route__sqs__") == hqn)) { - continue; - } - /* Handler route must be in the matching service directory. - * Prefix Routes from include_router have no file_path — match by path only. */ - int file_matches = (handler_route->file_path != NULL && - strstr(handler_route->file_path, svc_name) != NULL); - int is_prefix_route = - (handler_route->qualified_name != NULL && - strncmp(handler_route->qualified_name, "__route__ANY__", 14) == 0); - if (!file_matches && !is_prefix_route) { - continue; - } - - /* Check if the infra URL path matches the handler route path. - * Handler route name is like "POST /path" — extract the path part. */ - const char *handler_name = handler_route->name; - if (!handler_name) { - continue; - } - /* Skip method prefix: "POST /path" → "/path" */ - const char *handler_path = strchr(handler_name, '/'); - if (!handler_path) { - continue; - } - - /* Match: infra path contains handler path, handler path contains infra path, - * OR handler is root "/" and service name matches (microservice pattern: - * each service has one handler at "/", differentiated by hostname). */ - int path_match = - (strlen(handler_path) > 1 && (strstr(infra_path, handler_path) != NULL || - strstr(handler_path, infra_path) != NULL)); - int root_svc_match = (strcmp(handler_path, "/") == 0); - if (path_match || root_svc_match) { - /* Find the handler Function that HANDLES this decorator Route */ - const cbm_gbuf_edge_t **fn_handles = NULL; - int fn_hcount = 0; - cbm_gbuf_find_edges_by_target_type(gb, handler_route->id, "HANDLES", &fn_handles, - &fn_hcount); - for (int fh = 0; fh < fn_hcount; fh++) { - /* Create HANDLES: handler Function → infra Route - * This bridges the gap so DATA_FLOWS can find the handler */ - cbm_gbuf_insert_edge(gb, fn_handles[fh]->source_id, infra->id, "HANDLES", - "{\"source\":\"infra_match\"}"); - } - matched++; - break; - } - } + matched += match_one_infra_route(gb, infra, infra_path, svc_name, all_routes, route_count); } if (matched > 0) { @@ -299,7 +288,77 @@ static void match_infra_routes(cbm_gbuf_t *gb) { /* Phase 2a: Ensure all functions with route_path properties have Route+HANDLES edges. * During incremental indexing, only changed files get Route nodes from extraction. * This pass scans ALL Function/Method nodes and creates missing Route+HANDLES. */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Extract a JSON string property value into buf. Returns true if found. */ +static bool extract_json_prop(const char *json, const char *key, char *buf, int bufsz) { + if (!json) { + return false; + } + char pattern[64]; + snprintf(pattern, sizeof(pattern), "\"%s\":\"", key); + const char *p = strstr(json, pattern); + if (!p) { + return false; + } + p += strlen(pattern); + const char *end = strchr(p, '"'); + if (!end || end <= p) { + return false; + } + int len = (int)(end - p); + if (len >= bufsz) { + return false; + } + memcpy(buf, p, (size_t)len); + buf[len] = '\0'; + return true; +} + +/* Process a single Function/Method node: create Route+HANDLES if it has route_path. + * Returns 1 if a new HANDLES edge was created, 0 otherwise. */ +static int ensure_one_decorator_route(cbm_gbuf_t *gb, const cbm_gbuf_node_t *func) { + if (!func->properties_json) { + return 0; + } + + char path[256]; + if (!extract_json_prop(func->properties_json, "route_path", path, sizeof(path))) { + return 0; + } + if (path[0] != '/') { + return 0; + } + + char method[16] = "ANY"; + extract_json_prop(func->properties_json, "route_method", method, sizeof(method)); + + char route_qn[CBM_ROUTE_QN_SIZE]; + snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", method, path); + const cbm_gbuf_node_t *existing = cbm_gbuf_find_by_qn(gb, route_qn); + + char rprops[256]; + snprintf(rprops, sizeof(rprops), "{\"method\":\"%s\",\"source\":\"decorator\"}", method); + int64_t route_id = cbm_gbuf_upsert_node(gb, "Route", path, route_qn, + func->file_path ? func->file_path : "", 0, 0, rprops); + + if (existing) { + const cbm_gbuf_edge_t **existing_handles = NULL; + int eh_count = 0; + cbm_gbuf_find_edges_by_target_type(gb, route_id, "HANDLES", &existing_handles, &eh_count); + for (int eh = 0; eh < eh_count; eh++) { + if (existing_handles[eh]->source_id == func->id) { + return 0; + } + } + } + + char hprops[512]; + snprintf(hprops, sizeof(hprops), "{\"handler\":\"%s\"}", + func->qualified_name ? func->qualified_name : ""); + cbm_gbuf_insert_edge(gb, func->id, route_id, "HANDLES", hprops); + return 1; +} + +/* Phase 2a: Ensure all functions with route_path properties have Route+HANDLES edges. */ static void ensure_decorator_routes(cbm_gbuf_t *gb) { const char *labels[] = {"Function", "Method"}; int created = 0; @@ -311,78 +370,7 @@ static void ensure_decorator_routes(cbm_gbuf_t *gb) { continue; } for (int i = 0; i < count; i++) { - const cbm_gbuf_node_t *func = nodes[i]; - if (!func->properties_json) { - continue; - } - /* Extract route_path from properties */ - const char *rp = strstr(func->properties_json, "\"route_path\":\""); - if (!rp) { - continue; - } - rp += 14; /* skip "route_path":" */ - const char *rp_end = strchr(rp, '"'); - if (!rp_end || rp_end <= rp) { - continue; - } - int plen = (int)(rp_end - rp); - char path[256]; - if (plen >= (int)sizeof(path)) { - continue; - } - memcpy(path, rp, (size_t)plen); - path[plen] = '\0'; - if (path[0] != '/') { - continue; - } - - /* Extract route_method */ - const char *rm = strstr(func->properties_json, "\"route_method\":\""); - char method[16] = "ANY"; - if (rm) { - rm += 16; - const char *rm_end = strchr(rm, '"'); - if (rm_end && rm_end > rm && (rm_end - rm) < 16) { - memcpy(method, rm, (size_t)(rm_end - rm)); - method[rm_end - rm] = '\0'; - } - } - - /* Check if Route node already exists */ - char route_qn[CBM_ROUTE_QN_SIZE]; - snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", method, path); - const cbm_gbuf_node_t *existing = cbm_gbuf_find_by_qn(gb, route_qn); - - /* Create Route if missing */ - char rprops[256]; - snprintf(rprops, sizeof(rprops), "{\"method\":\"%s\",\"source\":\"decorator\"}", - method); - int64_t route_id = cbm_gbuf_upsert_node( - gb, "Route", path, route_qn, func->file_path ? func->file_path : "", 0, 0, rprops); - - /* Check if HANDLES edge already exists */ - if (existing) { - const cbm_gbuf_edge_t **existing_handles = NULL; - int eh_count = 0; - cbm_gbuf_find_edges_by_target_type(gb, route_id, "HANDLES", &existing_handles, - &eh_count); - int already_handled = 0; - for (int eh = 0; eh < eh_count; eh++) { - if (existing_handles[eh]->source_id == func->id) { - already_handled = 1; - break; - } - } - if (already_handled) { - continue; - } - } - - char hprops[512]; - snprintf(hprops, sizeof(hprops), "{\"handler\":\"%s\"}", - func->qualified_name ? func->qualified_name : ""); - cbm_gbuf_insert_edge(gb, func->id, route_id, "HANDLES", hprops); - created++; + created += ensure_one_decorator_route(gb, nodes[i]); } } @@ -398,7 +386,37 @@ static void ensure_decorator_routes(cbm_gbuf_t *gb) { * (from the registering file), derive the service directory, then find decorator * Routes in that directory tree and create HANDLES from their handler Functions * to the prefix Route. This bridges include_router → decorator → handler. */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Bridge decorator handler Functions to a prefix Route. Returns number connected. */ +static int bridge_funcs_to_prefix(cbm_gbuf_t *gb, const cbm_gbuf_node_t *prefix_route, + const char *registrar_path, int dir_len, + const char *prefix_segs) { + const cbm_gbuf_node_t **funcs = NULL; + int func_count = 0; + cbm_gbuf_find_by_label(gb, "Function", &funcs, &func_count); + + int connected = 0; + for (int fi = 0; fi < func_count; fi++) { + const cbm_gbuf_node_t *func = funcs[fi]; + if (!func->file_path) { + continue; + } + if (strncmp(func->file_path, registrar_path, (size_t)dir_len) != 0) { + continue; + } + if (!func->properties_json || !strstr(func->properties_json, "\"route_path\"")) { + continue; + } + if (prefix_segs && prefix_segs[0] && !strstr(func->file_path, prefix_segs)) { + continue; + } + cbm_gbuf_insert_edge(gb, func->id, prefix_route->id, "HANDLES", + "{\"source\":\"prefix_decorator_bridge\"}"); + connected++; + } + return connected; +} + +/* Phase 2b: Connect prefix Routes to decorator handler Functions. */ static void connect_prefix_to_decorators(cbm_gbuf_t *gb) { const cbm_gbuf_node_t **routes = NULL; int route_count = 0; @@ -410,13 +428,11 @@ static void connect_prefix_to_decorators(cbm_gbuf_t *gb) { for (int ri = 0; ri < route_count; ri++) { const cbm_gbuf_node_t *prefix_route = routes[ri]; - /* Only process prefix Routes from include_router */ if (!prefix_route->qualified_name || strncmp(prefix_route->qualified_name, "__route__ANY__/", 15) != 0) { continue; } - /* Find the CALLS edge TO this prefix Route (from the registering function) */ const cbm_gbuf_edge_t **calls_in = NULL; int calls_count = 0; cbm_gbuf_find_edges_by_target_type(gb, prefix_route->id, "CALLS", &calls_in, &calls_count); @@ -424,55 +440,22 @@ static void connect_prefix_to_decorators(cbm_gbuf_t *gb) { continue; } - /* Get the registering file's directory as the service root */ const cbm_gbuf_node_t *registrar = cbm_gbuf_find_by_id(gb, calls_in[0]->source_id); if (!registrar || !registrar->file_path) { continue; } - /* Extract directory: "docker-images/cloud-runs/svc/main.py" → - * "docker-images/cloud-runs/svc/" */ const char *last_slash = strrchr(registrar->file_path, '/'); if (!last_slash) { continue; } int dir_len = (int)(last_slash - registrar->file_path) + 1; - /* Convert prefix path to directory segments for file path matching. - * Prefix "/bq/bulk/ranked" → "bq/bulk/ranked" (skip leading /) */ const char *prefix_path = prefix_route->name; const char *prefix_segs = (prefix_path && prefix_path[0] == '/') ? prefix_path + 1 : prefix_path; - /* Find Function/Method nodes whose file path contains the prefix segments - * AND has route_path in properties (AST-extracted decorator route). */ - const cbm_gbuf_node_t **funcs = NULL; - int func_count = 0; - cbm_gbuf_find_by_label(gb, "Function", &funcs, &func_count); - - for (int fi = 0; fi < func_count; fi++) { - const cbm_gbuf_node_t *func = funcs[fi]; - if (!func->file_path) { - continue; - } - /* Must be in the same service directory */ - if (strncmp(func->file_path, registrar->file_path, (size_t)dir_len) != 0) { - continue; - } - /* Must have route_path in properties (AST-extracted decorator route) */ - if (!func->properties_json || !strstr(func->properties_json, "\"route_path\"")) { - continue; - } - /* File path must contain the prefix path segments. - * E.g., prefix "/bq/bulk/ranked" → file must contain "bq/bulk/ranked" */ - if (prefix_segs && prefix_segs[0] && !strstr(func->file_path, prefix_segs)) { - continue; - } - /* Create HANDLES: handler Function → prefix Route */ - char props[256]; - snprintf(props, sizeof(props), "{\"source\":\"prefix_decorator_bridge\"}"); - cbm_gbuf_insert_edge(gb, func->id, prefix_route->id, "HANDLES", props); - connected++; - } + connected += + bridge_funcs_to_prefix(gb, prefix_route, registrar->file_path, dir_len, prefix_segs); } if (connected > 0) { @@ -539,7 +522,152 @@ static const char *find_args_in_props(const char *props) { return p + 7; /* skip "args":[ , points to first { or ] */ } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Append handler_params and caller_args to DATA_FLOWS props. Closes with '}'. */ +static void finish_data_flow_props(char *props, size_t propsz, size_t pos, + const char *handler_params, const char *args_json) { + if (handler_params[0]) { + int w = snprintf(props + pos, propsz - pos, ",\"handler_params\":[%s]", handler_params); + if (w > 0) { + pos += (size_t)w; + } + } + if (args_json) { + int w = snprintf(props + pos, propsz - pos, ",\"caller_args\":[%.*s", 400, args_json); + if (w > 0) { + pos += (size_t)w; + char *close = strchr(props + (pos - (size_t)w) + 14, ']'); + if (close && close < props + propsz - 2) { + pos = (size_t)(close - props) + 1; + } + } + } + if (pos < propsz - 1) { + props[pos] = '}'; + props[pos + 1] = '\0'; + } +} + +typedef struct { + int64_t source_id; + const char *props; + const char *edge_type; +} caller_edge_ref_t; + +/* Try to create a DATA_FLOWS edge between caller and handler via a route. + * Returns: 1=created, 0=skipped (self/duplicate), -1=skipped (has direct call). */ +static int try_create_data_flow(cbm_gbuf_t *gb, int64_t caller_id, int64_t handler_id, + const cbm_gbuf_node_t *route, const char *edge_type, + const char *caller_props, bool via_infra) { + if (caller_id == handler_id) { + return 0; + } + if (has_direct_call(gb, caller_id, handler_id)) { + return -1; + } + + const char *args_json = find_args_in_props(caller_props); + const cbm_gbuf_node_t *handler_node = cbm_gbuf_find_by_id(gb, handler_id); + char handler_params[512]; + extract_param_names(handler_node, handler_params, sizeof(handler_params)); + + char props[2048]; + int n; + if (via_infra) { + n = snprintf(props, sizeof(props), + "{\"via\":\"%s\",\"route\":\"%s\",\"edge_type\":\"%s\",\"via_infra\":true", + route->name ? route->name : "", + route->qualified_name ? route->qualified_name : "", edge_type); + } else { + n = snprintf(props, sizeof(props), "{\"via\":\"%s\",\"route\":\"%s\",\"edge_type\":\"%s\"", + route->name ? route->name : "", + route->qualified_name ? route->qualified_name : "", edge_type); + } + + if (n > 0 && (size_t)n < sizeof(props) - 100) { + finish_data_flow_props(props, sizeof(props), (size_t)n, handler_params, args_json); + } + cbm_gbuf_insert_edge(gb, caller_id, handler_id, "DATA_FLOWS", props); + return 1; +} + +/* Collect extra handler IDs reachable via INFRA_MAPS chain from a route. */ +static int collect_infra_handlers(cbm_gbuf_t *gb, int64_t route_id, int64_t *out, int max_out) { + const cbm_gbuf_edge_t **infra_edges = NULL; + int infra_count = 0; + cbm_gbuf_find_edges_by_source_type(gb, route_id, "INFRA_MAPS", &infra_edges, &infra_count); + + int n = 0; + for (int ie = 0; ie < infra_count; ie++) { + const cbm_gbuf_edge_t **ep_handles = NULL; + int ep_hcount = 0; + cbm_gbuf_find_edges_by_target_type(gb, infra_edges[ie]->target_id, "HANDLES", &ep_handles, + &ep_hcount); + for (int eh = 0; eh < ep_hcount && n < max_out; eh++) { + out[n++] = ep_handles[eh]->source_id; + } + } + return n; +} + +/* Collect caller edges (HTTP_CALLS + ASYNC_CALLS) targeting a route. */ +static int collect_caller_edges(cbm_gbuf_t *gb, int64_t route_id, caller_edge_ref_t *out, + int max_out) { + int n = 0; + const cbm_gbuf_edge_t **http_edges = NULL; + int http_count = 0; + cbm_gbuf_find_edges_by_target_type(gb, route_id, "HTTP_CALLS", &http_edges, &http_count); + for (int i = 0; i < http_count && n < max_out; i++) { + out[n].source_id = http_edges[i]->source_id; + out[n].props = http_edges[i]->properties_json; + out[n].edge_type = "HTTP_CALLS"; + n++; + } + const cbm_gbuf_edge_t **async_edges = NULL; + int async_count = 0; + cbm_gbuf_find_edges_by_target_type(gb, route_id, "ASYNC_CALLS", &async_edges, &async_count); + for (int i = 0; i < async_count && n < max_out; i++) { + out[n].source_id = async_edges[i]->source_id; + out[n].props = async_edges[i]->properties_json; + out[n].edge_type = "ASYNC_CALLS"; + n++; + } + return n; +} + +/* Create DATA_FLOWS between callers and handlers for one route node. */ +static void create_route_data_flows(cbm_gbuf_t *gb, const cbm_gbuf_node_t *route, + const caller_edge_ref_t *callers, int n_callers, int *flows, + int *skipped) { + const cbm_gbuf_edge_t **handles_edges = NULL; + int handles_count = 0; + cbm_gbuf_find_edges_by_target_type(gb, route->id, "HANDLES", &handles_edges, &handles_count); + + int64_t extra_handlers[32]; + int n_extra = collect_infra_handlers(gb, route->id, extra_handlers, 32); + + for (int ci = 0; ci < n_callers; ci++) { + for (int hi = 0; hi < handles_count; hi++) { + int rc = try_create_data_flow(gb, callers[ci].source_id, handles_edges[hi]->source_id, + route, callers[ci].edge_type, callers[ci].props, false); + if (rc > 0) { + (*flows)++; + } else if (rc < 0) { + (*skipped)++; + } + } + for (int xi = 0; xi < n_extra; xi++) { + int rc = try_create_data_flow(gb, callers[ci].source_id, extra_handlers[xi], route, + callers[ci].edge_type, callers[ci].props, true); + if (rc > 0) { + (*flows)++; + } else if (rc < 0) { + (*skipped)++; + } + } + } +} + +/* Phase 3: Create DATA_FLOWS edges by linking callers through Route to handlers. */ static void create_data_flows(cbm_gbuf_t *gb) { const cbm_gbuf_node_t **routes = NULL; int route_count = 0; @@ -551,181 +679,9 @@ static void create_data_flows(cbm_gbuf_t *gb) { int skipped = 0; for (int ri = 0; ri < route_count; ri++) { - const cbm_gbuf_node_t *route = routes[ri]; - - /* Collect caller edges (HTTP_CALLS + ASYNC_CALLS → Route) */ - const cbm_gbuf_edge_t **http_edges = NULL; - int http_count = 0; - cbm_gbuf_find_edges_by_target_type(gb, route->id, "HTTP_CALLS", &http_edges, &http_count); - - const cbm_gbuf_edge_t **async_edges = NULL; - int async_count = 0; - cbm_gbuf_find_edges_by_target_type(gb, route->id, "ASYNC_CALLS", &async_edges, - &async_count); - - /* Collect caller edge references (need properties for arg mapping) */ - struct { - int64_t source_id; - const char *props; - const char *edge_type; - } caller_edges[64]; - int n_callers = 0; - for (int ei = 0; ei < http_count && n_callers < 64; ei++) { - caller_edges[n_callers].source_id = http_edges[ei]->source_id; - caller_edges[n_callers].props = http_edges[ei]->properties_json; - caller_edges[n_callers].edge_type = "HTTP_CALLS"; - n_callers++; - } - for (int ei = 0; ei < async_count && n_callers < 64; ei++) { - caller_edges[n_callers].source_id = async_edges[ei]->source_id; - caller_edges[n_callers].props = async_edges[ei]->properties_json; - caller_edges[n_callers].edge_type = "ASYNC_CALLS"; - n_callers++; - } - - /* Collect handler nodes: direct HANDLES → Route, plus HANDLES on - * endpoint Routes reachable via INFRA_MAPS (topic → endpoint → handler) */ - const cbm_gbuf_edge_t **handles_edges = NULL; - int handles_count = 0; - cbm_gbuf_find_edges_by_target_type(gb, route->id, "HANDLES", &handles_edges, - &handles_count); - - /* Also follow INFRA_MAPS: topic Route → endpoint Route → HANDLES - * This bridges async topic Routes to their push endpoint handlers. */ - const cbm_gbuf_edge_t **infra_edges = NULL; - int infra_count = 0; - cbm_gbuf_find_edges_by_source_type(gb, route->id, "INFRA_MAPS", &infra_edges, &infra_count); - - /* Extra handlers found via INFRA_MAPS chain */ - int64_t extra_handlers[32]; - int n_extra = 0; - for (int ie = 0; ie < infra_count; ie++) { - int64_t endpoint_id = infra_edges[ie]->target_id; - /* Find HANDLES edges on the endpoint Route */ - const cbm_gbuf_edge_t **ep_handles = NULL; - int ep_hcount = 0; - cbm_gbuf_find_edges_by_target_type(gb, endpoint_id, "HANDLES", &ep_handles, &ep_hcount); - for (int eh = 0; eh < ep_hcount && n_extra < 32; eh++) { - extra_handlers[n_extra++] = ep_handles[eh]->source_id; - } - } - - for (int ci = 0; ci < n_callers; ci++) { - for (int hi = 0; hi < handles_count; hi++) { - int64_t caller_id = caller_edges[ci].source_id; - int64_t handler_id = handles_edges[hi]->source_id; - - if (caller_id == handler_id) { - continue; - } - - /* Skip if direct CALLS edge already exists */ - if (has_direct_call(gb, caller_id, handler_id)) { - skipped++; - continue; - } - - /* Build value mapping: caller args → handler params */ - const char *args_json = find_args_in_props(caller_edges[ci].props); - - const cbm_gbuf_node_t *handler_node = cbm_gbuf_find_by_id(gb, handler_id); - char handler_params[512]; - extract_param_names(handler_node, handler_params, sizeof(handler_params)); - - /* Build DATA_FLOWS properties with actual value mapping */ - char props[2048]; - int n = snprintf( - props, sizeof(props), "{\"via\":\"%s\",\"route\":\"%s\",\"edge_type\":\"%s\"", - route->name ? route->name : "", - route->qualified_name ? route->qualified_name : "", caller_edges[ci].edge_type); - - if (n > 0 && (size_t)n < sizeof(props) - 100) { - size_t pos = (size_t)n; - - /* Include handler param_names */ - if (handler_params[0]) { - int w = snprintf(props + pos, sizeof(props) - pos, - ",\"handler_params\":[%s]", handler_params); - if (w > 0) { - pos += (size_t)w; - } - } - - /* Include caller args (copy from source edge) */ - if (args_json) { - int w = snprintf(props + pos, sizeof(props) - pos, ",\"caller_args\":[%.*s", - 400, args_json); - if (w > 0) { - pos += (size_t)w; - /* Find closing ] in the copied fragment */ - char *close = strchr(props + (pos - (size_t)w) + 14, ']'); - if (close && close < props + sizeof(props) - 2) { - pos = (size_t)(close - props) + 1; - } - } - } - - if (pos < sizeof(props) - 1) { - props[pos] = '}'; - props[pos + 1] = '\0'; - } - } - cbm_gbuf_insert_edge(gb, caller_id, handler_id, "DATA_FLOWS", props); - flows++; - } - - /* Also check handlers found via INFRA_MAPS chain */ - for (int xi = 0; xi < n_extra; xi++) { - int64_t caller_id = caller_edges[ci].source_id; - int64_t handler_id = extra_handlers[xi]; - if (caller_id == handler_id) { - continue; - } - if (has_direct_call(gb, caller_id, handler_id)) { - skipped++; - continue; - } - const char *args_json = find_args_in_props(caller_edges[ci].props); - const cbm_gbuf_node_t *handler_node = cbm_gbuf_find_by_id(gb, handler_id); - char handler_params[512]; - extract_param_names(handler_node, handler_params, sizeof(handler_params)); - - char props[2048]; - int n = snprintf(props, sizeof(props), - "{\"via\":\"%s\",\"route\":\"%s\",\"edge_type\":\"%s\"," - "\"via_infra\":true", - route->name ? route->name : "", - route->qualified_name ? route->qualified_name : "", - caller_edges[ci].edge_type); - if (n > 0 && (size_t)n < sizeof(props) - 100) { - size_t pos = (size_t)n; - if (handler_params[0]) { - int w = snprintf(props + pos, sizeof(props) - pos, - ",\"handler_params\":[%s]", handler_params); - if (w > 0) { - pos += (size_t)w; - } - } - if (args_json) { - int w = snprintf(props + pos, sizeof(props) - pos, ",\"caller_args\":[%.*s", - 400, args_json); - if (w > 0) { - pos += (size_t)w; - char *close = strchr(props + (pos - (size_t)w) + 14, ']'); - if (close && close < props + sizeof(props) - 2) { - pos = (size_t)(close - props) + 1; - } - } - } - if (pos < sizeof(props) - 1) { - props[pos] = '}'; - props[pos + 1] = '\0'; - } - } - cbm_gbuf_insert_edge(gb, caller_id, handler_id, "DATA_FLOWS", props); - flows++; - } - } + caller_edge_ref_t caller_edges[64]; + int n_callers = collect_caller_edges(gb, routes[ri]->id, caller_edges, 64); + create_route_data_flows(gb, routes[ri], caller_edges, n_callers, &flows, &skipped); } if (flows > 0 || skipped > 0) { diff --git a/src/pipeline/pass_tests.c b/src/pipeline/pass_tests.c index c7cc30cb1..27283cc90 100644 --- a/src/pipeline/pass_tests.c +++ b/src/pipeline/pass_tests.c @@ -132,54 +132,44 @@ bool cbm_is_test_func_name(const char *name) { return false; } +/* Assemble dir/name+ext into a heap-allocated path. */ +static char *build_path(const char *test_path, size_t dir_len, const char *name, size_t name_len, + const char *ext, size_t ext_len) { + char *result = malloc(dir_len + 1 + name_len + ext_len + 1); + if (dir_len > 0) { + memcpy(result, test_path, dir_len); + result[dir_len] = '/'; + memcpy(result + dir_len + 1, name, name_len); + memcpy(result + dir_len + 1 + name_len, ext, ext_len + 1); + } else { + memcpy(result, name, name_len); + memcpy(result + name_len, ext, ext_len + 1); + } + return result; +} + /* Try to derive the production file path from a test file path. * Returns heap-allocated string or NULL. Caller must free(). */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) static char *test_to_prod_path(const char *test_path) { if (!test_path) { return NULL; } const char *base = strrchr(test_path, '/'); - const char *dir_end = base ? base : test_path; size_t dir_len = base ? (size_t)(base - test_path) : 0; - base = base ? base + 1 : test_path; /* Go: foo_test.go → foo.go */ const char *suffix = strstr(base, "_test.go"); if (suffix && suffix[8] == '\0') { - size_t name_len = suffix - base; - char *result = malloc(dir_len + 1 + name_len + 3 + 1); - if (dir_len > 0) { - memcpy(result, test_path, dir_len); - result[dir_len] = '/'; - memcpy(result + dir_len + 1, base, name_len); - memcpy(result + dir_len + 1 + name_len, ".go", 4); - } else { - memcpy(result, base, name_len); - memcpy(result + name_len, ".go", 4); - } - return result; + return build_path(test_path, dir_len, base, (size_t)(suffix - base), ".go", 3); } /* Python: test_foo.py → foo.py */ if (strncmp(base, "test_", 5) == 0) { const char *ext = strstr(base, ".py"); if (ext && ext[3] == '\0') { - const char *name_start = base + 5; - size_t name_len = ext - name_start; - char *result = malloc(dir_len + 1 + name_len + 3 + 1); - if (dir_len > 0) { - memcpy(result, test_path, dir_len); - result[dir_len] = '/'; - memcpy(result + dir_len + 1, name_start, name_len); - memcpy(result + dir_len + 1 + name_len, ".py", 4); - } else { - memcpy(result, name_start, name_len); - memcpy(result + name_len, ".py", 4); - } - return result; + return build_path(test_path, dir_len, base + 5, (size_t)(ext - base - 5), ".py", 3); } } @@ -188,102 +178,95 @@ static char *test_to_prod_path(const char *test_path) { const char *pat = s == 0 ? ".test." : ".spec."; const char *found = strstr(base, pat); if (found) { - size_t prefix_len = found - base; - const char *ext = found + 5; /* skip ".test" or ".spec" (both 5 chars) */ - size_t ext_len = strlen(ext); - char *result = malloc(dir_len + 1 + prefix_len + ext_len + 1); - if (dir_len > 0) { - memcpy(result, test_path, dir_len); - result[dir_len] = '/'; - memcpy(result + dir_len + 1, base, prefix_len); - memcpy(result + dir_len + 1 + prefix_len, ext, ext_len + 1); - } else { - memcpy(result, base, prefix_len); - memcpy(result + prefix_len, ext, ext_len + 1); - } - return result; + const char *ext = found + 5; + return build_path(test_path, dir_len, base, (size_t)(found - base), ext, strlen(ext)); } } - (void)dir_end; return NULL; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Create TESTS edges from test functions to production functions via CALLS edges. */ +static int create_tests_edges(cbm_pipeline_ctx_t *ctx) { + int count = 0; + const cbm_gbuf_edge_t **call_edges = NULL; + int call_count = 0; + int rc = cbm_gbuf_find_edges_by_type(ctx->gbuf, "CALLS", &call_edges, &call_count); + if (rc != 0 || call_count == 0) { + return 0; + } + + for (int i = 0; i < call_count; i++) { + const cbm_gbuf_edge_t *e = call_edges[i]; + const cbm_gbuf_node_t *src = cbm_gbuf_find_by_id(ctx->gbuf, e->source_id); + const cbm_gbuf_node_t *tgt = cbm_gbuf_find_by_id(ctx->gbuf, e->target_id); + if (!src || !tgt) { + continue; + } + + bool src_is_test = + node_is_test(src) || (src->file_path && cbm_is_test_path(src->file_path)); + if (!src_is_test) { + continue; + } + + bool tgt_is_test = + node_is_test(tgt) || (tgt->file_path && cbm_is_test_path(tgt->file_path)); + if (tgt_is_test) { + continue; + } + + if (!cbm_is_test_func_name(src->name)) { + continue; + } + + cbm_gbuf_insert_edge(ctx->gbuf, src->id, tgt->id, "TESTS", "{}"); + count++; + } + return count; +} + +/* Create TESTS_FILE edges from test File nodes to production File nodes. */ +static int create_tests_file_edges(cbm_pipeline_ctx_t *ctx) { + int count = 0; + const cbm_gbuf_node_t **file_nodes = NULL; + int file_node_count = 0; + int rc = cbm_gbuf_find_by_label(ctx->gbuf, "File", &file_nodes, &file_node_count); + if (rc != 0) { + return 0; + } + + for (int i = 0; i < file_node_count; i++) { + const cbm_gbuf_node_t *fnode = file_nodes[i]; + if (!fnode->file_path || !cbm_is_test_path(fnode->file_path)) { + continue; + } + + char *prod_path = test_to_prod_path(fnode->file_path); + if (!prod_path) { + continue; + } + + char *prod_qn = cbm_pipeline_fqn_compute(ctx->project_name, prod_path, "__file__"); + const cbm_gbuf_node_t *prod_node = cbm_gbuf_find_by_qn(ctx->gbuf, prod_qn); + free(prod_qn); + free(prod_path); + + if (prod_node && fnode->id != prod_node->id) { + cbm_gbuf_insert_edge(ctx->gbuf, fnode->id, prod_node->id, "TESTS_FILE", "{}"); + count++; + } + } + return count; +} + int cbm_pipeline_pass_tests(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, int file_count) { cbm_log_info("pass.start", "pass", "tests"); (void)files; (void)file_count; - int tests_count = 0; - int tests_file_count = 0; - - /* ── TESTS edges: test function → production function ────────── */ - /* Scan CALLS edges: if src is test + target is not test → TESTS edge */ - const cbm_gbuf_edge_t **call_edges = NULL; - int call_count = 0; - int rc = cbm_gbuf_find_edges_by_type(ctx->gbuf, "CALLS", &call_edges, &call_count); - if (rc == 0 && call_count > 0) { - for (int i = 0; i < call_count; i++) { - const cbm_gbuf_edge_t *e = call_edges[i]; - const cbm_gbuf_node_t *src = cbm_gbuf_find_by_id(ctx->gbuf, e->source_id); - const cbm_gbuf_node_t *tgt = cbm_gbuf_find_by_id(ctx->gbuf, e->target_id); - if (!src || !tgt) { - continue; - } - - /* Source must be a test, target must not */ - bool src_is_test = - node_is_test(src) || (src->file_path && cbm_is_test_path(src->file_path)); - if (!src_is_test) { - continue; - } - - bool tgt_is_test = - node_is_test(tgt) || (tgt->file_path && cbm_is_test_path(tgt->file_path)); - if (tgt_is_test) { - continue; - } - - /* Gate: source must have a test function name */ - if (!cbm_is_test_func_name(src->name)) { - continue; - } - - cbm_gbuf_insert_edge(ctx->gbuf, src->id, tgt->id, "TESTS", "{}"); - tests_count++; - } - } - - /* ── TESTS_FILE edges: test file → production file ───────────── */ - /* For each File node that is a test, find the corresponding prod file */ - const cbm_gbuf_node_t **file_nodes = NULL; - int file_node_count = 0; - rc = cbm_gbuf_find_by_label(ctx->gbuf, "File", &file_nodes, &file_node_count); - if (rc == 0) { - for (int i = 0; i < file_node_count; i++) { - const cbm_gbuf_node_t *fnode = file_nodes[i]; - if (!fnode->file_path || !cbm_is_test_path(fnode->file_path)) { - continue; - } - - char *prod_path = test_to_prod_path(fnode->file_path); - if (!prod_path) { - continue; - } - - /* Find the production file node */ - char *prod_qn = cbm_pipeline_fqn_compute(ctx->project_name, prod_path, "__file__"); - const cbm_gbuf_node_t *prod_node = cbm_gbuf_find_by_qn(ctx->gbuf, prod_qn); - free(prod_qn); - free(prod_path); - - if (prod_node && fnode->id != prod_node->id) { - cbm_gbuf_insert_edge(ctx->gbuf, fnode->id, prod_node->id, "TESTS_FILE", "{}"); - tests_file_count++; - } - } - } + int tests_count = create_tests_edges(ctx); + int tests_file_count = create_tests_file_edges(ctx); cbm_log_info("pass.done", "pass", "tests", "tests", itoa_log(tests_count), "tests_file", itoa_log(tests_file_count)); diff --git a/src/pipeline/pass_usages.c b/src/pipeline/pass_usages.c index 29d1cdebc..d09912c71 100644 --- a/src/pipeline/pass_usages.c +++ b/src/pipeline/pass_usages.c @@ -70,44 +70,40 @@ static bool is_checked_exception(const char *name) { return true; /* Default: treat as checked */ } -/* Build per-file import map from cached extraction result or graph buffer edges. */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) -static int build_import_map(cbm_pipeline_ctx_t *ctx, const char *rel_path, - const CBMFileResult *result, const char ***out_keys, - const char ***out_vals, int *out_count) { - *out_keys = NULL; - *out_vals = NULL; - *out_count = 0; +/* Build import map from cached extraction result (fast path). */ +static int build_import_map_from_cache(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result, + const char ***out_keys, const char ***out_vals, + int *out_count) { + const char **keys = calloc((size_t)result->imports.count, sizeof(const char *)); + const char **vals = calloc((size_t)result->imports.count, sizeof(const char *)); + int count = 0; - /* Fast path: build from cached extraction result (no JSON parsing) */ - if (result && result->imports.count > 0) { - const char **keys = calloc((size_t)result->imports.count, sizeof(const char *)); - const char **vals = calloc((size_t)result->imports.count, sizeof(const char *)); - int count = 0; - - for (int i = 0; i < result->imports.count; i++) { - const CBMImport *imp = &result->imports.items[i]; - if (!imp->local_name || !imp->local_name[0] || !imp->module_path) { - continue; - } - char *target_qn = cbm_pipeline_fqn_module(ctx->project_name, imp->module_path); - const cbm_gbuf_node_t *target = cbm_gbuf_find_by_qn(ctx->gbuf, target_qn); - free(target_qn); - if (!target) { - continue; - } - keys[count] = strdup(imp->local_name); - vals[count] = target->qualified_name; - count++; + for (int i = 0; i < result->imports.count; i++) { + const CBMImport *imp = &result->imports.items[i]; + if (!imp->local_name || !imp->local_name[0] || !imp->module_path) { + continue; } - - *out_keys = keys; - *out_vals = vals; - *out_count = count; - return 0; + char *target_qn = cbm_pipeline_fqn_module(ctx->project_name, imp->module_path); + const cbm_gbuf_node_t *target = cbm_gbuf_find_by_qn(ctx->gbuf, target_qn); + free(target_qn); + if (!target) { + continue; + } + keys[count] = strdup(imp->local_name); + vals[count] = target->qualified_name; + count++; } - /* Slow path: scan graph buffer IMPORTS edges + parse JSON properties */ + *out_keys = keys; + *out_vals = vals; + *out_count = count; + return 0; +} + +/* Build import map from graph buffer IMPORTS edges (slow path). */ +static int build_import_map_from_edges(cbm_pipeline_ctx_t *ctx, const char *rel_path, + const char ***out_keys, const char ***out_vals, + int *out_count) { char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel_path, "__file__"); const cbm_gbuf_node_t *file_node = cbm_gbuf_find_by_qn(ctx->gbuf, file_qn); free(file_qn); @@ -130,20 +126,18 @@ static int build_import_map(cbm_pipeline_ctx_t *ctx, const char *rel_path, for (int i = 0; i < edge_count; i++) { const cbm_gbuf_edge_t *e = edges[i]; const cbm_gbuf_node_t *target = cbm_gbuf_find_by_id(ctx->gbuf, e->target_id); - if (!target) { + if (!target || !e->properties_json) { continue; } - if (e->properties_json) { - const char *start = strstr(e->properties_json, "\"local_name\":\""); - if (start) { - start += strlen("\"local_name\":\""); - const char *end = strchr(start, '"'); - if (end && end > start) { - keys[count] = cbm_strndup(start, end - start); - vals[count] = target->qualified_name; - count++; - } + const char *start = strstr(e->properties_json, "\"local_name\":\""); + if (start) { + start += strlen("\"local_name\":\""); + const char *end = strchr(start, '"'); + if (end && end > start) { + keys[count] = cbm_strndup(start, end - start); + vals[count] = target->qualified_name; + count++; } } } @@ -154,6 +148,20 @@ static int build_import_map(cbm_pipeline_ctx_t *ctx, const char *rel_path, return 0; } +/* Build per-file import map from cached extraction result or graph buffer edges. */ +static int build_import_map(cbm_pipeline_ctx_t *ctx, const char *rel_path, + const CBMFileResult *result, const char ***out_keys, + const char ***out_vals, int *out_count) { + *out_keys = NULL; + *out_vals = NULL; + *out_count = 0; + + if (result && result->imports.count > 0) { + return build_import_map_from_cache(ctx, result, out_keys, out_vals, out_count); + } + return build_import_map_from_edges(ctx, rel_path, out_keys, out_vals, out_count); +} + static void free_import_map(const char **keys, const char **vals, int count) { if (keys) { for (int i = 0; i < count; i++) { @@ -181,16 +189,115 @@ static const cbm_gbuf_node_t *find_enclosing_node(cbm_pipeline_ctx_t *ctx, const return node; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Resolve USAGE edges for one file's extracted usages. */ +static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result, + const char *rel, const char *module_qn, const char **imp_keys, + const char **imp_vals, int imp_count) { + int resolved = 0; + for (int u = 0; u < result->usages.count; u++) { + CBMUsage *usage = &result->usages.items[u]; + if (!usage->ref_name) { + continue; + } + + const cbm_gbuf_node_t *src = find_enclosing_node(ctx, usage->enclosing_func_qn, rel); + if (!src) { + continue; + } + + cbm_resolution_t res = cbm_registry_resolve(ctx->registry, usage->ref_name, module_qn, + imp_keys, imp_vals, imp_count); + if (!res.qualified_name || res.qualified_name[0] == '\0') { + continue; + } + + const cbm_gbuf_node_t *tgt = cbm_gbuf_find_by_qn(ctx->gbuf, res.qualified_name); + if (!tgt || src->id == tgt->id) { + continue; + } + + char uprops[256]; + snprintf(uprops, sizeof(uprops), "{\"callee\":\"%s\"}", usage->ref_name); + cbm_gbuf_insert_edge(ctx->gbuf, src->id, tgt->id, "USAGE", uprops); + resolved++; + } + return resolved; +} + +/* Resolve THROWS/RAISES edges for one file's extracted throws. */ +static int resolve_throw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result, + const char *rel, const char *module_qn, const char **imp_keys, + const char **imp_vals, int imp_count) { + int resolved = 0; + for (int t = 0; t < result->throws.count; t++) { + CBMThrow *thr = &result->throws.items[t]; + if (!thr->exception_name || !thr->enclosing_func_qn) { + continue; + } + + const cbm_gbuf_node_t *src = find_enclosing_node(ctx, thr->enclosing_func_qn, rel); + if (!src) { + continue; + } + + const char *edge_type = is_checked_exception(thr->exception_name) ? "THROWS" : "RAISES"; + cbm_resolution_t res = cbm_registry_resolve(ctx->registry, thr->exception_name, module_qn, + imp_keys, imp_vals, imp_count); + + const cbm_gbuf_node_t *tgt = NULL; + if (res.qualified_name && res.qualified_name[0]) { + tgt = cbm_gbuf_find_by_qn(ctx->gbuf, res.qualified_name); + } + if (!tgt || src->id == tgt->id) { + continue; + } + + cbm_gbuf_insert_edge(ctx->gbuf, src->id, tgt->id, edge_type, "{}"); + resolved++; + } + return resolved; +} + +/* Resolve READS/WRITES edges for one file's extracted read/write accesses. */ +static int resolve_rw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result, const char *rel, + const char *module_qn, const char **imp_keys, const char **imp_vals, + int imp_count) { + int resolved = 0; + for (int r = 0; r < result->rw.count; r++) { + CBMReadWrite *rw = &result->rw.items[r]; + if (!rw->var_name) { + continue; + } + + const cbm_gbuf_node_t *src = find_enclosing_node(ctx, rw->enclosing_func_qn, rel); + if (!src) { + continue; + } + + cbm_resolution_t res = cbm_registry_resolve(ctx->registry, rw->var_name, module_qn, + imp_keys, imp_vals, imp_count); + if (!res.qualified_name || res.qualified_name[0] == '\0') { + continue; + } + + const cbm_gbuf_node_t *tgt = cbm_gbuf_find_by_qn(ctx->gbuf, res.qualified_name); + if (!tgt || src->id == tgt->id) { + continue; + } + + const char *edge_type = rw->is_write ? "WRITES" : "READS"; + cbm_gbuf_insert_edge(ctx->gbuf, src->id, tgt->id, edge_type, "{}"); + resolved++; + } + return resolved; +} + int cbm_pipeline_pass_usages(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, int file_count) { cbm_log_info("pass.start", "pass", "usages", "files", itoa_log(file_count)); - int total_usages = 0; int usage_resolved = 0; - int total_throws = 0; int throw_resolved = 0; - int total_rw = 0; int rw_resolved = 0; int errors = 0; @@ -202,21 +309,19 @@ int cbm_pipeline_pass_usages(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *fil const char *path = files[i].path; const char *rel = files[i].rel_path; - /* Use cached extraction result or re-extract */ CBMFileResult *result = NULL; bool result_owned = false; if (ctx->result_cache) { result = ctx->result_cache[i]; } if (!result) { - CBMLanguage lang = files[i].language; int source_len = 0; char *source = read_file(path, &source_len); if (!source) { errors++; continue; } - result = cbm_extract_file(source, source_len, lang, ctx->project_name, rel, + result = cbm_extract_file(source, source_len, files[i].language, ctx->project_name, rel, CBM_EXTRACT_BUDGET, NULL, NULL); free(source); if (!result) { @@ -226,7 +331,6 @@ int cbm_pipeline_pass_usages(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *fil result_owned = true; } - /* Skip files with no usages/throws/rw */ if (result->usages.count == 0 && result->throws.count == 0 && result->rw.count == 0) { if (result_owned) { cbm_free_result(result); @@ -234,7 +338,6 @@ int cbm_pipeline_pass_usages(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *fil continue; } - /* Build import map */ const char **imp_keys = NULL; const char **imp_vals = NULL; int imp_count = 0; @@ -242,100 +345,11 @@ int cbm_pipeline_pass_usages(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *fil char *module_qn = cbm_pipeline_fqn_module(ctx->project_name, rel); - /* ── USAGE edges ────────────────────────────────────────── */ - for (int u = 0; u < result->usages.count; u++) { - CBMUsage *usage = &result->usages.items[u]; - if (!usage->ref_name) { - continue; - } - total_usages++; - - const cbm_gbuf_node_t *src = find_enclosing_node(ctx, usage->enclosing_func_qn, rel); - if (!src) { - continue; - } - - cbm_resolution_t res = cbm_registry_resolve(ctx->registry, usage->ref_name, module_qn, - imp_keys, imp_vals, imp_count); - if (!res.qualified_name || res.qualified_name[0] == '\0') { - continue; - } - - const cbm_gbuf_node_t *tgt = cbm_gbuf_find_by_qn(ctx->gbuf, res.qualified_name); - if (!tgt || src->id == tgt->id) { - continue; - } - - { - char uprops[256]; - snprintf(uprops, sizeof(uprops), "{\"callee\":\"%s\"}", usage->ref_name); - cbm_gbuf_insert_edge(ctx->gbuf, src->id, tgt->id, "USAGE", uprops); - } - usage_resolved++; - } - - /* ── THROWS / RAISES edges ──────────────────────────────── */ - for (int t = 0; t < result->throws.count; t++) { - CBMThrow *thr = &result->throws.items[t]; - if (!thr->exception_name || !thr->enclosing_func_qn) { - continue; - } - total_throws++; - - const cbm_gbuf_node_t *src = find_enclosing_node(ctx, thr->enclosing_func_qn, rel); - if (!src) { - continue; - } - - const char *edge_type = is_checked_exception(thr->exception_name) ? "THROWS" : "RAISES"; - - /* Try to resolve exception class */ - cbm_resolution_t res = cbm_registry_resolve(ctx->registry, thr->exception_name, - module_qn, imp_keys, imp_vals, imp_count); - - const cbm_gbuf_node_t *tgt = NULL; - if (res.qualified_name && res.qualified_name[0]) { - tgt = cbm_gbuf_find_by_qn(ctx->gbuf, res.qualified_name); - } - if (!tgt) { - continue; /* Exception class not in graph — skip */ - } - if (src->id == tgt->id) { - continue; - } - - cbm_gbuf_insert_edge(ctx->gbuf, src->id, tgt->id, edge_type, "{}"); - throw_resolved++; - } - - /* ── READS / WRITES edges ──────────────────────────────── */ - for (int r = 0; r < result->rw.count; r++) { - CBMReadWrite *rw = &result->rw.items[r]; - if (!rw->var_name) { - continue; - } - total_rw++; - - const cbm_gbuf_node_t *src = find_enclosing_node(ctx, rw->enclosing_func_qn, rel); - if (!src) { - continue; - } - - cbm_resolution_t res = cbm_registry_resolve(ctx->registry, rw->var_name, module_qn, - imp_keys, imp_vals, imp_count); - if (!res.qualified_name || res.qualified_name[0] == '\0') { - continue; - } - - const cbm_gbuf_node_t *tgt = cbm_gbuf_find_by_qn(ctx->gbuf, res.qualified_name); - if (!tgt || src->id == tgt->id) { - continue; - } - - const char *edge_type = rw->is_write ? "WRITES" : "READS"; - cbm_gbuf_insert_edge(ctx->gbuf, src->id, tgt->id, edge_type, "{}"); - rw_resolved++; - } + usage_resolved += + resolve_usage_edges(ctx, result, rel, module_qn, imp_keys, imp_vals, imp_count); + throw_resolved += + resolve_throw_edges(ctx, result, rel, module_qn, imp_keys, imp_vals, imp_count); + rw_resolved += resolve_rw_edges(ctx, result, rel, module_qn, imp_keys, imp_vals, imp_count); free(module_qn); free_import_map(imp_keys, imp_vals, imp_count); @@ -346,9 +360,5 @@ int cbm_pipeline_pass_usages(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *fil cbm_log_info("pass.done", "pass", "usages", "usage", itoa_log(usage_resolved), "throws", itoa_log(throw_resolved), "rw", itoa_log(rw_resolved), "errors", itoa_log(errors)); - /* Suppress unused-but-set for totals — included for future debug logging */ - (void)total_usages; - (void)total_throws; - (void)total_rw; return 0; } diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index 479b1c74a..10881e9f9 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -168,9 +168,108 @@ static void registry_visitor(const cbm_gbuf_node_t *node, void *userdata) { cbm_registry_add(r, node->name, node->qualified_name, node->label); } +/* Run parallel or sequential extract+resolve for changed files. */ +static void run_extract_resolve(cbm_pipeline_ctx_t *ctx, cbm_file_info_t *changed_files, int ci) { + struct timespec t; + +#define MIN_FILES_FOR_PARALLEL_INCR 50 + int worker_count = cbm_default_worker_count(true); + bool use_parallel = (worker_count > 1 && ci > MIN_FILES_FOR_PARALLEL_INCR); + + if (use_parallel) { + cbm_log_info("incremental.mode", "mode", "parallel", "workers", itoa_buf(worker_count), + "changed", itoa_buf(ci)); + + _Atomic int64_t shared_ids; + atomic_init(&shared_ids, cbm_gbuf_next_id(ctx->gbuf)); + + CBMFileResult **cache = (CBMFileResult **)calloc(ci, sizeof(CBMFileResult *)); + if (cache) { + cbm_clock_gettime(CLOCK_MONOTONIC, &t); + cbm_parallel_extract(ctx, changed_files, ci, cache, &shared_ids, worker_count); + cbm_gbuf_set_next_id(ctx->gbuf, atomic_load(&shared_ids)); + cbm_log_info("pass.timing", "pass", "incr_extract", "elapsed_ms", + itoa_buf((int)elapsed_ms(t))); + + cbm_clock_gettime(CLOCK_MONOTONIC, &t); + cbm_build_registry_from_cache(ctx, changed_files, ci, cache); + cbm_log_info("pass.timing", "pass", "incr_registry", "elapsed_ms", + itoa_buf((int)elapsed_ms(t))); + + cbm_clock_gettime(CLOCK_MONOTONIC, &t); + cbm_parallel_resolve(ctx, changed_files, ci, cache, &shared_ids, worker_count); + cbm_gbuf_set_next_id(ctx->gbuf, atomic_load(&shared_ids)); + cbm_log_info("pass.timing", "pass", "incr_resolve", "elapsed_ms", + itoa_buf((int)elapsed_ms(t))); + + for (int j = 0; j < ci; j++) { + if (cache[j]) { + cbm_free_result(cache[j]); + } + } + free(cache); + } + } else { + cbm_log_info("incremental.mode", "mode", "sequential", "changed", itoa_buf(ci)); + cbm_pipeline_pass_definitions(ctx, changed_files, ci); + cbm_pipeline_pass_calls(ctx, changed_files, ci); + cbm_pipeline_pass_usages(ctx, changed_files, ci); + cbm_pipeline_pass_semantic(ctx, changed_files, ci); + } +} + +/* Run post-extraction passes (tests, decorator tags, configlink, httplinks). */ +static void run_postpasses(cbm_pipeline_ctx_t *ctx, cbm_file_info_t *changed_files, int ci, + const char *project) { + struct timespec t; + + cbm_clock_gettime(CLOCK_MONOTONIC, &t); + cbm_pipeline_pass_tests(ctx, changed_files, ci); + cbm_log_info("pass.timing", "pass", "incr_tests", "elapsed_ms", itoa_buf((int)elapsed_ms(t))); + + cbm_clock_gettime(CLOCK_MONOTONIC, &t); + cbm_pipeline_pass_decorator_tags(ctx->gbuf, project); + cbm_log_info("pass.timing", "pass", "incr_decorator_tags", "elapsed_ms", + itoa_buf((int)elapsed_ms(t))); + + cbm_clock_gettime(CLOCK_MONOTONIC, &t); + cbm_pipeline_pass_configlink(ctx); + cbm_log_info("pass.timing", "pass", "incr_configlink", "elapsed_ms", + itoa_buf((int)elapsed_ms(t))); + + cbm_clock_gettime(CLOCK_MONOTONIC, &t); + cbm_pipeline_pass_httplinks(ctx); + cbm_log_info("pass.timing", "pass", "incr_httplinks", "elapsed_ms", + itoa_buf((int)elapsed_ms(t))); +} + +/* Delete old DB and dump merged graph + hashes to disk. */ +static void dump_and_persist(cbm_gbuf_t *gbuf, const char *db_path, const char *project, + cbm_file_info_t *files, int file_count) { + struct timespec t; + cbm_clock_gettime(CLOCK_MONOTONIC, &t); + + cbm_unlink(db_path); + char wal[1040]; + char shm[1040]; + snprintf(wal, sizeof(wal), "%s-wal", db_path); + snprintf(shm, sizeof(shm), "%s-shm", db_path); + cbm_unlink(wal); + cbm_unlink(shm); + + int dump_rc = cbm_gbuf_dump_to_sqlite(gbuf, db_path); + cbm_log_info("incremental.dump", "rc", itoa_buf(dump_rc), "elapsed_ms", + itoa_buf((int)elapsed_ms(t))); + + cbm_store_t *hash_store = cbm_store_open_path(db_path); + if (hash_store) { + persist_hashes(hash_store, project, files, file_count); + cbm_store_close(hash_store); + } +} + /* ── Incremental pipeline entry point ────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_file_info_t *files, int file_count) { struct timespec t0; @@ -227,18 +326,6 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil cbm_log_info("incremental.reparse", "files", itoa_buf(ci)); - /* ═══════════════════════════════════════════════════════════════ - * RAM-first incremental pipeline: - * 1. Load entire existing DB into a graph buffer (RAM) - * 2. Delete stale nodes (changed + removed files) from the gbuf - * 3. Parallel extract changed files into a FRESH gbuf - * 4. Populate registry from ALL nodes (existing + fresh) for - * cross-file resolution accuracy - * 5. Parallel resolve on changed files (using full registry) - * 6. Merge fresh gbuf into existing gbuf - * 7. Delete old DB, dump merged gbuf to disk - * ═══════════════════════════════════════════════════════════════ */ - struct timespec t; /* Step 1: Load existing graph into RAM */ @@ -262,19 +349,13 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil return -1; } - /* Close store — done reading, will delete + rewrite at the end */ cbm_store_close(store); - store = NULL; - /* Step 2: Delete changed/removed files' nodes from in-memory graph */ + /* Step 2: Purge stale nodes */ cbm_clock_gettime(CLOCK_MONOTONIC, &t); - cbm_log_info("incremental.pre_purge", "nodes", itoa_buf(cbm_gbuf_node_count(existing)), "edges", - itoa_buf(cbm_gbuf_edge_count(existing))); for (int i = 0; i < ci; i++) { cbm_gbuf_delete_by_file(existing, changed_files[i].rel_path); } - cbm_log_info("incremental.post_purge", "nodes", itoa_buf(cbm_gbuf_node_count(existing)), - "edges", itoa_buf(cbm_gbuf_edge_count(existing))); for (int i = 0; i < deleted_count; i++) { cbm_gbuf_delete_by_file(existing, deleted[i]); free(deleted[i]); @@ -282,14 +363,8 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil free(deleted); cbm_log_info("incremental.purge", "elapsed_ms", itoa_buf((int)elapsed_ms(t))); - /* Step 3: Extract + resolve directly into EXISTING gbuf. - * After purge, existing has all unchanged nodes. New nodes from changed - * files go straight in. The resolver sees ALL nodes — both old and new — - * so cross-file edges (changed → unchanged AND unchanged → changed) are - * created correctly. No separate gbuf, no merge, no blind spots. */ + /* Step 3-5: Registry + extract + resolve */ cbm_registry_t *registry = cbm_registry_new(); - - /* Step 4: Populate registry from ALL existing nodes */ cbm_clock_gettime(CLOCK_MONOTONIC, &t); cbm_gbuf_foreach_node(existing, registry_visitor, registry); cbm_log_info("incremental.registry_seed", "symbols", itoa_buf(cbm_registry_size(registry)), @@ -298,12 +373,11 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil cbm_pipeline_ctx_t ctx = { .project_name = project, .repo_path = cbm_pipeline_repo_path(p), - .gbuf = existing, /* extract + resolve directly into existing */ + .gbuf = existing, .registry = registry, .cancelled = cbm_pipeline_cancelled_ptr(p), }; - /* Create File nodes for changed files (purge deleted them) */ for (int i = 0; i < ci; i++) { char *file_qn = cbm_pipeline_fqn_compute(project, changed_files[i].rel_path, "__file__"); if (file_qn) { @@ -313,105 +387,15 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil } } -#define MIN_FILES_FOR_PARALLEL_INCR 50 - int worker_count = cbm_default_worker_count(true); - bool use_parallel = (worker_count > 1 && ci > MIN_FILES_FOR_PARALLEL_INCR); - - if (use_parallel) { - cbm_log_info("incremental.mode", "mode", "parallel", "workers", itoa_buf(worker_count), - "changed", itoa_buf(ci)); - - _Atomic int64_t shared_ids; - atomic_init(&shared_ids, cbm_gbuf_next_id(existing)); - - CBMFileResult **cache = (CBMFileResult **)calloc(ci, sizeof(CBMFileResult *)); - if (cache) { - cbm_clock_gettime(CLOCK_MONOTONIC, &t); - cbm_parallel_extract(&ctx, changed_files, ci, cache, &shared_ids, worker_count); - cbm_gbuf_set_next_id(existing, atomic_load(&shared_ids)); - cbm_log_info("pass.timing", "pass", "incr_extract", "elapsed_ms", - itoa_buf((int)elapsed_ms(t))); - - cbm_clock_gettime(CLOCK_MONOTONIC, &t); - cbm_build_registry_from_cache(&ctx, changed_files, ci, cache); - cbm_log_info("pass.timing", "pass", "incr_registry", "elapsed_ms", - itoa_buf((int)elapsed_ms(t))); - - cbm_clock_gettime(CLOCK_MONOTONIC, &t); - cbm_parallel_resolve(&ctx, changed_files, ci, cache, &shared_ids, worker_count); - cbm_gbuf_set_next_id(existing, atomic_load(&shared_ids)); - cbm_log_info("pass.timing", "pass", "incr_resolve", "elapsed_ms", - itoa_buf((int)elapsed_ms(t))); - - for (int j = 0; j < ci; j++) { - if (cache[j]) { - cbm_free_result(cache[j]); - } - } - free(cache); - } - } else { - cbm_log_info("incremental.mode", "mode", "sequential", "changed", itoa_buf(ci)); - cbm_pipeline_pass_definitions(&ctx, changed_files, ci); - cbm_pipeline_pass_calls(&ctx, changed_files, ci); - cbm_pipeline_pass_usages(&ctx, changed_files, ci); - cbm_pipeline_pass_semantic(&ctx, changed_files, ci); - } - + run_extract_resolve(&ctx, changed_files, ci); cbm_pipeline_pass_k8s(&ctx, changed_files, ci); - - /* Step 6: Post-passes on the full graph (gbuf-only, no disk reads). - * These operate on the merged graph after extraction + resolve. - * Unchanged files' post-pass edges were loaded from DB. - * Changed files' post-pass edges are created here. */ - - cbm_clock_gettime(CLOCK_MONOTONIC, &t); - cbm_pipeline_pass_tests(&ctx, changed_files, ci); - cbm_log_info("pass.timing", "pass", "incr_tests", "elapsed_ms", itoa_buf((int)elapsed_ms(t))); - - cbm_clock_gettime(CLOCK_MONOTONIC, &t); - cbm_pipeline_pass_decorator_tags(existing, project); - cbm_log_info("pass.timing", "pass", "incr_decorator_tags", "elapsed_ms", - itoa_buf((int)elapsed_ms(t))); - - /* Configlink: key_symbol + dep_imports are gbuf-only. - * file_refs uses disk fallback for config files (typically <10 files). */ - cbm_clock_gettime(CLOCK_MONOTONIC, &t); - cbm_pipeline_pass_configlink(&ctx); - cbm_log_info("pass.timing", "pass", "incr_configlink", "elapsed_ms", - itoa_buf((int)elapsed_ms(t))); - - /* Httplinks: decorator-based routes from gbuf properties + disk fallback - * for source-based routes. AST-based route registration already ran during resolve. */ - cbm_clock_gettime(CLOCK_MONOTONIC, &t); - cbm_pipeline_pass_httplinks(&ctx); - cbm_log_info("pass.timing", "pass", "incr_httplinks", "elapsed_ms", - itoa_buf((int)elapsed_ms(t))); + run_postpasses(&ctx, changed_files, ci, project); free(changed_files); cbm_registry_free(registry); - /* Step 7: Delete old DB, dump merged graph to disk */ - cbm_clock_gettime(CLOCK_MONOTONIC, &t); - cbm_unlink(db_path); - char wal[1040]; - char shm[1040]; - snprintf(wal, sizeof(wal), "%s-wal", db_path); - snprintf(shm, sizeof(shm), "%s-shm", db_path); - cbm_unlink(wal); - cbm_unlink(shm); - - int dump_rc = cbm_gbuf_dump_to_sqlite(existing, db_path); - cbm_log_info("incremental.dump", "rc", itoa_buf(dump_rc), "elapsed_ms", - itoa_buf((int)elapsed_ms(t))); - - /* Persist file hashes for ALL files */ - cbm_store_t *hash_store = cbm_store_open_path(db_path); - if (hash_store) { - persist_hashes(hash_store, project, files, file_count); - cbm_store_close(hash_store); - } - + /* Step 7: Dump to disk */ + dump_and_persist(existing, db_path, project, files, file_count); cbm_gbuf_free(existing); cbm_log_info("incremental.done", "elapsed_ms", itoa_buf((int)elapsed_ms(t0))); diff --git a/src/pipeline/pipeline_internal.h b/src/pipeline/pipeline_internal.h index b7cd7ece0..eeec6b542 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -284,7 +284,7 @@ char *cbm_infra_qn(const char *project_name, const char *rel_path, const char *i * Each worker creates nodes in a per-worker gbuf, then merges into ctx->gbuf. * Caches CBMFileResult* in result_cache[file_idx] for reuse in Phase 3B/4. * shared_ids provides globally unique node/edge IDs across workers. */ -int cbm_parallel_extract(cbm_pipeline_ctx_t *ctx, cbm_file_info_t *files, int file_count, +int cbm_parallel_extract(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, int file_count, CBMFileResult **result_cache, _Atomic int64_t *shared_ids, int worker_count); diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index 3927bc71a..38b251d82 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -341,8 +341,37 @@ static cbm_resolution_t resolve_same_module(const cbm_registry_t *r, const char return empty_result(); } +/* Strategy 4: multiple candidates with import filtering. */ +static cbm_resolution_t resolve_multi_with_imports(const qn_array_t *arr, const char *module_qn, + const char **import_vals, int import_count) { + const char *filtered[256]; + int fcount = 0; + for (int i = 0; i < arr->count && fcount < 256; i++) { + if (is_import_reachable(arr->items[i], import_vals, import_count)) { + filtered[fcount++] = arr->items[i]; + } + } + if (fcount == 1) { + double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, arr->count); + return (cbm_resolution_t){filtered[0], "suffix_match", conf, arr->count}; + } + if (fcount > 1) { + const char *best = best_by_import_distance(filtered, fcount, module_qn); + if (best) { + double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, fcount); + return (cbm_resolution_t){best, "suffix_match", conf, fcount}; + } + } + /* No import-reachable — use all candidates with penalty */ + const char *best = best_by_import_distance((const char **)arr->items, arr->count, module_qn); + if (best) { + double conf = candidate_count_penalty(CONF_SUFFIX_MATCH * 0.5, arr->count); + return (cbm_resolution_t){best, "suffix_match", conf, arr->count}; + } + return empty_result(); +} + /* Strategy 3+4: Name lookup + suffix match */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char *callee_name, const char *module_qn, const char **import_vals, int import_count) { @@ -362,42 +391,14 @@ static cbm_resolution_t resolve_name_lookup(const cbm_registry_t *r, const char return (cbm_resolution_t){arr->items[0], "unique_name", conf, 1}; } - /* Strategy 4: multiple candidates — filter by import reachability, then best by distance */ + /* Strategy 4: multiple candidates */ if (import_vals && import_count > 0) { - const char *filtered[256]; - int fcount = 0; - for (int i = 0; i < arr->count && fcount < 256; i++) { - if (is_import_reachable(arr->items[i], import_vals, import_count)) { - filtered[fcount++] = arr->items[i]; - } - } - if (fcount == 1) { - double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, arr->count); - return (cbm_resolution_t){filtered[0], "suffix_match", conf, arr->count}; - } - if (fcount > 1) { - const char *best = best_by_import_distance(filtered, fcount, module_qn); - if (best) { - double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, fcount); - return (cbm_resolution_t){best, "suffix_match", conf, fcount}; - } - } - if (fcount == 0) { - /* No import-reachable — use all candidates with penalty */ - const char *best = - best_by_import_distance((const char **)arr->items, arr->count, module_qn); - if (best) { - double conf = candidate_count_penalty(CONF_SUFFIX_MATCH * 0.5, arr->count); - return (cbm_resolution_t){best, "suffix_match", conf, arr->count}; - } - } - } else { - const char *best = - best_by_import_distance((const char **)arr->items, arr->count, module_qn); - if (best) { - double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, arr->count); - return (cbm_resolution_t){best, "suffix_match", conf, arr->count}; - } + return resolve_multi_with_imports(arr, module_qn, import_vals, import_count); + } + const char *best = best_by_import_distance((const char **)arr->items, arr->count, module_qn); + if (best) { + double conf = candidate_count_penalty(CONF_SUFFIX_MATCH, arr->count); + return (cbm_resolution_t){best, "suffix_match", conf, arr->count}; } return empty_result(); } diff --git a/src/store/store.c b/src/store/store.c index 29c72bf37..0a8269696 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -1616,7 +1616,49 @@ int cbm_store_node_neighbor_names(cbm_store_t *s, int64_t node_id, int limit, ch return 0; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +static int count_degrees_direction(cbm_store_t *s, const int64_t *node_ids, int id_count, + const char *in_clause, bool has_type, const char *edge_type, + bool inbound, int *out_counts) { + char sql[8192]; + const char *id_col = inbound ? "target_id" : "source_id"; + if (has_type) { + snprintf(sql, sizeof(sql), + "SELECT %s, COUNT(*) FROM edges " + "WHERE %s IN (%s) AND type = ? GROUP BY %s", + id_col, id_col, in_clause, id_col); + } else { + snprintf(sql, sizeof(sql), + "SELECT %s, COUNT(*) FROM edges " + "WHERE %s IN (%s) GROUP BY %s", + id_col, id_col, in_clause, id_col); + } + + sqlite3_stmt *stmt = NULL; + if (sqlite3_prepare_v2(s->db, sql, -1, &stmt, NULL) != SQLITE_OK) { + return CBM_STORE_ERR; + } + + for (int i = 0; i < id_count; i++) { + sqlite3_bind_int64(stmt, i + 1, node_ids[i]); + } + if (has_type) { + bind_text(stmt, id_count + 1, edge_type); + } + + while (sqlite3_step(stmt) == SQLITE_ROW) { + int64_t nid = sqlite3_column_int64(stmt, 0); + int cnt = sqlite3_column_int(stmt, 1); + for (int i = 0; i < id_count; i++) { + if (node_ids[i] == nid) { + out_counts[i] = cnt; + break; + } + } + } + sqlite3_finalize(stmt); + return CBM_STORE_OK; +} + int cbm_store_batch_count_degrees(cbm_store_t *s, const int64_t *node_ids, int id_count, const char *edge_type, int *out_in, int *out_out) { if (!s || !node_ids || id_count <= 0 || !out_in || !out_out) { @@ -1639,81 +1681,14 @@ int cbm_store_batch_count_degrees(cbm_store_t *s, const int64_t *node_ids, int i bool has_type = edge_type && edge_type[0] != '\0'; - /* Inbound: COUNT grouped by target_id */ - char sql[8192]; - if (has_type) { - snprintf(sql, sizeof(sql), - "SELECT target_id, COUNT(*) FROM edges " - "WHERE target_id IN (%s) AND type = ? GROUP BY target_id", - in_clause); - } else { - snprintf(sql, sizeof(sql), - "SELECT target_id, COUNT(*) FROM edges " - "WHERE target_id IN (%s) GROUP BY target_id", - in_clause); + int rc = count_degrees_direction(s, node_ids, id_count, in_clause, has_type, edge_type, true, + out_in); + if (rc != CBM_STORE_OK) { + return rc; } - sqlite3_stmt *stmt = NULL; - if (sqlite3_prepare_v2(s->db, sql, -1, &stmt, NULL) != SQLITE_OK) { - return CBM_STORE_ERR; - } - - for (int i = 0; i < id_count; i++) { - sqlite3_bind_int64(stmt, i + 1, node_ids[i]); - } - if (has_type) { - bind_text(stmt, id_count + 1, edge_type); - } - - while (sqlite3_step(stmt) == SQLITE_ROW) { - int64_t nid = sqlite3_column_int64(stmt, 0); - int cnt = sqlite3_column_int(stmt, 1); - for (int i = 0; i < id_count; i++) { - if (node_ids[i] == nid) { - out_in[i] = cnt; - break; - } - } - } - sqlite3_finalize(stmt); - - /* Outbound: COUNT grouped by source_id */ - if (has_type) { - snprintf(sql, sizeof(sql), - "SELECT source_id, COUNT(*) FROM edges " - "WHERE source_id IN (%s) AND type = ? GROUP BY source_id", - in_clause); - } else { - snprintf(sql, sizeof(sql), - "SELECT source_id, COUNT(*) FROM edges " - "WHERE source_id IN (%s) GROUP BY source_id", - in_clause); - } - - if (sqlite3_prepare_v2(s->db, sql, -1, &stmt, NULL) != SQLITE_OK) { - return CBM_STORE_ERR; - } - - for (int i = 0; i < id_count; i++) { - sqlite3_bind_int64(stmt, i + 1, node_ids[i]); - } - if (has_type) { - bind_text(stmt, id_count + 1, edge_type); - } - - while (sqlite3_step(stmt) == SQLITE_ROW) { - int64_t nid = sqlite3_column_int64(stmt, 0); - int cnt = sqlite3_column_int(stmt, 1); - for (int i = 0; i < id_count; i++) { - if (node_ids[i] == nid) { - out_out[i] = cnt; - break; - } - } - } - sqlite3_finalize(stmt); - - return CBM_STORE_OK; + return count_degrees_direction(s, node_ids, id_count, in_clause, has_type, edge_type, false, + out_out); } /* ── UpsertFileHashBatch ───────────────────────────────────────── */ @@ -1934,103 +1909,132 @@ const char *cbm_strip_case_flag(const char *pattern) { return buf; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Bind value tracker for dynamic query building */ +typedef struct { + const char *text; +} search_bind_t; + +static void search_apply_degree_filter(char *sql, size_t sql_sz, const cbm_search_params_t *p) { + bool has_degree_filter = (p->min_degree >= 0 || p->max_degree >= 0); + if (!has_degree_filter) { + return; + } + char inner_sql[4096]; + snprintf(inner_sql, sizeof(inner_sql), "%s", sql); + if (p->min_degree >= 0 && p->max_degree >= 0) { + snprintf(sql, sql_sz, + "SELECT * FROM (%s) WHERE (in_deg + out_deg) >= %d AND (in_deg + out_deg) <= %d", + inner_sql, p->min_degree, p->max_degree); + } else if (p->min_degree >= 0) { + snprintf(sql, sql_sz, "SELECT * FROM (%s) WHERE (in_deg + out_deg) >= %d", inner_sql, + p->min_degree); + } else { + snprintf(sql, sql_sz, "SELECT * FROM (%s) WHERE (in_deg + out_deg) <= %d", inner_sql, + p->max_degree); + } +} + +/* Append a WHERE clause fragment, joining with AND if not the first. */ +static int where_append(char *where, int where_sz, int wlen, int *nparams, const char *cond) { + if (*nparams > 0) { + wlen += snprintf(where + wlen, where_sz - wlen, " AND "); + } + wlen += snprintf(where + wlen, where_sz - wlen, "%s", cond); + (*nparams)++; + return wlen; +} + +/* Bind a text parameter and increment the bind index. */ +static void where_bind_text(search_bind_t *binds, int *bind_idx, const char *val) { + binds[*bind_idx].text = val; + (*bind_idx)++; +} + +/* Build exclude-labels NOT IN clause with bind placeholders. */ +static void search_build_exclude_labels(const char **labels, search_bind_t *binds, int *bind_idx, + char *clause, int clause_sz) { + int elen = snprintf(clause, clause_sz, "n.label NOT IN ("); + for (int i = 0; labels[i]; i++) { + if (i > 0) { + elen += snprintf(clause + elen, clause_sz - elen, ","); + if (elen >= clause_sz) { + elen = clause_sz - 1; + } + } + elen += snprintf(clause + elen, clause_sz - elen, "?%d", *bind_idx + 1); + if (elen >= clause_sz) { + elen = clause_sz - 1; + } + where_bind_text(binds, bind_idx, labels[i]); + } + snprintf(clause + elen, clause_sz - elen, ")"); +} + +static int search_build_where(const cbm_search_params_t *params, char *where, int where_sz, + search_bind_t *binds, int *bind_idx, char **like_pattern_out) { + int wlen = 0; + int nparams = 0; + *like_pattern_out = NULL; + + char bind_buf[64]; + + if (params->project) { + snprintf(bind_buf, sizeof(bind_buf), "n.project = ?%d", *bind_idx + 1); + wlen = where_append(where, where_sz, wlen, &nparams, bind_buf); + where_bind_text(binds, bind_idx, params->project); + } + if (params->label) { + snprintf(bind_buf, sizeof(bind_buf), "n.label = ?%d", *bind_idx + 1); + wlen = where_append(where, where_sz, wlen, &nparams, bind_buf); + where_bind_text(binds, bind_idx, params->label); + } + if (params->name_pattern) { + if (params->case_sensitive) { + snprintf(bind_buf, sizeof(bind_buf), "n.name REGEXP ?%d", *bind_idx + 1); + } else { + snprintf(bind_buf, sizeof(bind_buf), "iregexp(?%d, n.name)", *bind_idx + 1); + } + wlen = where_append(where, where_sz, wlen, &nparams, bind_buf); + where_bind_text(binds, bind_idx, params->name_pattern); + } + if (params->file_pattern) { + *like_pattern_out = cbm_glob_to_like(params->file_pattern); + snprintf(bind_buf, sizeof(bind_buf), "n.file_path LIKE ?%d", *bind_idx + 1); + wlen = where_append(where, where_sz, wlen, &nparams, bind_buf); + where_bind_text(binds, bind_idx, *like_pattern_out); + } + if (params->exclude_labels) { + char excl_clause[512]; + search_build_exclude_labels(params->exclude_labels, binds, bind_idx, excl_clause, + (int)sizeof(excl_clause)); + (void)where_append(where, where_sz, wlen, &nparams, excl_clause); + } + + return nparams; +} + int cbm_store_search(cbm_store_t *s, const cbm_search_params_t *params, cbm_search_output_t *out) { memset(out, 0, sizeof(*out)); if (!s || !s->db) { return CBM_STORE_ERR; } - /* Build WHERE clauses dynamically */ char sql[4096]; char count_sql[4096]; int bind_idx = 0; - /* We build a query that selects nodes with optional degree subqueries */ const char *select_cols = "SELECT n.id, n.project, n.label, n.name, n.qualified_name, " "n.file_path, n.start_line, n.end_line, n.properties, " "(SELECT COUNT(*) FROM edges e WHERE e.target_id = n.id AND e.type = 'CALLS') AS in_deg, " "(SELECT COUNT(*) FROM edges e WHERE e.source_id = n.id AND e.type = 'CALLS') AS out_deg "; - /* Start building WHERE */ char where[2048] = ""; - int wlen = 0; - int nparams = 0; - - /* Track bind values */ - struct { - enum { BV_TEXT } type; - const char *text; - } binds[16]; - -#define ADD_WHERE(cond) \ - do { \ - if (nparams > 0) \ - wlen += snprintf(where + wlen, sizeof(where) - wlen, " AND "); \ - wlen += snprintf(where + wlen, sizeof(where) - wlen, "%s", cond); \ - nparams++; \ - } while (0) - -#define BIND_TEXT(val) \ - do { \ - bind_idx++; \ - binds[bind_idx - 1].type = BV_TEXT; \ - binds[bind_idx - 1].text = val; \ - } while (0) - - char bind_buf[64]; + search_bind_t binds[16]; char *like_pattern = NULL; - if (params->project) { - snprintf(bind_buf, sizeof(bind_buf), "n.project = ?%d", bind_idx + 1); - ADD_WHERE(bind_buf); - BIND_TEXT(params->project); - } - if (params->label) { - snprintf(bind_buf, sizeof(bind_buf), "n.label = ?%d", bind_idx + 1); - ADD_WHERE(bind_buf); - BIND_TEXT(params->label); - } - if (params->name_pattern) { - if (params->case_sensitive) { - /* Case-sensitive: use built-in REGEXP operator */ - snprintf(bind_buf, sizeof(bind_buf), "n.name REGEXP ?%d", bind_idx + 1); - } else { - /* Case-insensitive: use iregexp() function call syntax */ - snprintf(bind_buf, sizeof(bind_buf), "iregexp(?%d, n.name)", bind_idx + 1); - } - ADD_WHERE(bind_buf); - BIND_TEXT(params->name_pattern); - } - if (params->file_pattern) { - like_pattern = cbm_glob_to_like(params->file_pattern); - snprintf(bind_buf, sizeof(bind_buf), "n.file_path LIKE ?%d", bind_idx + 1); - ADD_WHERE(bind_buf); - BIND_TEXT(like_pattern); - } - - /* Exclude labels: use parameterized placeholders to prevent SQL injection */ - if (params->exclude_labels) { - char excl_clause[512] = "n.label NOT IN ("; - int elen = (int)strlen(excl_clause); - for (int i = 0; params->exclude_labels[i]; i++) { - if (i > 0) { - elen += snprintf(excl_clause + elen, sizeof(excl_clause) - (size_t)elen, ","); - if (elen >= (int)sizeof(excl_clause)) { - elen = (int)sizeof(excl_clause) - 1; - } - } - elen += snprintf(excl_clause + elen, sizeof(excl_clause) - (size_t)elen, "?%d", - bind_idx + 1); - if (elen >= (int)sizeof(excl_clause)) { - elen = (int)sizeof(excl_clause) - 1; - } - BIND_TEXT(params->exclude_labels[i]); - } - snprintf(excl_clause + elen, sizeof(excl_clause) - (size_t)elen, ")"); - ADD_WHERE(excl_clause); - } + int nparams = + search_build_where(params, where, (int)sizeof(where), binds, &bind_idx, &like_pattern); /* Build full SQL */ if (nparams > 0) { @@ -2039,35 +2043,17 @@ int cbm_store_search(cbm_store_t *s, const cbm_search_params_t *params, cbm_sear snprintf(sql, sizeof(sql), "%s FROM nodes n", select_cols); } - /* Degree filters: -1 = no filter, 0+ = active filter. - * Wraps in subquery to filter on computed degree columns. */ + /* Degree filters */ bool has_degree_filter = (params->min_degree >= 0 || params->max_degree >= 0); - if (has_degree_filter) { - char inner_sql[4096]; - snprintf(inner_sql, sizeof(inner_sql), "%s", sql); - if (params->min_degree >= 0 && params->max_degree >= 0) { - snprintf( - sql, sizeof(sql), - "SELECT * FROM (%s) WHERE (in_deg + out_deg) >= %d AND (in_deg + out_deg) <= %d", - inner_sql, params->min_degree, params->max_degree); - } else if (params->min_degree >= 0) { - snprintf(sql, sizeof(sql), "SELECT * FROM (%s) WHERE (in_deg + out_deg) >= %d", - inner_sql, params->min_degree); - } else { - snprintf(sql, sizeof(sql), "SELECT * FROM (%s) WHERE (in_deg + out_deg) <= %d", - inner_sql, params->max_degree); - } - } + search_apply_degree_filter(sql, sizeof(sql), params); /* Count query (wrap the full query) */ snprintf(count_sql, sizeof(count_sql), "SELECT COUNT(*) FROM (%s)", sql); - /* Add ORDER BY + LIMIT. - * When degree filter wraps in subquery, column refs lose the "n." prefix. */ + /* Add ORDER BY + LIMIT */ int limit = params->limit > 0 ? params->limit : 500000; int offset = params->offset; - bool has_degree_wrap = has_degree_filter; - const char *name_col = has_degree_wrap ? "name" : "n.name"; + const char *name_col = has_degree_filter ? "name" : "n.name"; char order_limit[128]; snprintf(order_limit, sizeof(order_limit), " ORDER BY %s LIMIT %d OFFSET %d", name_col, limit, offset); @@ -2146,12 +2132,97 @@ void cbm_store_search_free(cbm_search_output_t *out) { /* ── BFS Traversal ──────────────────────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +static int bfs_collect_edges(cbm_store_t *s, int64_t start_id, const cbm_node_hop_t *visited, + int visited_count, const char *types_clause, const char **edge_types, + int edge_type_count, cbm_edge_info_t **out_edges, + int *out_edge_count) { + /* Build ID set: root + all visited */ + char id_set[4096]; + int ilen = snprintf(id_set, sizeof(id_set), "%lld", (long long)start_id); + if (ilen >= (int)sizeof(id_set)) { + ilen = (int)sizeof(id_set) - 1; + } + for (int i = 0; i < visited_count; i++) { + ilen += snprintf(id_set + ilen, sizeof(id_set) - (size_t)ilen, ",%lld", + (long long)visited[i].node.id); + if (ilen >= (int)sizeof(id_set)) { + ilen = (int)sizeof(id_set) - 1; + } + } + + char edge_sql[8192]; + snprintf(edge_sql, sizeof(edge_sql), + "SELECT n1.name, n2.name, e.type " + "FROM edges e " + "JOIN nodes n1 ON n1.id = e.source_id " + "JOIN nodes n2 ON n2.id = e.target_id " + "WHERE e.source_id IN (%s) AND e.target_id IN (%s) " + "AND e.type IN (%s)", + id_set, id_set, types_clause); + + sqlite3_stmt *estmt = NULL; + int rc = sqlite3_prepare_v2(s->db, edge_sql, -1, &estmt, NULL); + if (rc != SQLITE_OK) { + *out_edges = NULL; + *out_edge_count = 0; + return CBM_STORE_OK; + } + + if (edge_type_count > 0) { + for (int i = 0; i < edge_type_count; i++) { + bind_text(estmt, i + 1, edge_types[i]); + } + } else { + bind_text(estmt, 1, "CALLS"); + } + + int ecap = 8; + int en = 0; + cbm_edge_info_t *edges = malloc(ecap * sizeof(cbm_edge_info_t)); + + while (sqlite3_step(estmt) == SQLITE_ROW) { + if (en >= ecap) { + ecap *= 2; + edges = safe_realloc(edges, ecap * sizeof(cbm_edge_info_t)); + } + edges[en].from_name = heap_strdup((const char *)sqlite3_column_text(estmt, 0)); + edges[en].to_name = heap_strdup((const char *)sqlite3_column_text(estmt, 1)); + edges[en].type = heap_strdup((const char *)sqlite3_column_text(estmt, 2)); + edges[en].confidence = 1.0; + en++; + } + sqlite3_finalize(estmt); + + *out_edges = edges; + *out_edge_count = en; + return CBM_STORE_OK; +} + +/* Build parameterized placeholder list "?1,?2,?3" for N edge types. */ +static void bfs_build_types_clause(int edge_type_count, char *buf, int buf_sz) { + if (edge_type_count <= 0) { + snprintf(buf, buf_sz, "?1"); + return; + } + int tlen = 0; + for (int i = 0; i < edge_type_count; i++) { + if (i > 0) { + tlen += snprintf(buf + tlen, buf_sz - tlen, ","); + if (tlen >= buf_sz) { + tlen = buf_sz - 1; + } + } + tlen += snprintf(buf + tlen, buf_sz - tlen, "?%d", i + 1); + if (tlen >= buf_sz) { + tlen = buf_sz - 1; + } + } +} + int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const char **edge_types, int edge_type_count, int max_depth, int max_results, cbm_traverse_result_t *out) { memset(out, 0, sizeof(*out)); - /* Load root node */ cbm_node_t root = {0}; int rc = cbm_store_find_node_by_id(s, start_id, &root); if (rc != CBM_STORE_OK) { @@ -2159,25 +2230,8 @@ int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const } out->root = root; - /* Build edge type IN clause with parameterized placeholders */ - char types_clause[512] = "?1"; - const char *default_edge_type = "CALLS"; - if (edge_type_count > 0) { - int tlen = 0; - for (int i = 0; i < edge_type_count; i++) { - if (i > 0) { - tlen += snprintf(types_clause + tlen, sizeof(types_clause) - (size_t)tlen, ","); - if (tlen >= (int)sizeof(types_clause)) { - tlen = (int)sizeof(types_clause) - 1; - } - } - tlen += - snprintf(types_clause + tlen, sizeof(types_clause) - (size_t)tlen, "?%d", i + 1); - if (tlen >= (int)sizeof(types_clause)) { - tlen = (int)sizeof(types_clause) - 1; - } - } - } + char types_clause[512]; + bfs_build_types_clause(edge_type_count, types_clause, (int)sizeof(types_clause)); /* Build recursive CTE for BFS */ char sql[4096]; @@ -2224,7 +2278,7 @@ int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const bind_text(stmt, i + 1, edge_types[i]); } } else { - bind_text(stmt, 1, default_edge_type); + bind_text(stmt, 1, "CALLS"); } int cap = 16; @@ -2248,62 +2302,8 @@ int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const /* Collect edges between visited nodes (including root) */ if (n > 0) { - /* Build ID set: root + all visited */ - char id_set[4096]; - int ilen = snprintf(id_set, sizeof(id_set), "%lld", (long long)start_id); - if (ilen >= (int)sizeof(id_set)) { - ilen = (int)sizeof(id_set) - 1; - } - for (int i = 0; i < n; i++) { - ilen += snprintf(id_set + ilen, sizeof(id_set) - (size_t)ilen, ",%lld", - (long long)out->visited[i].node.id); - if (ilen >= (int)sizeof(id_set)) { - ilen = (int)sizeof(id_set) - 1; - } - } - - char edge_sql[8192]; - snprintf(edge_sql, sizeof(edge_sql), - "SELECT n1.name, n2.name, e.type " - "FROM edges e " - "JOIN nodes n1 ON n1.id = e.source_id " - "JOIN nodes n2 ON n2.id = e.target_id " - "WHERE e.source_id IN (%s) AND e.target_id IN (%s) " - "AND e.type IN (%s)", - id_set, id_set, types_clause); - - sqlite3_stmt *estmt = NULL; - rc = sqlite3_prepare_v2(s->db, edge_sql, -1, &estmt, NULL); - if (rc == SQLITE_OK) { - /* Bind edge type parameters for the edge query */ - if (edge_type_count > 0) { - for (int i = 0; i < edge_type_count; i++) { - bind_text(estmt, i + 1, edge_types[i]); - } - } else { - bind_text(estmt, 1, default_edge_type); - } - - int ecap = 8; - int en = 0; - cbm_edge_info_t *edges = malloc(ecap * sizeof(cbm_edge_info_t)); - - while (sqlite3_step(estmt) == SQLITE_ROW) { - if (en >= ecap) { - ecap *= 2; - edges = safe_realloc(edges, ecap * sizeof(cbm_edge_info_t)); - } - edges[en].from_name = heap_strdup((const char *)sqlite3_column_text(estmt, 0)); - edges[en].to_name = heap_strdup((const char *)sqlite3_column_text(estmt, 1)); - edges[en].type = heap_strdup((const char *)sqlite3_column_text(estmt, 2)); - edges[en].confidence = 1.0; - en++; - } - sqlite3_finalize(estmt); - - out->edges = edges; - out->edge_count = en; - } + bfs_collect_edges(s, start_id, out->visited, n, types_clause, edge_types, edge_type_count, + &out->edges, &out->edge_count); } else { out->edges = NULL; out->edge_count = 0; @@ -2608,114 +2608,35 @@ bool cbm_is_test_file_path(const char *fp) { return strstr(fp, "test") != NULL; } -/* File extension → language name mapping */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* File extension → language name mapping (table-driven) */ +typedef struct { + const char *ext; + const char *lang; +} ext_lang_entry_t; + +static const ext_lang_entry_t ext_lang_table[] = { + {".py", "Python"}, {".go", "Go"}, {".js", "JavaScript"}, {".jsx", "JavaScript"}, + {".ts", "TypeScript"}, {".tsx", "TypeScript"}, {".rs", "Rust"}, {".java", "Java"}, + {".cpp", "C++"}, {".cc", "C++"}, {".cxx", "C++"}, {".c", "C"}, + {".h", "C"}, {".cs", "C#"}, {".php", "PHP"}, {".lua", "Lua"}, + {".scala", "Scala"}, {".kt", "Kotlin"}, {".rb", "Ruby"}, {".sh", "Bash"}, + {".bash", "Bash"}, {".zig", "Zig"}, {".ex", "Elixir"}, {".exs", "Elixir"}, + {".hs", "Haskell"}, {".ml", "OCaml"}, {".mli", "OCaml"}, {".html", "HTML"}, + {".css", "CSS"}, {".yaml", "YAML"}, {".yml", "YAML"}, {".toml", "TOML"}, + {".hcl", "HCL"}, {".tf", "HCL"}, {".sql", "SQL"}, {".erl", "Erlang"}, + {".swift", "Swift"}, {".dart", "Dart"}, {".groovy", "Groovy"}, {".pl", "Perl"}, + {".r", "R"}, {".scss", "SCSS"}, {".vue", "Vue"}, {".svelte", "Svelte"}, + {NULL, NULL}, +}; + static const char *ext_to_lang(const char *ext) { if (!ext) { return NULL; } - /* Common extensions */ - if (strcmp(ext, ".py") == 0) { - return "Python"; - } - if (strcmp(ext, ".go") == 0) { - return "Go"; - } - if (strcmp(ext, ".js") == 0 || strcmp(ext, ".jsx") == 0) { - return "JavaScript"; - } - if (strcmp(ext, ".ts") == 0 || strcmp(ext, ".tsx") == 0) { - return "TypeScript"; - } - if (strcmp(ext, ".rs") == 0) { - return "Rust"; - } - if (strcmp(ext, ".java") == 0) { - return "Java"; - } - if (strcmp(ext, ".cpp") == 0 || strcmp(ext, ".cc") == 0 || strcmp(ext, ".cxx") == 0) { - return "C++"; - } - if (strcmp(ext, ".c") == 0 || strcmp(ext, ".h") == 0) { - return "C"; - } - if (strcmp(ext, ".cs") == 0) { - return "C#"; - } - if (strcmp(ext, ".php") == 0) { - return "PHP"; - } - if (strcmp(ext, ".lua") == 0) { - return "Lua"; - } - if (strcmp(ext, ".scala") == 0) { - return "Scala"; - } - if (strcmp(ext, ".kt") == 0) { - return "Kotlin"; - } - if (strcmp(ext, ".rb") == 0) { - return "Ruby"; - } - if (strcmp(ext, ".sh") == 0 || strcmp(ext, ".bash") == 0) { - return "Bash"; - } - if (strcmp(ext, ".zig") == 0) { - return "Zig"; - } - if (strcmp(ext, ".ex") == 0 || strcmp(ext, ".exs") == 0) { - return "Elixir"; - } - if (strcmp(ext, ".hs") == 0) { - return "Haskell"; - } - if (strcmp(ext, ".ml") == 0 || strcmp(ext, ".mli") == 0) { - return "OCaml"; - } - if (strcmp(ext, ".html") == 0) { - return "HTML"; - } - if (strcmp(ext, ".css") == 0) { - return "CSS"; - } - if (strcmp(ext, ".yaml") == 0 || strcmp(ext, ".yml") == 0) { - return "YAML"; - } - if (strcmp(ext, ".toml") == 0) { - return "TOML"; - } - if (strcmp(ext, ".hcl") == 0 || strcmp(ext, ".tf") == 0) { - return "HCL"; - } - if (strcmp(ext, ".sql") == 0) { - return "SQL"; - } - if (strcmp(ext, ".erl") == 0) { - return "Erlang"; - } - if (strcmp(ext, ".swift") == 0) { - return "Swift"; - } - if (strcmp(ext, ".dart") == 0) { - return "Dart"; - } - if (strcmp(ext, ".groovy") == 0) { - return "Groovy"; - } - if (strcmp(ext, ".pl") == 0) { - return "Perl"; - } - if (strcmp(ext, ".r") == 0) { - return "R"; - } - if (strcmp(ext, ".scss") == 0) { - return "SCSS"; - } - if (strcmp(ext, ".vue") == 0) { - return "Vue"; - } - if (strcmp(ext, ".svelte") == 0) { - return "Svelte"; + for (const ext_lang_entry_t *e = ext_lang_table; e->ext; e++) { + if (strcmp(ext, e->ext) == 0) { + return e->lang; + } } return NULL; } @@ -2839,7 +2760,30 @@ static int arch_entry_points(cbm_store_t *s, const char *project, cbm_architectu return CBM_STORE_OK; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Extract a JSON string value from a simple JSON object by key name. */ +static char *extract_json_string_prop(const char *json, const char *key, int key_len) { + if (!json) { + return NULL; + } + const char *m = strstr(json, key); + if (!m) { + return NULL; + } + m = strchr(m + key_len, '"'); + if (!m) { + return NULL; + } + m++; + const char *end = strchr(m, '"'); + if (!end || end - m >= 256) { + return NULL; + } + char vbuf[256]; + memcpy(vbuf, m, end - m); + vbuf[end - m] = '\0'; + return heap_strdup(vbuf); +} + static int arch_routes(cbm_store_t *s, const char *project, cbm_architecture_info_t *out) { const char *sql = "SELECT name, properties, COALESCE(file_path, '') FROM nodes " "WHERE project=?1 AND label='Route' " @@ -2868,57 +2812,25 @@ static int arch_routes(cbm_store_t *s, const char *project, cbm_architecture_inf arr = safe_realloc(arr, cap * sizeof(cbm_route_info_t)); } - /* Parse JSON properties for method, path, handler */ arr[n].method = heap_strdup(""); arr[n].path = heap_strdup(name); arr[n].handler = heap_strdup(""); - if (props) { - /* Simple JSON extraction — find "method":"...", "path":"...", "handler":"..." */ - const char *m; - char vbuf[256]; - m = strstr(props, "\"method\""); - if (m) { - m = strchr(m + 8, '"'); - if (m) { - m++; - const char *end = strchr(m, '"'); - if (end && end - m < (int)sizeof(vbuf)) { - memcpy(vbuf, m, end - m); - vbuf[end - m] = '\0'; - free((void *)arr[n].method); - arr[n].method = heap_strdup(vbuf); - } - } - } - m = strstr(props, "\"path\""); - if (m) { - m = strchr(m + 6, '"'); - if (m) { - m++; - const char *end = strchr(m, '"'); - if (end && end - m < (int)sizeof(vbuf)) { - memcpy(vbuf, m, end - m); - vbuf[end - m] = '\0'; - free((void *)arr[n].path); - arr[n].path = heap_strdup(vbuf); - } - } - } - m = strstr(props, "\"handler\""); - if (m) { - m = strchr(m + 9, '"'); - if (m) { - m++; - const char *end = strchr(m, '"'); - if (end && end - m < (int)sizeof(vbuf)) { - memcpy(vbuf, m, end - m); - vbuf[end - m] = '\0'; - free((void *)arr[n].handler); - arr[n].handler = heap_strdup(vbuf); - } - } - } + char *val; + val = extract_json_string_prop(props, "\"method\"", 8); + if (val) { + free((void *)arr[n].method); + arr[n].method = val; + } + val = extract_json_string_prop(props, "\"path\"", 6); + if (val) { + free((void *)arr[n].path); + arr[n].path = val; + } + val = extract_json_string_prop(props, "\"handler\"", 9); + if (val) { + free((void *)arr[n].handler); + arr[n].handler = val; } n++; } @@ -2962,7 +2874,36 @@ static int arch_hotspots(cbm_store_t *s, const char *project, cbm_architecture_i return CBM_STORE_OK; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Look up package name for a node ID in the parallel arrays. */ +static const char *lookup_pkg(const int64_t *nids, char **npkgs, int nn, int64_t id) { + for (int i = 0; i < nn; i++) { + if (nids[i] == id) { + return npkgs[i]; + } + } + return NULL; +} + +/* Accumulate a cross-package boundary into parallel arrays. */ +static void accum_boundary(const char *src_pkg, const char *tgt_pkg, char **bfroms, char **btos, + int *bcounts, int *bn, int bcap) { + int found = -1; + for (int i = 0; i < *bn; i++) { + if (strcmp(bfroms[i], src_pkg) == 0 && strcmp(btos[i], tgt_pkg) == 0) { + found = i; + break; + } + } + if (found >= 0) { + bcounts[found]++; + } else if (*bn < bcap) { + bfroms[*bn] = heap_strdup(src_pkg); + btos[*bn] = heap_strdup(tgt_pkg); + bcounts[*bn] = 1; + (*bn)++; + } +} + static int arch_boundaries(cbm_store_t *s, const char *project, cbm_cross_pkg_boundary_t **out_arr, int *out_count) { /* Build nodeID → package map */ @@ -2975,7 +2916,6 @@ static int arch_boundaries(cbm_store_t *s, const char *project, cbm_cross_pkg_bo } bind_text(nstmt, 1, project); - /* Simple parallel arrays for node → package mapping */ int ncap = 256; int nn = 0; int64_t *nids = malloc(ncap * sizeof(int64_t)); @@ -3008,7 +2948,6 @@ static int arch_boundaries(cbm_store_t *s, const char *project, cbm_cross_pkg_bo } bind_text(estmt, 1, project); - /* Boundary counts: parallel arrays for from→to→count */ int bcap = 32; int bn = 0; char **bfroms = malloc(bcap * sizeof(char *)); @@ -3018,35 +2957,12 @@ static int arch_boundaries(cbm_store_t *s, const char *project, cbm_cross_pkg_bo while (sqlite3_step(estmt) == SQLITE_ROW) { int64_t src_id = sqlite3_column_int64(estmt, 0); int64_t tgt_id = sqlite3_column_int64(estmt, 1); - const char *src_pkg = NULL; - const char *tgt_pkg = NULL; - for (int i = 0; i < nn; i++) { - if (nids[i] == src_id) { - src_pkg = npkgs[i]; - } - if (nids[i] == tgt_id) { - tgt_pkg = npkgs[i]; - } - } + const char *src_pkg = lookup_pkg(nids, npkgs, nn, src_id); + const char *tgt_pkg = lookup_pkg(nids, npkgs, nn, tgt_id); if (!src_pkg || !tgt_pkg || !src_pkg[0] || !tgt_pkg[0] || strcmp(src_pkg, tgt_pkg) == 0) { continue; } - - int found = -1; - for (int i = 0; i < bn; i++) { - if (strcmp(bfroms[i], src_pkg) == 0 && strcmp(btos[i], tgt_pkg) == 0) { - found = i; - break; - } - } - if (found >= 0) { - bcounts[found]++; - } else if (bn < bcap) { - bfroms[bn] = heap_strdup(src_pkg); - btos[bn] = heap_strdup(tgt_pkg); - bcounts[bn] = 1; - bn++; - } + accum_boundary(src_pkg, tgt_pkg, bfroms, btos, bcounts, &bn, bcap); } sqlite3_finalize(estmt); for (int i = 0; i < nn; i++) { @@ -3093,7 +3009,76 @@ static int arch_boundaries(cbm_store_t *s, const char *project, cbm_cross_pkg_bo return CBM_STORE_OK; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +#define MAX_PREVIEW_NAMES 15 + +/* Fallback: derive packages from QN segments when no Package nodes exist. */ +static int arch_packages_from_qn(cbm_store_t *s, const char *project, + cbm_package_summary_t **out_arr, int *out_count) { + const char *qsql = "SELECT qualified_name FROM nodes WHERE project=?1 AND label IN " + "('Function','Method','Class')"; + sqlite3_stmt *stmt = NULL; + if (sqlite3_prepare_v2(s->db, qsql, -1, &stmt, NULL) != SQLITE_OK) { + store_set_error_sqlite(s, "arch_packages_qn"); + return CBM_STORE_ERR; + } + bind_text(stmt, 1, project); + + char *pnames[64]; + int pcounts[64]; + int np = 0; + while (sqlite3_step(stmt) == SQLITE_ROW) { + const char *qn = (const char *)sqlite3_column_text(stmt, 0); + const char *pkg = cbm_qn_to_package(qn); + if (!pkg[0]) { + continue; + } + int found = -1; + for (int i = 0; i < np; i++) { + if (strcmp(pnames[i], pkg) == 0) { + found = i; + break; + } + } + if (found >= 0) { + pcounts[found]++; + } else if (np < 64) { + pnames[np] = heap_strdup(pkg); + pcounts[np] = 1; + np++; + } + } + sqlite3_finalize(stmt); + + /* Sort by count desc */ + for (int i = 1; i < np; i++) { + int j = i; + while (j > 0 && pcounts[j] > pcounts[j - 1]) { + int tc = pcounts[j]; + pcounts[j] = pcounts[j - 1]; + pcounts[j - 1] = tc; + char *tn = pnames[j]; + pnames[j] = pnames[j - 1]; + pnames[j - 1] = tn; + j--; + } + } + if (np > MAX_PREVIEW_NAMES) { + for (int i = MAX_PREVIEW_NAMES; i < np; i++) { + free(pnames[i]); + } + np = MAX_PREVIEW_NAMES; + } + + cbm_package_summary_t *arr = calloc(np, sizeof(cbm_package_summary_t)); + for (int i = 0; i < np; i++) { + arr[i].name = pnames[i]; + arr[i].node_count = pcounts[i]; + } + *out_arr = arr; + *out_count = np; + return CBM_STORE_OK; +} + static int arch_packages(cbm_store_t *s, const char *project, cbm_architecture_info_t *out) { /* Try Package nodes first */ const char *sql = @@ -3123,69 +3108,9 @@ static int arch_packages(cbm_store_t *s, const char *project, cbm_architecture_i /* Fallback: group by QN segment if no Package nodes */ if (n == 0) { free(arr); - const char *qsql = "SELECT qualified_name FROM nodes WHERE project=?1 AND label IN " - "('Function','Method','Class')"; - if (sqlite3_prepare_v2(s->db, qsql, -1, &stmt, NULL) != SQLITE_OK) { - store_set_error_sqlite(s, "arch_packages_qn"); - return CBM_STORE_ERR; - } - bind_text(stmt, 1, project); - - /* Count per package using parallel arrays */ - char *pnames[64]; - int pcounts[64]; - int np = 0; - while (sqlite3_step(stmt) == SQLITE_ROW) { - const char *qn = (const char *)sqlite3_column_text(stmt, 0); - const char *pkg = cbm_qn_to_package(qn); - if (!pkg[0]) { - continue; - } - int found = -1; - for (int i = 0; i < np; i++) { - if (strcmp(pnames[i], pkg) == 0) { - found = i; - break; - } - } - if (found >= 0) { - { - pcounts[found]++; - } - } else if (np < 64) { - pnames[np] = heap_strdup(pkg); - pcounts[np] = 1; - np++; - } - } - sqlite3_finalize(stmt); - - /* Sort by count desc */ - for (int i = 1; i < np; i++) { - int j = i; - while (j > 0 && pcounts[j] > pcounts[j - 1]) { - int tc = pcounts[j]; - pcounts[j] = pcounts[j - 1]; - pcounts[j - 1] = tc; - char *tn = pnames[j]; - pnames[j] = pnames[j - 1]; - pnames[j - 1] = tn; - j--; - } - } -#define MAX_PREVIEW_NAMES 15 - if (np > MAX_PREVIEW_NAMES) { - for (int i = MAX_PREVIEW_NAMES; i < np; i++) { - free(pnames[i]); - } - np = MAX_PREVIEW_NAMES; - } - - arr = calloc(np, sizeof(cbm_package_summary_t)); - n = np; - for (int i = 0; i < np; i++) { - arr[i].name = pnames[i]; - arr[i].node_count = pcounts[i]; + int rc = arch_packages_from_qn(s, project, &arr, &n); + if (rc != CBM_STORE_OK) { + return rc; } } @@ -3229,7 +3154,47 @@ static void classify_layer(const char *pkg, int in, int out_deg, bool has_routes (void)pkg; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Find or insert a package name, returning its index. Returns -1 if full. */ +static int find_or_add_pkg(char **all_pkgs, int *npkgs, int max_pkgs, const char *pkg) { + for (int j = 0; j < *npkgs; j++) { + if (strcmp(all_pkgs[j], pkg) == 0) { + return j; + } + } + if (*npkgs < max_pkgs) { + int idx = *npkgs; + all_pkgs[idx] = heap_strdup(pkg); + (*npkgs)++; + return idx; + } + return -1; +} + +/* Check if a package name appears in an array. */ +static bool pkg_in_list(const char *pkg, char **list, int count) { + for (int j = 0; j < count; j++) { + if (strcmp(pkg, list[j]) == 0) { + return true; + } + } + return false; +} + +/* Collect package names from nodes matching a SQL query. */ +static int collect_pkg_names(cbm_store_t *s, const char *sql, const char *project, char **pkgs, + int max_pkgs) { + sqlite3_stmt *stmt = NULL; + sqlite3_prepare_v2(s->db, sql, -1, &stmt, NULL); + bind_text(stmt, 1, project); + int count = 0; + while (sqlite3_step(stmt) == SQLITE_ROW && count < max_pkgs) { + const char *qn = (const char *)sqlite3_column_text(stmt, 0); + pkgs[count++] = heap_strdup(cbm_qn_to_package(qn)); + } + sqlite3_finalize(stmt); + return count; +} + static int arch_layers(cbm_store_t *s, const char *project, cbm_architecture_info_t *out) { /* Get boundaries for fan analysis */ cbm_cross_pkg_boundary_t *boundaries = NULL; @@ -3239,36 +3204,17 @@ static int arch_layers(cbm_store_t *s, const char *project, cbm_architecture_inf return rc; } - /* Check which packages have Route nodes */ + /* Collect route and entry point packages */ char *route_pkgs[32]; - int nrpkgs = 0; - { - const char *sql = "SELECT qualified_name FROM nodes WHERE project=?1 AND label='Route'"; - sqlite3_stmt *stmt = NULL; - sqlite3_prepare_v2(s->db, sql, -1, &stmt, NULL); - bind_text(stmt, 1, project); - while (sqlite3_step(stmt) == SQLITE_ROW && nrpkgs < 32) { - const char *qn = (const char *)sqlite3_column_text(stmt, 0); - route_pkgs[nrpkgs++] = heap_strdup(cbm_qn_to_package(qn)); - } - sqlite3_finalize(stmt); - } + int nrpkgs = + collect_pkg_names(s, "SELECT qualified_name FROM nodes WHERE project=?1 AND label='Route'", + project, route_pkgs, 32); - /* Check which packages have entry points */ char *entry_pkgs[32]; - int nepkgs = 0; - { - const char *sql = "SELECT qualified_name FROM nodes WHERE project=?1 AND " - "json_extract(properties, '$.is_entry_point') = 1"; - sqlite3_stmt *stmt = NULL; - sqlite3_prepare_v2(s->db, sql, -1, &stmt, NULL); - bind_text(stmt, 1, project); - while (sqlite3_step(stmt) == SQLITE_ROW && nepkgs < 32) { - const char *qn = (const char *)sqlite3_column_text(stmt, 0); - entry_pkgs[nepkgs++] = heap_strdup(cbm_qn_to_package(qn)); - } - sqlite3_finalize(stmt); - } + int nepkgs = collect_pkg_names(s, + "SELECT qualified_name FROM nodes WHERE project=?1 AND " + "json_extract(properties, '$.is_entry_point') = 1", + project, entry_pkgs, 32); /* Compute fan-in/out per package */ char *all_pkgs[64]; @@ -3279,35 +3225,11 @@ static int arch_layers(cbm_store_t *s, const char *project, cbm_architecture_inf memset(fan_out, 0, sizeof(fan_out)); for (int i = 0; i < bcount; i++) { - /* Add or find "from" package */ - int fi = -1; - for (int j = 0; j < npkgs; j++) { - if (strcmp(all_pkgs[j], boundaries[i].from) == 0) { - fi = j; - break; - } - } - if (fi < 0 && npkgs < 64) { - fi = npkgs; - all_pkgs[npkgs] = heap_strdup(boundaries[i].from); - npkgs++; - } + int fi = find_or_add_pkg(all_pkgs, &npkgs, 64, boundaries[i].from); if (fi >= 0) { fan_out[fi] += boundaries[i].call_count; } - - int ti = -1; - for (int j = 0; j < npkgs; j++) { - if (strcmp(all_pkgs[j], boundaries[i].to) == 0) { - ti = j; - break; - } - } - if (ti < 0 && npkgs < 64) { - ti = npkgs; - all_pkgs[npkgs] = heap_strdup(boundaries[i].to); - npkgs++; - } + int ti = find_or_add_pkg(all_pkgs, &npkgs, 64, boundaries[i].to); if (ti >= 0) { fan_in[ti] += boundaries[i].call_count; } @@ -3315,49 +3237,18 @@ static int arch_layers(cbm_store_t *s, const char *project, cbm_architecture_inf /* Also include route/entry packages */ for (int i = 0; i < nrpkgs; i++) { - int found = -1; - for (int j = 0; j < npkgs; j++) { - if (strcmp(all_pkgs[j], route_pkgs[i]) == 0) { - found = j; - break; - } - } - if (found < 0 && npkgs < 64) { - all_pkgs[npkgs] = heap_strdup(route_pkgs[i]); - npkgs++; - } + find_or_add_pkg(all_pkgs, &npkgs, 64, route_pkgs[i]); } for (int i = 0; i < nepkgs; i++) { - int found = -1; - for (int j = 0; j < npkgs; j++) { - if (strcmp(all_pkgs[j], entry_pkgs[i]) == 0) { - found = j; - break; - } - } - if (found < 0 && npkgs < 64) { - all_pkgs[npkgs] = heap_strdup(entry_pkgs[i]); - npkgs++; - } + find_or_add_pkg(all_pkgs, &npkgs, 64, entry_pkgs[i]); } /* Classify each package */ out->layers = calloc(npkgs, sizeof(cbm_package_layer_t)); out->layer_count = npkgs; for (int i = 0; i < npkgs; i++) { - bool has_route = false, has_entry = false; - for (int j = 0; j < nrpkgs; j++) { - if (strcmp(all_pkgs[i], route_pkgs[j]) == 0) { - has_route = true; - break; - } - } - for (int j = 0; j < nepkgs; j++) { - if (strcmp(all_pkgs[i], entry_pkgs[j]) == 0) { - has_entry = true; - break; - } - } + bool has_route = pkg_in_list(all_pkgs[i], route_pkgs, nrpkgs); + bool has_entry = pkg_in_list(all_pkgs[i], entry_pkgs, nepkgs); const char *layer; const char *reason; classify_layer(all_pkgs[i], fan_in[i], fan_out[i], has_route, has_entry, &layer, &reason); @@ -3393,151 +3284,139 @@ static int arch_layers(cbm_store_t *s, const char *project, cbm_architecture_inf return CBM_STORE_OK; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity,readability-function-size) -static int arch_file_tree(cbm_store_t *s, const char *project, cbm_architecture_info_t *out) { - const char *sql = "SELECT file_path FROM nodes WHERE project=?1 AND label='File'"; - sqlite3_stmt *stmt = NULL; - if (sqlite3_prepare_v2(s->db, sql, -1, &stmt, NULL) != SQLITE_OK) { - store_set_error_sqlite(s, "arch_file_tree"); - return CBM_STORE_ERR; +/* Add a child to a dir entry if not already present. */ +static void dir_add_child(char ***children, int *child_count, int *child_cap, const char *child) { + for (int k = 0; k < *child_count; k++) { + if (strcmp((*children)[k], child) == 0) { + return; + } } - bind_text(stmt, 1, project); + if (*child_count >= *child_cap) { + *child_cap = *child_cap ? *child_cap * 2 : 4; + *children = realloc(*children, *child_cap * sizeof(char *)); + } + (*children)[(*child_count)++] = heap_strdup(child); +} - /* Collect all file paths + build directory children map */ - int fcap = 32; - int fn = 0; - char **files = malloc(fcap * sizeof(char *)); +/* Find or create a directory entry by path. Returns its index, or -1 if full. */ +static int dir_find_or_create(char **dir_paths, int *dir_child_counts, char ***dir_children, + int *dir_children_caps, int *dn, int dcap, const char *dir) { + for (int i = 0; i < *dn; i++) { + if (strcmp(dir_paths[i], dir) == 0) { + return i; + } + } + if (*dn < dcap) { + int idx = *dn; + dir_paths[idx] = heap_strdup(dir); + dir_child_counts[idx] = 0; + dir_children[idx] = NULL; + dir_children_caps[idx] = 0; + (*dn)++; + return idx; + } + return -1; +} - /* Directory tree: parallel arrays of dir → children set */ - int dcap = 64; - int dn = 0; - char **dir_paths = calloc(dcap, sizeof(char *)); - int *dir_child_counts = calloc(dcap, sizeof(int)); - /* Track unique children per dir using a simple string array */ - char ***dir_children = calloc(dcap, sizeof(char **)); - int *dir_children_caps = calloc(dcap, sizeof(int)); +/* Create a file tree entry by checking if a path is a file and counting dir children. */ +static cbm_file_tree_entry_t make_tree_entry(const char *path, char **files, int fn, + char **dir_paths, const int *dir_child_counts, + int dn) { + cbm_file_tree_entry_t e = {0}; + e.path = heap_strdup(path); + bool is_file = false; + for (int f = 0; f < fn; f++) { + if (strcmp(files[f], path) == 0) { + is_file = true; + break; + } + } + e.type = heap_strdup(is_file ? "file" : "dir"); + for (int d = 0; d < dn; d++) { + if (strcmp(dir_paths[d], path) == 0) { + e.children = dir_child_counts[d]; + break; + } + } + return e; +} - while (sqlite3_step(stmt) == SQLITE_ROW) { - const char *fp = (const char *)sqlite3_column_text(stmt, 0); - if (!fp) { +/* Split a path by '/' into parts. Returns number of parts. */ +static int split_path_parts(const char *fp, char *buf, int buf_sz, char **parts, int max_parts) { + strncpy(buf, fp, buf_sz - 1); + buf[buf_sz - 1] = '\0'; + int nparts = 0; + char *p = buf; + parts[nparts++] = p; + while (*p && nparts < max_parts) { + if (*p == '/') { + *p = '\0'; + parts[nparts++] = p + 1; + } + p++; + } + return nparts; +} + +/* Register dir hierarchy for one file path. */ +static void arch_register_file_dirs(const char *fp, char **dir_paths, int *dir_child_counts, + char ***dir_children, int *dir_children_caps, int *dn, + int dcap) { + char tmp[512]; + char *parts[16]; + int nparts = split_path_parts(fp, tmp, (int)sizeof(tmp), parts, 16); + + int ri = dir_find_or_create(dir_paths, dir_child_counts, dir_children, dir_children_caps, dn, + dcap, ""); + if (ri >= 0 && nparts > 0) { + dir_add_child(&dir_children[ri], &dir_child_counts[ri], &dir_children_caps[ri], parts[0]); + } + + for (int depth = 0; depth < nparts - 1 && depth < 3; depth++) { + char dir[512] = ""; + for (int k = 0; k <= depth; k++) { + if (k > 0) { + strcat(dir, "/"); + } + strcat(dir, parts[k]); + } + const char *child = (depth + 1 < nparts) ? parts[depth + 1] : NULL; + if (!child) { continue; } - if (fn >= fcap) { - fcap *= 2; - files = safe_realloc(files, fcap * sizeof(char *)); - } - files[fn++] = heap_strdup(fp); - - /* Register path components in dir tree (up to 3 levels deep) */ - char tmp[512]; - strncpy(tmp, fp, sizeof(tmp) - 1); - tmp[sizeof(tmp) - 1] = '\0'; - - /* Split by '/' */ - char *parts[16]; - int nparts = 0; - char *p = tmp; - parts[nparts++] = p; - while (*p && nparts < 16) { - if (*p == '/') { - *p = '\0'; - parts[nparts++] = p + 1; - } - p++; - } - - /* Register root children */ - { - int ri = -1; - for (int i = 0; i < dn; i++) { - if (strcmp(dir_paths[i], "") == 0) { - ri = i; - break; - } - } - if (ri < 0 && dn < dcap) { - ri = dn; - dir_paths[dn] = heap_strdup(""); - dir_child_counts[dn] = 0; - dir_children[dn] = NULL; - dir_children_caps[dn] = 0; - dn++; - } - if (ri >= 0 && nparts > 0) { - /* Check if child already exists */ - bool exists = false; - for (int k = 0; k < dir_child_counts[ri]; k++) { - if (strcmp(dir_children[ri][k], parts[0]) == 0) { - exists = true; - break; - } - } - if (!exists) { - if (dir_child_counts[ri] >= dir_children_caps[ri]) { - dir_children_caps[ri] = - dir_children_caps[ri] ? dir_children_caps[ri] * 2 : 4; - dir_children[ri] = - realloc(dir_children[ri], dir_children_caps[ri] * sizeof(char *)); - } - dir_children[ri][dir_child_counts[ri]++] = heap_strdup(parts[0]); - } - } - } - - /* Register deeper dir children (depth 0..2) */ - for (int depth = 0; depth < nparts - 1 && depth < 3; depth++) { - /* Build dir path */ - char dir[512] = ""; - for (int k = 0; k <= depth; k++) { - if (k > 0) { - strcat(dir, "/"); - } - strcat(dir, parts[k]); - } - const char *child = (depth + 1 < nparts) ? parts[depth + 1] : NULL; - if (!child) { - continue; - } - - int di = -1; - for (int i = 0; i < dn; i++) { - if (strcmp(dir_paths[i], dir) == 0) { - di = i; - break; - } - } - if (di < 0 && dn < dcap) { - di = dn; - dir_paths[dn] = heap_strdup(dir); - dir_child_counts[dn] = 0; - dir_children[dn] = NULL; - dir_children_caps[dn] = 0; - dn++; - } - if (di >= 0) { - bool exists = false; - for (int k = 0; k < dir_child_counts[di]; k++) { - if (strcmp(dir_children[di][k], child) == 0) { - exists = true; - break; - } - } - if (!exists) { - if (dir_child_counts[di] >= dir_children_caps[di]) { - dir_children_caps[di] = - dir_children_caps[di] ? dir_children_caps[di] * 2 : 4; - dir_children[di] = - realloc(dir_children[di], dir_children_caps[di] * sizeof(char *)); - } - dir_children[di][dir_child_counts[di]++] = heap_strdup(child); - } - } + int di = dir_find_or_create(dir_paths, dir_child_counts, dir_children, dir_children_caps, + dn, dcap, dir); + if (di >= 0) { + dir_add_child(&dir_children[di], &dir_child_counts[di], &dir_children_caps[di], child); } } - sqlite3_finalize(stmt); +} - /* Build file set for type detection */ - /* Collect tree entries */ +/* Count the number of '/' in a string. */ +static int count_slashes(const char *s) { + int n = 0; + for (; *s; s++) { + if (*s == '/') { + n++; + } + } + return n; +} + +/* Push a tree entry, growing the array if needed. */ +static void push_tree_entry(cbm_file_tree_entry_t **entries, int *en, int *ecap, + cbm_file_tree_entry_t e) { + if (*en >= *ecap) { + *ecap *= 2; + *entries = safe_realloc(*entries, *ecap * sizeof(cbm_file_tree_entry_t)); + } + (*entries)[(*en)++] = e; +} + +/* Collect tree entries from dir arrays. */ +static void arch_collect_entries(char **dir_paths, int *dir_child_counts, char ***dir_children, + int dn, char **files, int fn, cbm_file_tree_entry_t **entries_out, + int *en_out) { int ecap = 64; int en = 0; cbm_file_tree_entry_t *entries = calloc(ecap, sizeof(cbm_file_tree_entry_t)); @@ -3548,83 +3427,22 @@ static int arch_file_tree(cbm_store_t *s, const char *project, cbm_architecture_ continue; } for (int k = 0; k < dir_child_counts[i]; k++) { - if (en >= ecap) { - ecap *= 2; - entries = safe_realloc(entries, ecap * sizeof(cbm_file_tree_entry_t)); - } - const char *child = dir_children[i][k]; - /* Check if it's a file */ - bool is_file = false; - for (int f = 0; f < fn; f++) { - if (strcmp(files[f], child) == 0) { - is_file = true; - break; - } - } - /* Count its children in dir tree */ - int nch = 0; - for (int d = 0; d < dn; d++) { - if (strcmp(dir_paths[d], child) == 0) { - nch = dir_child_counts[d]; - break; - } - } - entries[en].path = heap_strdup(child); - if (is_file) { - entries[en].type = heap_strdup("file"); - } else { - entries[en].type = heap_strdup("dir"); - } - entries[en].children = nch; - en++; + push_tree_entry( + &entries, &en, &ecap, + make_tree_entry(dir_children[i][k], files, fn, dir_paths, dir_child_counts, dn)); } } - /* Non-root dir children */ + /* Non-root dir children (depth < 3) */ for (int i = 0; i < dn; i++) { - if (strcmp(dir_paths[i], "") == 0) { + if (strcmp(dir_paths[i], "") == 0 || count_slashes(dir_paths[i]) >= 3) { continue; } - /* Limit depth to 3 levels */ - int slashes = 0; - for (const char *p = dir_paths[i]; *p; p++) { - if (*p == '/') { - slashes++; - } - } - if (slashes >= 3) { - continue; - } - for (int k = 0; k < dir_child_counts[i]; k++) { - if (en >= ecap) { - ecap *= 2; - entries = safe_realloc(entries, ecap * sizeof(cbm_file_tree_entry_t)); - } char path[512]; snprintf(path, sizeof(path), "%s/%s", dir_paths[i], dir_children[i][k]); - bool is_file = false; - for (int f = 0; f < fn; f++) { - if (strcmp(files[f], path) == 0) { - is_file = true; - break; - } - } - int nch = 0; - for (int d = 0; d < dn; d++) { - if (strcmp(dir_paths[d], path) == 0) { - nch = dir_child_counts[d]; - break; - } - } - entries[en].path = heap_strdup(path); - if (is_file) { - entries[en].type = heap_strdup("file"); - } else { - entries[en].type = heap_strdup("dir"); - } - entries[en].children = nch; - en++; + push_tree_entry(&entries, &en, &ecap, + make_tree_entry(path, files, fn, dir_paths, dir_child_counts, dn)); } } @@ -3639,7 +3457,13 @@ static int arch_file_tree(cbm_store_t *s, const char *project, cbm_architecture_ } } - /* Cleanup dir tree */ + *entries_out = entries; + *en_out = en; +} + +/* Free dir arrays. */ +static void arch_free_dirs(char **dir_paths, int *dir_child_counts, char ***dir_children, + int *dir_children_caps, int dn, char **files, int fn) { for (int i = 0; i < dn; i++) { free(dir_paths[i]); for (int k = 0; k < dir_child_counts[i]; k++) { @@ -3655,54 +3479,84 @@ static int arch_file_tree(cbm_store_t *s, const char *project, cbm_architecture_ free(files[i]); } free(files); +} - out->file_tree = entries; - out->file_tree_count = en; +static int arch_file_tree(cbm_store_t *s, const char *project, cbm_architecture_info_t *out) { + const char *sql = "SELECT file_path FROM nodes WHERE project=?1 AND label='File'"; + sqlite3_stmt *stmt = NULL; + if (sqlite3_prepare_v2(s->db, sql, -1, &stmt, NULL) != SQLITE_OK) { + store_set_error_sqlite(s, "arch_file_tree"); + return CBM_STORE_ERR; + } + bind_text(stmt, 1, project); + + int fcap = 32; + int fn = 0; + char **files = malloc(fcap * sizeof(char *)); + + int dcap = 64; + int dn = 0; + char **dir_paths = calloc(dcap, sizeof(char *)); + int *dir_child_counts = calloc(dcap, sizeof(int)); + char ***dir_children = calloc(dcap, sizeof(char **)); + int *dir_children_caps = calloc(dcap, sizeof(int)); + + while (sqlite3_step(stmt) == SQLITE_ROW) { + const char *fp = (const char *)sqlite3_column_text(stmt, 0); + if (!fp) { + continue; + } + if (fn >= fcap) { + fcap *= 2; + files = safe_realloc(files, fcap * sizeof(char *)); + } + files[fn++] = heap_strdup(fp); + arch_register_file_dirs(fp, dir_paths, dir_child_counts, dir_children, dir_children_caps, + &dn, dcap); + } + sqlite3_finalize(stmt); + + arch_collect_entries(dir_paths, dir_child_counts, dir_children, dn, files, fn, &out->file_tree, + &out->file_tree_count); + + arch_free_dirs(dir_paths, dir_child_counts, dir_children, dir_children_caps, dn, files, fn); return CBM_STORE_OK; } /* ── Louvain community detection ───────────────────────────────── */ -// NOLINTNEXTLINE(readability-function-cognitive-complexity,readability-function-size) -int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *edges, - int edge_count, cbm_louvain_result_t **out, int *out_count) { - if (node_count <= 0) { - *out = NULL; - *out_count = 0; - return CBM_STORE_OK; +/* Build deduplicated, normalized edge weight arrays from raw edges. + * Returns total number of unique edges in *out_wn. */ +/* Find the index of a node ID in the nodes array, or -1. */ +static int louvain_node_index(const int64_t *nodes, int n, int64_t id) { + for (int i = 0; i < n; i++) { + if (nodes[i] == id) { + return i; + } } + return -1; +} - int n = node_count; - - /* Build adjacency: edge weights */ +static void louvain_build_weights(const int64_t *nodes, int n, const cbm_louvain_edge_t *edges, + int edge_count, int **out_wsi, int **out_wdi, double **out_ww, + int *out_wn) { int wcap = edge_count > 0 ? edge_count : 1; int wn = 0; int *wsi = malloc(wcap * sizeof(int)); int *wdi = malloc(wcap * sizeof(int)); double *ww = malloc(wcap * sizeof(double)); - /* Map node IDs to indices */ for (int e = 0; e < edge_count; e++) { - int si = -1; - int di = -1; - for (int i = 0; i < n; i++) { - if (nodes[i] == edges[e].src) { - si = i; - } - if (nodes[i] == edges[e].dst) { - di = i; - } - } + int si = louvain_node_index(nodes, n, edges[e].src); + int di = louvain_node_index(nodes, n, edges[e].dst); if (si < 0 || di < 0 || si == di) { continue; } - /* Normalize edge key */ if (si > di) { int tmp = si; si = di; di = tmp; } - /* Check if already exists */ int found = -1; for (int i = 0; i < wn; i++) { if (wsi[i] == si && wdi[i] == di) { @@ -3725,39 +3579,165 @@ int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t * wn++; } } + *out_wsi = wsi; + *out_wdi = wdi; + *out_ww = ww; + *out_wn = wn; +} + +/* Add a bidirectional edge to adjacency lists. */ +static void adj_add_edge(int **adj, double **adj_w, int *adj_n, int *adj_cap, int si, int di, + double w) { + if (adj_n[si] >= adj_cap[si]) { + int new_cap = adj_cap[si] ? adj_cap[si] * 2 : 4; + int *new_adj = safe_realloc(adj[si], new_cap * sizeof(int)); + double *new_w = safe_realloc(adj_w[si], new_cap * sizeof(double)); + /* Zero-init new slots to satisfy static analyzer */ + for (int k = adj_cap[si]; k < new_cap; k++) { + new_adj[k] = 0; + new_w[k] = 0.0; + } + adj[si] = new_adj; + adj_w[si] = new_w; + adj_cap[si] = new_cap; + } + adj[si][adj_n[si]] = di; + adj_w[si][adj_n[si]] = w; + adj_n[si]++; + + if (adj_n[di] >= adj_cap[di]) { + adj_cap[di] = adj_cap[di] ? adj_cap[di] * 2 : 4; + adj[di] = safe_realloc(adj[di], adj_cap[di] * sizeof(int)); + adj_w[di] = safe_realloc(adj_w[di], adj_cap[di] * sizeof(double)); + } + adj[di][adj_n[di]] = si; + adj_w[di][adj_n[di]] = w; + adj_n[di]++; +} + +/* Run one Louvain iteration. Returns true if any node moved. */ +/* Linear congruential generator (glibc constants) */ +#define LCG_MULTIPLIER 1103515245U +#define LCG_INCREMENT 12345U + +static bool louvain_iteration(int n, int **adj, double **adj_w, const int *adj_n, int *community, + const double *degree, double total_weight, int iter) { + bool improved = false; + + double *comm_degree = calloc(n, sizeof(double)); + for (int i = 0; i < n; i++) { + comm_degree[community[i]] += degree[i]; + } + + int *order = calloc(n, sizeof(int)); + for (int i = 0; i < n; i++) { + order[i] = i; + } + unsigned int seed = (unsigned int)((iter * 1000) + n); + for (int i = n - 1; i > 0; i--) { + seed = (seed * LCG_MULTIPLIER) + LCG_INCREMENT; + int j = (int)((seed >> 16) % (unsigned int)(i + 1)); + int tmp = order[i]; + order[i] = order[j]; + order[j] = tmp; + } + + for (int oi = 0; oi < n; oi++) { + int i = order[oi]; + int cur_comm = community[i]; + + double *nc_weight = calloc(n, sizeof(double)); + bool *nc_seen = calloc(n, sizeof(bool)); + for (int j = 0; j < adj_n[i]; j++) { + int nc = community[adj[i][j]]; + nc_weight[nc] += adj_w[i][j]; + nc_seen[nc] = true; + } + + comm_degree[cur_comm] -= degree[i]; + + int best_comm = cur_comm; + double best_gain = 0.0; + for (int c = 0; c < n; c++) { + if (!nc_seen[c]) { + continue; + } + double gain = nc_weight[c] - (degree[i] * comm_degree[c] / (2.0 * total_weight)); + if (gain > best_gain) { + best_gain = gain; + best_comm = c; + } + } + + double cur_gain = + nc_weight[cur_comm] - (degree[i] * comm_degree[cur_comm] / (2.0 * total_weight)); + if (cur_gain >= best_gain) { + best_comm = cur_comm; + } + + community[i] = best_comm; + comm_degree[best_comm] += degree[i]; + + if (best_comm != cur_comm) { + improved = true; + } + + free(nc_weight); + free(nc_seen); + } + free(order); + free(comm_degree); + return improved; +} + +static void louvain_free_adj(int **adj, double **adj_w, int *adj_n, int *adj_cap, int n) { + for (int i = 0; i < n; i++) { + free(adj[i]); + free(adj_w[i]); + } + free(adj); + free(adj_w); + free(adj_n); + free(adj_cap); +} + +int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *edges, + int edge_count, cbm_louvain_result_t **out, int *out_count) { + if (node_count <= 0) { + *out = NULL; + *out_count = 0; + return CBM_STORE_OK; + } + + int n = node_count; + + /* Build deduplicated edge weights */ + int *wsi; + int *wdi; + double *ww; + int wn; + louvain_build_weights(nodes, n, edges, edge_count, &wsi, &wdi, &ww, &wn); /* Build adjacency lists */ int **adj = calloc(n, sizeof(int *)); double **adj_w = calloc(n, sizeof(double *)); int *adj_n = calloc(n, sizeof(int)); int *adj_cap = calloc(n, sizeof(int)); + if (!adj || !adj_w || !adj_n || !adj_cap) { + free(adj); + free(adj_w); + free(adj_n); + free(adj_cap); + free(wsi); + free(wdi); + free(ww); + return -1; + } double total_weight = 0; for (int i = 0; i < wn; i++) { - int si = wsi[i]; - int di = wdi[i]; - double w = ww[i]; - total_weight += w; - - /* Add si → di */ - if (adj_n[si] >= adj_cap[si]) { - adj_cap[si] = adj_cap[si] ? adj_cap[si] * 2 : 4; - adj[si] = safe_realloc(adj[si], adj_cap[si] * sizeof(int)); - adj_w[si] = safe_realloc(adj_w[si], adj_cap[si] * sizeof(double)); - } - adj[si][adj_n[si]] = di; - adj_w[si][adj_n[si]] = w; - adj_n[si]++; - - /* Add di → si */ - if (adj_n[di] >= adj_cap[di]) { - adj_cap[di] = adj_cap[di] ? adj_cap[di] * 2 : 4; - adj[di] = safe_realloc(adj[di], adj_cap[di] * sizeof(int)); - adj_w[di] = safe_realloc(adj_w[di], adj_cap[di] * sizeof(double)); - } - adj[di][adj_n[di]] = si; - adj_w[di][adj_n[di]] = w; - adj_n[di]++; + total_weight += ww[i]; + adj_add_edge(adj, adj_w, adj_n, adj_cap, wsi[i], wdi[i], ww[i]); } free(wsi); free(wdi); @@ -3770,7 +3750,6 @@ int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t * } if (total_weight == 0) { - /* No edges: each node in its own community */ cbm_louvain_result_t *result = malloc(n * sizeof(cbm_louvain_result_t)); for (int i = 0; i < n; i++) { result[i].node_id = nodes[i]; @@ -3778,6 +3757,14 @@ int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t * } *out = result; *out_count = n; + free(community); + louvain_free_adj(adj, adj_w, adj_n, adj_cap, n); + return CBM_STORE_OK; + } + + /* Compute node degrees */ + double *degree = calloc(n, sizeof(double)); + if (!degree) { free(community); for (int i = 0; i < n; i++) { free(adj[i]); @@ -3786,96 +3773,19 @@ int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t * free(adj); free(adj_w); free(adj_n); - free(adj_cap); - return CBM_STORE_OK; + return -1; } - - /* Compute node degrees */ - double *degree = calloc(n, sizeof(double)); for (int i = 0; i < n; i++) { - for (int j = 0; j < adj_n[i]; j++) { - degree[i] += adj_w[i][j]; + if (adj_w[i]) { + for (int j = 0; j < adj_n[i]; j++) { + degree[i] += adj_w[i][j]; + } } } /* Main Louvain loop (10 iterations max) */ for (int iter = 0; iter < 10; iter++) { - bool improved = false; - - /* Community total degree */ - double *comm_degree = calloc(n, sizeof(double)); - for (int i = 0; i < n; i++) { - comm_degree[community[i]] += degree[i]; - } - - /* Random order (simple LCG shuffle) */ - int *order = calloc(n, sizeof(int)); - for (int i = 0; i < n; i++) { - order[i] = i; - } - unsigned int seed = (unsigned int)((iter * 1000) + n); - for (int i = n - 1; i > 0; i--) { -/* Linear congruential generator (glibc constants) */ -#define LCG_MULTIPLIER 1103515245U -#define LCG_INCREMENT 12345U - seed = (seed * LCG_MULTIPLIER) + LCG_INCREMENT; - int j = (int)((seed >> 16) % (unsigned int)(i + 1)); - int tmp = order[i]; - order[i] = order[j]; - order[j] = tmp; - } - - for (int oi = 0; oi < n; oi++) { - int i = order[oi]; - int cur_comm = community[i]; - - /* Compute weights to neighboring communities */ - double *nc_weight = calloc(n, sizeof(double)); - bool *nc_seen = calloc(n, sizeof(bool)); - for (int j = 0; j < adj_n[i]; j++) { - int nc = community[adj[i][j]]; - nc_weight[nc] += adj_w[i][j]; - nc_seen[nc] = true; - } - - /* Remove node from community */ - comm_degree[cur_comm] -= degree[i]; - - int best_comm = cur_comm; - double best_gain = 0.0; - - for (int c = 0; c < n; c++) { - if (!nc_seen[c]) { - continue; - } - double gain = nc_weight[c] - (degree[i] * comm_degree[c] / (2.0 * total_weight)); - if (gain > best_gain) { - best_gain = gain; - best_comm = c; - } - } - - /* Also consider staying */ - double cur_gain = - nc_weight[cur_comm] - (degree[i] * comm_degree[cur_comm] / (2.0 * total_weight)); - if (cur_gain >= best_gain) { - best_comm = cur_comm; - } - - community[i] = best_comm; - comm_degree[best_comm] += degree[i]; - - if (best_comm != cur_comm) { - improved = true; - } - - free(nc_weight); - free(nc_seen); - } - free(order); - free(comm_degree); - - if (!improved) { + if (!louvain_iteration(n, adj, adj_w, adj_n, community, degree, total_weight, iter)) { break; } } @@ -3891,14 +3801,7 @@ int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t * free(community); free(degree); - for (int i = 0; i < n; i++) { - free(adj[i]); - free(adj_w[i]); - } - free(adj); - free(adj_w); - free(adj_n); - free(adj_cap); + louvain_free_adj(adj, adj_w, adj_n, adj_cap, n); return CBM_STORE_OK; } @@ -4065,7 +3968,61 @@ static bool is_canonical_section(const char *name) { return false; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Save a completed ADR section into result, trimming whitespace. */ +static void adr_save_section(cbm_adr_sections_t *result, char *section_name, char *buf, + int buf_len) { + if (!section_name || result->count >= 16) { + return; + } + /* Trim trailing whitespace */ + while (buf_len > 0 && (buf[buf_len - 1] == '\n' || buf[buf_len - 1] == ' ')) { + buf[--buf_len] = '\0'; + } + /* Skip leading whitespace */ + char *trimmed = buf; + while (*trimmed == '\n' || *trimmed == ' ') { + trimmed++; + } + result->keys[result->count] = section_name; + result->values[result->count] = heap_strdup(trimmed); + result->count++; +} + +/* Try to extract a canonical section header from a "## Header" line. + * Returns heap-allocated header string if canonical, NULL otherwise. */ +static char *adr_try_section_header(const char *line, int line_len) { + if (line_len <= 3 || line[0] != '#' || line[1] != '#' || line[2] != ' ') { + return NULL; + } + char header[64]; + int hlen = line_len - 3; + if (hlen >= (int)sizeof(header)) { + hlen = (int)sizeof(header) - 1; + } + memcpy(header, line + 3, hlen); + header[hlen] = '\0'; + while (hlen > 0 && + (header[hlen - 1] == ' ' || header[hlen - 1] == '\t' || header[hlen - 1] == '\r')) { + header[--hlen] = '\0'; + } + if (!is_canonical_section(header)) { + return NULL; + } + return heap_strdup(header); +} + +/* Append a line to the current section content buffer. */ +static void adr_append_line(char *buf, int buf_sz, int *len, const char *line, int line_len) { + if (*len > 0) { + buf[(*len)++] = '\n'; + } + if (*len + line_len < buf_sz - 1) { + memcpy(buf + *len, line, line_len); + *len += line_len; + buf[*len] = '\0'; + } +} + cbm_adr_sections_t cbm_adr_parse_sections(const char *content) { cbm_adr_sections_t result; memset(&result, 0, sizeof(result)); @@ -4079,86 +4036,36 @@ cbm_adr_sections_t cbm_adr_parse_sections(const char *content) { int content_len = 0; while (*p) { - /* Find end of line */ const char *eol = strchr(p, '\n'); int line_len = eol ? (int)(eol - p) : (int)strlen(p); - /* Check for canonical section header */ - if (line_len > 3 && p[0] == '#' && p[1] == '#' && p[2] == ' ') { - char header[64]; - int hlen = line_len - 3; - if (hlen >= (int)sizeof(header)) { - hlen = (int)sizeof(header) - 1; - } - memcpy(header, p + 3, hlen); - header[hlen] = '\0'; - /* Trim trailing whitespace */ - while (hlen > 0 && (header[hlen - 1] == ' ' || header[hlen - 1] == '\t' || - header[hlen - 1] == '\r')) { - header[--hlen] = '\0'; - } - - if (is_canonical_section(header)) { - /* Save previous section */ - if (current_section && result.count < 16) { - /* Trim content */ - while (content_len > 0 && (current_content[content_len - 1] == '\n' || - current_content[content_len - 1] == ' ')) { - current_content[--content_len] = '\0'; - } - /* Skip leading whitespace */ - char *trimmed = current_content; - while (*trimmed == '\n' || *trimmed == ' ') { - trimmed++; - } - result.keys[result.count] = current_section; - result.values[result.count] = heap_strdup(trimmed); - result.count++; - } - current_section = heap_strdup(header); - current_content[0] = '\0'; - content_len = 0; - p = eol ? eol + 1 : p + line_len; - continue; - } - } - - /* Append line to current content */ - if (current_section) { - if (content_len > 0 || line_len > 0) { - if (content_len > 0) { - current_content[content_len++] = '\n'; - } - if (content_len + line_len < (int)sizeof(current_content) - 1) { - memcpy(current_content + content_len, p, line_len); - content_len += line_len; - current_content[content_len] = '\0'; - } - } + char *new_section = adr_try_section_header(p, line_len); + if (new_section) { + adr_save_section(&result, current_section, current_content, content_len); + current_section = new_section; + current_content[0] = '\0'; + content_len = 0; + } else if (current_section && (content_len > 0 || line_len > 0)) { + adr_append_line(current_content, (int)sizeof(current_content), &content_len, p, + line_len); } p = eol ? eol + 1 : p + line_len; } - /* Save last section */ - if (current_section && result.count < 16) { - while (content_len > 0 && (current_content[content_len - 1] == '\n' || - current_content[content_len - 1] == ' ')) { - current_content[--content_len] = '\0'; - } - char *trimmed = current_content; - while (*trimmed == '\n' || *trimmed == ' ') { - trimmed++; - } - result.keys[result.count] = current_section; - result.values[result.count] = heap_strdup(trimmed); - result.count++; - } - + adr_save_section(&result, current_section, current_content, content_len); return result; } -// NOLINTNEXTLINE(readability-function-cognitive-complexity) +/* Append a section to the render buffer. */ +static int adr_render_section(char *buf, int buf_sz, int pos, const char *key, const char *value) { + if (pos > 0) { + pos += snprintf(buf + pos, buf_sz - pos, "\n\n"); + } + pos += snprintf(buf + pos, buf_sz - pos, "## %s\n%s", key, value); + return pos; +} + char *cbm_adr_render(const cbm_adr_sections_t *sections) { if (!sections || sections->count == 0) { return heap_strdup(""); @@ -4175,11 +4082,8 @@ char *cbm_adr_render(const cbm_adr_sections_t *sections) { continue; } if (strcmp(sections->keys[i], canonical_sections[c]) == 0) { - if (pos > 0) { - pos += snprintf(buf + pos, sizeof(buf) - pos, "\n\n"); - } - pos += snprintf(buf + pos, sizeof(buf) - pos, "## %s\n%s", sections->keys[i], - sections->values[i]); + pos = adr_render_section(buf, (int)sizeof(buf), pos, sections->keys[i], + sections->values[i]); rendered[i] = true; break; } @@ -4187,7 +4091,6 @@ char *cbm_adr_render(const cbm_adr_sections_t *sections) { } /* Non-canonical sections alphabetically */ - /* Collect indices of non-rendered sections */ int extra[16]; int nextra = 0; for (int i = 0; i < sections->count; i++) { @@ -4195,7 +4098,6 @@ char *cbm_adr_render(const cbm_adr_sections_t *sections) { extra[nextra++] = i; } } - /* Sort extra by key name */ for (int i = 1; i < nextra; i++) { int j = i; while (j > 0 && strcmp(sections->keys[extra[j]], sections->keys[extra[j - 1]]) < 0) { @@ -4207,11 +4109,8 @@ char *cbm_adr_render(const cbm_adr_sections_t *sections) { } for (int i = 0; i < nextra; i++) { int idx = extra[i]; - if (pos > 0) { - pos += snprintf(buf + pos, sizeof(buf) - pos, "\n\n"); - } - pos += snprintf(buf + pos, sizeof(buf) - pos, "## %s\n%s", sections->keys[idx], - sections->values[idx]); + pos = adr_render_section(buf, (int)sizeof(buf), pos, sections->keys[idx], + sections->values[idx]); } return heap_strdup(buf);