#include "cbm.h" #include "arena.h" // CBMArena, cbm_arena_alloc/strdup/sprintf #include "helpers.h" #include "lang_specs.h" #include "foundation/constants.h" #include "foundation/platform.h" // safe_realloc (frees old on failure) #include "foundation/log.h" // cbm_log_warn #include "extract_node_stack.h" #include "simhash/minhash.h" #include "semantic/ast_profile.h" #include "tree_sitter/api.h" // TSNode, ts_node_* #include // uint32_t #include // snprintf #include // getenv, atoi #include #include // Buffer sizes for local arrays (base classes, params, return types). #define MAX_COMMENT_LEN 500 #define MAX_BASES 16 #define MAX_BASES_MINUS_1 15 #define MAX_PARAMS CBM_SZ_32 #define MAX_PARAMS_MINUS_1 31 #define MAX_RETURN_TYPES 16 #define MAX_RETURN_TYPES_MINUS_1 15 // Tree traversal limits. enum { TEMPLATE_DEPTH_LIMIT = 4, DECLARATOR_DEPTH_LIMIT = CBM_DECLARATOR_DEPTH_LIMIT, // shared define in helpers.h EXPORT_ANCESTOR_DEPTH = 4, FUNC_PARENT_CLIMB_LIMIT = 4, /* fun_expr -> term -> uni_term -> let_binding (Nickel) */ DECORATOR_SCAN_LIMIT = 3, C_RETURN_WALK_DEPTH = 5, VAR_RECURSION_LIMIT = 8, NESTED_CLASS_STACK_CAP = 128, FP_HASH_MUL = 31, /* FNV-like hash multiplier for identifier dedup */ FP_HASH_NONZERO = 1, /* OR mask to ensure hash is never zero (sentinel) */ FP_SPACE_SEP = 1, /* one byte for space separator between tokens */ }; /* Hash a span of source text. */ static uint32_t hash_source_span(const char *source, uint32_t start, int len) { uint32_t h = 0; for (int x = 0; x < len; x++) { h = (h * FP_HASH_MUL) + (uint32_t)(unsigned char)source[start + (uint32_t)x]; } return h; } /* Try to append a unique identifier to the buffer. Returns true if appended. */ static bool try_append_ident(const char *source, uint32_t s, int len, uint32_t *seen, int seen_size, uint32_t seen_mask, char *buf, int buf_size, int *pos, int *count) { uint32_t h = hash_source_span(source, s, len); uint32_t key = h | FP_HASH_NONZERO; uint32_t slot = h & seen_mask; bool dup = false; for (int p = 0; p < seen_size; p++) { uint32_t idx = (slot + (uint32_t)p) & seen_mask; if (seen[idx] == 0) { seen[idx] = key; break; } if (seen[idx] == key) { dup = true; break; } } if (dup || *pos + len + FP_SPACE_SEP >= buf_size) { return false; } if (*pos > 0) { buf[(*pos)++] = ' '; } memcpy(buf + *pos, source + s, (size_t)len); *pos += len; (*count)++; return true; } /* Walk AST body, collect unique identifier text as space-separated string. * Returns arena-allocated string or NULL. */ static char *extract_body_ident_tokens(CBMExtractCtx *ctx, TSNode body) { enum { BT_STACK = 512, BT_BUF = 512, BT_MAX_IDENTS = 40, BT_SEEN = 128, BT_SEEN_MASK = 127 }; TSNode bt_stack[BT_STACK]; int bt_top = 0; bt_stack[bt_top++] = body; char bt_buf[BT_BUF]; int bt_pos = 0; uint32_t bt_seen[BT_SEEN]; memset(bt_seen, 0, sizeof(bt_seen)); int bt_count = 0; while (bt_top > 0 && bt_count < BT_MAX_IDENTS) { TSNode nd = bt_stack[--bt_top]; uint32_t nc = ts_node_child_count(nd); if (nc == 0) { const char *k = ts_node_type(nd); if (strcmp(k, "identifier") == 0 || strcmp(k, "field_identifier") == 0 || strcmp(k, "property_identifier") == 0) { uint32_t s = ts_node_start_byte(nd); int len = (int)(ts_node_end_byte(nd) - s); if (len > 0 && len < CBM_SZ_64 && s < (uint32_t)ctx->source_len) { try_append_ident(ctx->source, s, len, bt_seen, BT_SEEN, BT_SEEN_MASK, bt_buf, BT_BUF, &bt_pos, &bt_count); } } } else { for (int i = (int)nc - SKIP_ONE; i >= 0 && bt_top < BT_STACK; i--) { bt_stack[bt_top++] = ts_node_child(nd, (uint32_t)i); } } } if (bt_pos > 0) { bt_buf[bt_pos] = '\0'; return cbm_arena_strdup(ctx->arena, bt_buf); } return NULL; } /* Compute MinHash fingerprint for a function body node and store in def. * Sets def->fingerprint (arena-allocated) and def->fingerprint_k on success, * leaves them NULL/0 if the body is too short. */ static void compute_fingerprint(CBMExtractCtx *ctx, CBMDefinition *def, TSNode func_node) { /* Find the function body child */ TSNode body = ts_node_child_by_field_name(func_node, TS_FIELD("body")); if (ts_node_is_null(body)) { body = func_node; } cbm_minhash_t result; if (!cbm_minhash_compute(body, ctx->source, (int)ctx->language, &result)) { return; /* Too short or empty — no fingerprint */ } /* Arena-allocate the fingerprint array */ uint32_t *fp = cbm_arena_alloc(ctx->arena, CBM_MINHASH_K * sizeof(uint32_t)); if (!fp) { return; } memcpy(fp, result.values, CBM_MINHASH_K * sizeof(uint32_t)); def->fingerprint = fp; def->fingerprint_k = CBM_MINHASH_K; /* AST structural profile (signals 8, 9, 11) — rides the same body node */ cbm_ast_profile_t profile; int pc = 0; if (def->param_names) { while (def->param_names[pc]) { pc++; } } if (cbm_ast_profile_compute(body, ctx->source, def->param_names, pc, &profile)) { profile.body_lines = (uint16_t)def->lines; char sp_buf[CBM_AST_PROFILE_BUF]; cbm_ast_profile_to_str(&profile, sp_buf, sizeof(sp_buf)); def->structural_profile = cbm_arena_strdup(ctx->arena, sp_buf); } /* Extract raw identifier tokens from body for semantic search */ def->body_tokens = extract_body_ident_tokens(ctx, body); } // Tree-sitter row is 0-based; lines are 1-based. // Null-terminated array allocation: need count + 1 for terminator. enum { NULL_TERM = 1 }; // String operations. enum { SKIP_CHAR = 1, // skip one character (dot, quote, prefix) PAIR_CHARS = 2, // pair of delimiters (quotes, parens) SECOND_CHILD_IDX = 1, // index of second child FIRST_LINE = 1, // first line number }; // Return type pair array size. enum { RT_PAIR_SIZE = 2 }; // Forward declarations static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec); static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec); static void walk_defs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec, int depth_unused); static void extract_variables(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec); static void extract_class_variables(CBMExtractCtx *ctx, TSNode class_node, const CBMLangSpec *spec); static void extract_rust_impl(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec); static void extract_class_methods(CBMExtractCtx *ctx, TSNode class_node, const char *class_qn, const CBMLangSpec *spec); static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const char *class_qn, const CBMLangSpec *spec); static TSNode find_class_body(TSNode class_node, CBMLanguage lang); static void extract_enum_members(CBMExtractCtx *ctx, TSNode node, const char *class_qn); static void extract_elixir_call(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec); // --- Helpers --- // Get "name" field from a node static TSNode func_name_node(TSNode node) { TSNode name = ts_node_child_by_field_name(node, TS_FIELD("name")); if (ts_node_is_null(name)) { /* Protobuf rpc: name is in rpc_name child, not "name" field */ name = cbm_find_child_by_kind(node, "rpc_name"); } return name; } // 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, TS_FIELD("variables")); 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 < TEMPLATE_DEPTH_LIMIT; 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, TS_FIELD("pattern")); 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); } /* Julia short-form `name(args) = body` parses as an `assignment` whose LHS is * a call_expression (`name(args)`); the function name is that call's head * identifier. A plain `x = 5` (non-call LHS) is not a function — resolve NULL * so it is neither extracted as a def nor scoped. */ if (lang == CBM_LANG_JULIA && strcmp(kind, "assignment") == 0) { if (ts_node_named_child_count(node) > 0) { TSNode lhs = ts_node_named_child(node, 0); if (!ts_node_is_null(lhs) && strcmp(ts_node_type(lhs), "call_expression") == 0) { return resolve_julia_func_name(lhs); } } } 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, TS_FIELD("declId")); 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, TS_FIELD("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, TS_FIELD("functionDeclarationLeft")); 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→symbol. // The defined symbol's head can be a user_symbol (lowercase user names) OR a // builtin_symbol (capitalized names like Square/Cube, which the grammar tags as // builtin even when user-defined). For a bare `Name = value` (set_top with no // apply), the LHS is the symbol itself. Accept all three forms so multiple // defs in one file each resolve to a distinct name instead of collapsing. static TSNode resolve_wolfram_func_name(TSNode node) { if (ts_node_named_child_count(node) > 0) { TSNode lhs = ts_node_named_child(node, 0); const char *lk = ts_node_type(lhs); if (strcmp(lk, "apply") == 0 && ts_node_named_child_count(lhs) > 0) { TSNode head = ts_node_named_child(lhs, 0); const char *hk = ts_node_type(head); if (strcmp(hk, "user_symbol") == 0 || strcmp(hk, "builtin_symbol") == 0) { return head; } } else if (strcmp(lk, "user_symbol") == 0 || strcmp(lk, "builtin_symbol") == 0) { return lhs; } } 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; } // C++/CUDA: out-of-line method definitions name the function with a qualified // declarator (`Foo::bar`, or `ns::Foo::bar`). Return the immediate enclosing // class name (the scope segment directly left of the function name, e.g. "Foo"), // or NULL when the declarator is unqualified (a plain free function). Without // this, an out-of-line definition — whose class body lives declaration-only in a // header — would be recorded as a free Function with no link to its class. char *cbm_cpp_out_of_line_parent_class(CBMArena *a, TSNode node, const char *source) { // Descend the declarator chain to its qualified_identifier, if any. TSNode qid = {0}; TSNode decl = ts_node_child_by_field_name(node, TS_FIELD("declarator")); for (int depth = 0; depth < DECLARATOR_DEPTH_LIMIT && !ts_node_is_null(decl); depth++) { const char *dk = ts_node_type(decl); if (strcmp(dk, "qualified_identifier") == 0 || strcmp(dk, "scoped_identifier") == 0) { qid = decl; break; } TSNode inner = ts_node_child_by_field_name(decl, TS_FIELD("declarator")); 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; } if (ts_node_is_null(qid)) { return NULL; } // The qualified_identifier's `scope` is the parent. For a nested scope // (`ns::Foo`) descend through its `name` field to the innermost segment so // the direct parent ("Foo") is returned, not the outer namespace. TSNode scope = ts_node_child_by_field_name(qid, TS_FIELD("scope")); if (ts_node_is_null(scope)) { return NULL; } for (int depth = 0; depth < DECLARATOR_DEPTH_LIMIT; depth++) { const char *sk = ts_node_type(scope); if (strcmp(sk, "qualified_identifier") != 0 && strcmp(sk, "scoped_identifier") != 0) { break; } TSNode name = ts_node_child_by_field_name(scope, TS_FIELD("name")); if (ts_node_is_null(name)) { break; } scope = name; } char *text = cbm_node_text(a, scope, source); return (text && text[0]) ? text : NULL; } // 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, TS_FIELD("left")); if (ts_node_is_null(lhs)) { lhs = ts_node_child_by_field_name(parent, TS_FIELD("lhs")); } 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; } // Max descent for the (System)Verilog name-wrapper search (module/class headers // nest the identifier a few levels deep; bounding avoids walking large subtrees // like port lists). enum { CBM_DESCENDANT_MAX_DEPTH = 6 }; // Verilog/SystemVerilog: find the first descendant node of the given kind in // pre-order (depth-bounded). The (System)Verilog grammar has FIELD_COUNT 0, so // def names live on nested *_identifier wrappers reachable only by node kind. // Tree-sitter trees are acyclic, so the bounded recursion always terminates. static TSNode find_first_descendant_by_kind(TSNode node, const char *kind, // NOLINT(misc-no-recursion) int max_depth) { if (max_depth < 0 || ts_node_is_null(node)) { TSNode null_node = {0}; return null_node; } 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), kind) == 0) { return child; } TSNode found = find_first_descendant_by_kind(child, kind, max_depth - 1); if (!ts_node_is_null(found)) { return found; } } TSNode null_node = {0}; return null_node; } // Forward declaration for mutual recursion. Exported (see helpers.h) so the // unified/calls extractor shares this one resolver — see cbm_resolve_func_name. TSNode cbm_resolve_func_name(TSNode node, CBMLanguage lang); static bool is_cpp_template_inner_kind(const char *kind) { return strcmp(kind, "function_definition") == 0 || strcmp(kind, "declaration") == 0 || strcmp(kind, "field_declaration") == 0; } // C++/CUDA: find inner function/declaration inside template_declaration. // Returns the inner node (not the resolved name) to break the recursive cycle. static TSNode find_cpp_template_inner_node(TSNode node, CBMLanguage lang) { if ((lang != CBM_LANG_CPP && lang != CBM_LANG_CUDA) || strcmp(ts_node_type(node), "template_declaration") != 0) { return node; } 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 (is_cpp_template_inner_kind(ck)) { return ch; } if (strcmp(ck, "template_declaration") == 0) { TSNode nested = find_cpp_template_inner_node(ch, lang); if (!ts_node_is_null(nested) && !ts_node_eq(nested, ch)) { return nested; } } } TSNode null_node = {0}; return null_node; } // Try arrow_function name via parent variable_declarator (top-level resolution) // or object-literal property (`pair` key) — the latter covers factory functions // that return an object of arrow methods (the Zustand actions-slice pattern, #341). static TSNode resolve_toplevel_arrow_name(TSNode node, const char *kind) { if (strcmp(kind, "arrow_function") != 0) { TSNode null_node = {0}; return null_node; } TSNode parent = ts_node_parent(node); if (ts_node_is_null(parent)) { TSNode null_node = {0}; return null_node; } const char *pk = ts_node_type(parent); if (strcmp(pk, "variable_declarator") == 0 || strcmp(pk, "public_field_definition") == 0) { /* `const f = () => {}` and the class-field form `f = () => {}` both name * the arrow via the parent's `name` child (#new_ts_class_field_arrow): * resolving it lets push_boundary_scopes push a SCOPE_FUNC so in-body * calls source to the method, not the enclosing class/module. */ return ts_node_child_by_field_name(parent, TS_FIELD("name")); } if (strcmp(pk, "field_definition") == 0) { return ts_node_child_by_field_name(parent, TS_FIELD("property")); } if (strcmp(pk, "pair") == 0) { return ts_node_child_by_field_name(parent, TS_FIELD("key")); } TSNode null_node = {0}; return null_node; } // Try C/C++/CUDA/GLSL function_definition declarator name or template unwrap. static TSNode resolve_func_name_c_family(TSNode *node_ptr, CBMLanguage lang, const char *kind) { if ((lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA) && strcmp(kind, "template_declaration") == 0) { TSNode inner = find_cpp_template_inner_node(*node_ptr, lang); if (!ts_node_is_null(inner)) { *node_ptr = inner; /* signal caller to retry */ } TSNode null_node = {0}; return null_node; } if ((lang == CBM_LANG_C || lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA || lang == CBM_LANG_GLSL || lang == CBM_LANG_HLSL || lang == CBM_LANG_ISPC || lang == CBM_LANG_SLANG || lang == CBM_LANG_OBJC) && strcmp(kind, "function_definition") == 0) { /* Objective-C top-level C functions (`static int helper(int x) {...}`) * have the same declarator structure as C — without this they get no * name node and are dropped, so a call to them never resolves an edge. */ return cbm_resolve_c_declarator_name_node(*node_ptr); } TSNode null_node = {0}; return null_node; } // Resolve the name node for a function, handling language-specific quirks. // Uses a loop to handle template_declaration unwrapping (avoids recursion). TSNode cbm_resolve_func_name(TSNode node, CBMLanguage lang) { enum { MAX_TEMPLATE_DEPTH = 2 }; for (int tmpl_depth = 0; tmpl_depth < MAX_TEMPLATE_DEPTH; tmpl_depth++) { const char *kind = ts_node_type(node); if (lang == CBM_LANG_HASKELL && strcmp(kind, "signature") == 0) { TSNode null_node = {0}; return null_node; } TSNode name = func_name_node(node); if (lang == CBM_LANG_R && strcmp(kind, "function_definition") == 0) { return resolve_r_func_name(node); } if (!ts_node_is_null(name)) { return name; } /* Swift and newer tree-sitter-kotlin: function_declaration has no `name` * field; the function name is a `simple_identifier` child. */ if ((lang == CBM_LANG_SWIFT || lang == CBM_LANG_KOTLIN) && strcmp(kind, "function_declaration") == 0) { TSNode si = cbm_find_child_by_kind(node, "simple_identifier"); if (!ts_node_is_null(si)) { return si; } } // PowerShell function_statement has no `name` field; the name is a // `function_name` child node (#35). if (lang == CBM_LANG_POWERSHELL && strcmp(kind, "function_statement") == 0) { TSNode fn = cbm_find_child_by_kind(node, "function_name"); if (!ts_node_is_null(fn)) { return fn; } } /* Cairo / D / Odin / Squirrel: the def node has no `name` field; the name * is a plain `identifier` child (same shape as the Swift/Kotlin case). */ if ((lang == CBM_LANG_CAIRO || lang == CBM_LANG_DLANG || lang == CBM_LANG_ODIN || lang == CBM_LANG_SQUIRREL) && (strcmp(kind, "function_definition") == 0 || strcmp(kind, "function_signature") == 0 || strcmp(kind, "function_declaration") == 0 || strcmp(kind, "procedure_declaration") == 0)) { TSNode id = cbm_find_child_by_kind(node, "identifier"); if (!ts_node_is_null(id)) { return id; } } /* Ada: subprogram_body/_declaration carry the `name` field on a nested * procedure_specification/function_specification child, not on themselves. */ if (lang == CBM_LANG_ADA && (strcmp(kind, "subprogram_body") == 0 || strcmp(kind, "subprogram_declaration") == 0)) { TSNode spec = cbm_find_child_by_kind(node, "procedure_specification"); if (ts_node_is_null(spec)) { spec = cbm_find_child_by_kind(node, "function_specification"); } if (!ts_node_is_null(spec)) { TSNode nm = ts_node_child_by_field_name(spec, TS_FIELD("name")); if (!ts_node_is_null(nm)) { return nm; } } } /* Pascal: defProc carries the `name` field on its `header` (declProc) child. */ if (lang == CBM_LANG_PASCAL && strcmp(kind, "defProc") == 0) { TSNode hdr = ts_node_child_by_field_name(node, TS_FIELD("header")); if (!ts_node_is_null(hdr)) { TSNode nm = ts_node_child_by_field_name(hdr, TS_FIELD("name")); if (!ts_node_is_null(nm)) { return nm; } } } /* Just: a `recipe` carries its name on the nested `recipe_header`'s * `name` field (an identifier), not on the recipe node itself. */ if (lang == CBM_LANG_JUST && strcmp(kind, "recipe") == 0) { TSNode hdr = cbm_find_child_by_kind(node, "recipe_header"); if (!ts_node_is_null(hdr)) { TSNode nm = ts_node_child_by_field_name(hdr, TS_FIELD("name")); if (!ts_node_is_null(nm)) { return nm; } } } /* ReScript: the `function` (arrow) node — already in func_types — has no * name; the binding name is on the enclosing let_binding's `pattern` field. * Resolving via the parent keeps plain value let-bindings out of func_types. */ if (lang == CBM_LANG_RESCRIPT && strcmp(kind, "function") == 0) { TSNode parent = ts_node_parent(node); if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "let_binding") == 0) { TSNode pat = ts_node_child_by_field_name(parent, TS_FIELD("pattern")); if (!ts_node_is_null(pat)) { return pat; } } } /* Nickel: the lambda is a `fun_expr` with no name; the binding name is on * the enclosing let_binding's `pat` field (a `pattern` wrapping an `ident`). * Resolving via the parent keeps anonymous lambdas (e.g. `map (fun x => x) * xs`), whose parent is not a let_binding, out of func_types. */ if (lang == CBM_LANG_NICKEL && strcmp(kind, "fun_expr") == 0) { TSNode parent = ts_node_parent(node); /* let_binding wraps the bound term in a `term`/`uni_term` chain, so the * fun_expr's immediate parent is not the let_binding directly. */ for (int up = 0; up < FUNC_PARENT_CLIMB_LIMIT && !ts_node_is_null(parent); up++) { if (strcmp(ts_node_type(parent), "let_binding") == 0) { TSNode pat = ts_node_child_by_field_name(parent, TS_FIELD("pat")); if (!ts_node_is_null(pat)) { TSNode inner = ts_node_child_by_field_name(pat, TS_FIELD("pat")); return ts_node_is_null(inner) ? pat : inner; } break; } parent = ts_node_parent(parent); } } /* Nix: a named function is a `function_expression` (lambda `x: body`) with * no name of its own — the binding name lives on the enclosing `binding`'s * `attrpath` field (`name = x: ...`). Resolve through the parent binding to * the attrpath's `attr` identifier so `addOne = x: ...` mints a Function * def. A lambda whose parent is not a binding (e.g. an inline `map (x: x)` * argument) resolves null and stays out of func_types. */ if (lang == CBM_LANG_NIX && strcmp(kind, "function_expression") == 0) { TSNode parent = ts_node_parent(node); if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "binding") == 0) { TSNode attrpath = ts_node_child_by_field_name(parent, TS_FIELD("attrpath")); if (!ts_node_is_null(attrpath)) { TSNode attr = ts_node_child_by_field_name(attrpath, TS_FIELD("attr")); return ts_node_is_null(attr) ? attrpath : attr; } } } /* Fortran: subroutine/function wrap an inner *_statement that carries the * `name` field; the outer node walk_defs matched has no name itself. */ if (lang == CBM_LANG_FORTRAN && (strcmp(kind, "subroutine") == 0 || strcmp(kind, "function") == 0)) { TSNode stmt = cbm_find_child_by_kind(node, "subroutine_statement"); if (ts_node_is_null(stmt)) { stmt = cbm_find_child_by_kind(node, "function_statement"); } if (!ts_node_is_null(stmt)) { TSNode nm = ts_node_child_by_field_name(stmt, TS_FIELD("name")); if (!ts_node_is_null(nm)) { return nm; } } } /* F#: function_or_value_defn's name is on a function_declaration_left / * value_declaration_left child (a bare identifier, no `name` field). */ if (lang == CBM_LANG_FSHARP && strcmp(kind, "function_or_value_defn") == 0) { TSNode lhs = cbm_find_child_by_kind(node, "function_declaration_left"); if (ts_node_is_null(lhs)) { lhs = cbm_find_child_by_kind(node, "value_declaration_left"); } if (!ts_node_is_null(lhs)) { TSNode nm = cbm_find_child_by_kind(lhs, "identifier"); if (ts_node_is_null(nm)) { nm = cbm_find_child_by_kind(lhs, "long_identifier"); } if (!ts_node_is_null(nm)) { return nm; } } } /* Groovy: top-level function_definition carries the name on the `function` * field (not `name`); fall back to the first `identifier` child. */ if (lang == CBM_LANG_GROOVY && strcmp(kind, "function_definition") == 0) { TSNode fn = ts_node_child_by_field_name(node, TS_FIELD("function")); if (ts_node_is_null(fn)) { fn = cbm_find_child_by_kind(node, "identifier"); } if (!ts_node_is_null(fn)) { return fn; } } /* Agda (FIELD_COUNT 0): the only `function` carrying the name is the type * signature line, whose lhs holds a `function_name` alias child. The body * line's lhs has no function_name child -> resolves null and is skipped. */ if (lang == CBM_LANG_AGDA && strcmp(kind, "function") == 0) { TSNode lhs = cbm_find_child_by_kind(node, "lhs"); if (!ts_node_is_null(lhs)) { TSNode fn = cbm_find_child_by_kind(lhs, "function_name"); if (!ts_node_is_null(fn)) { return fn; } } } /* Pony: def nodes have no `name` field; the name is the first plain * `identifier` child after the keyword/annotation/capability. */ if (lang == CBM_LANG_PONY && (strcmp(kind, "method") == 0 || strcmp(kind, "constructor") == 0 || strcmp(kind, "ffi_method") == 0)) { TSNode id = cbm_find_child_by_kind(node, "identifier"); if (!ts_node_is_null(id)) { return id; } } /* COBOL: program_definition has no `name` field; the program name is * identification_division > program_name (a leaf holding the clean name). */ if (lang == CBM_LANG_COBOL && strcmp(kind, "program_definition") == 0) { TSNode iddiv = cbm_find_child_by_kind(node, "identification_division"); if (!ts_node_is_null(iddiv)) { TSNode pname = cbm_find_child_by_kind(iddiv, "program_name"); if (!ts_node_is_null(pname)) { return pname; } } } /* Teal: the `local function foo()` form reduces to a function_statement * whose name is carried on a `function_name` child rather than the `name` * field (the field is only populated for the bare `function foo()` form). * func_name_node() already handled the field case above; here we cover the * function_name child so local functions also produce a Function def. */ if (lang == CBM_LANG_TEAL && (strcmp(kind, "function_statement") == 0 || strcmp(kind, "function_signature") == 0)) { TSNode fn = cbm_find_child_by_kind(node, "function_name"); if (!ts_node_is_null(fn)) { return fn; } } /* SCSS: function_statement/mixin_statement have no `name` field; the def * name is a plain `name` child node. */ if (lang == CBM_LANG_SCSS && (strcmp(kind, "function_statement") == 0 || strcmp(kind, "mixin_statement") == 0)) { TSNode nm = cbm_find_child_by_kind(node, "name"); if (!ts_node_is_null(nm)) { return nm; } } /* Jsonnet: a function binding is a `bind` node carrying the name on the * `function` field (an `id`), plus a `params` field. Plain value binds * (`local x = 1`) have no `params` field -> resolve null -> skipped, so * only function binds become Function defs. */ if (lang == CBM_LANG_JSONNET && strcmp(kind, "bind") == 0) { TSNode params = ts_node_child_by_field_name(node, TS_FIELD("params")); if (!ts_node_is_null(params)) { TSNode nm = ts_node_child_by_field_name(node, TS_FIELD("function")); if (!ts_node_is_null(nm)) { return nm; } } } /* Typst: `#let greet(name) = ...` parses to a `let` whose `pattern` field * is a `call` node (the function signature); the name is that call's * `item` field (an ident). A plain `#let x = 1` has a non-call pattern -> * resolve null -> skipped, keeping value bindings out of func_types. */ if (lang == CBM_LANG_TYPST && strcmp(kind, "let") == 0) { TSNode pat = ts_node_child_by_field_name(node, TS_FIELD("pattern")); if (!ts_node_is_null(pat) && strcmp(ts_node_type(pat), "call") == 0) { TSNode item = ts_node_child_by_field_name(pat, TS_FIELD("item")); if (!ts_node_is_null(item)) { return item; } } } /* SQL: create_function has no `name` field; the function name is nested as * object_reference > `name` field (an identifier). */ if (lang == CBM_LANG_SQL && strcmp(kind, "create_function") == 0) { TSNode oref = cbm_find_child_by_kind(node, "object_reference"); if (!ts_node_is_null(oref)) { TSNode nm = ts_node_child_by_field_name(oref, TS_FIELD("name")); if (!ts_node_is_null(nm)) { return nm; } } } /* Elm: value_declaration carries its name on the * `functionDeclarationLeft` field's function_declaration_left child, * whose first lower_case_identifier is the function name. */ if (lang == CBM_LANG_ELM && strcmp(kind, "value_declaration") == 0) { TSNode lhs = ts_node_child_by_field_name(node, TS_FIELD("functionDeclarationLeft")); if (ts_node_is_null(lhs)) { lhs = cbm_find_child_by_kind(node, "function_declaration_left"); } if (!ts_node_is_null(lhs)) { TSNode nm = cbm_find_child_by_kind(lhs, "lower_case_identifier"); if (!ts_node_is_null(nm)) { return nm; } } } /* Pine Script: function_declaration_statement carries the name on the * `function` field (or `method` field for the method form), not `name`. */ if (lang == CBM_LANG_PINE && strcmp(kind, "function_declaration_statement") == 0) { TSNode nm = ts_node_child_by_field_name(node, TS_FIELD("function")); if (ts_node_is_null(nm)) { nm = ts_node_child_by_field_name(node, TS_FIELD("method")); } if (!ts_node_is_null(nm)) { return nm; } } /* Smali (no `name` field): method_definition > method_signature > * method_identifier holds the method name. */ if (lang == CBM_LANG_SMALI && strcmp(kind, "method_definition") == 0) { TSNode sig = cbm_find_child_by_kind(node, "method_signature"); if (!ts_node_is_null(sig)) { TSNode mid = cbm_find_child_by_kind(sig, "method_identifier"); if (!ts_node_is_null(mid)) { return mid; } } } /* Verilog/SystemVerilog (FIELD_COUNT 0): function/task names live on a * nested *_identifier wrapper; the function name is the first * simple_identifier descendant (params/returns come after the name). */ if ((lang == CBM_LANG_VERILOG || lang == CBM_LANG_SYSTEMVERILOG) && (strcmp(kind, "function_declaration") == 0 || strcmp(kind, "task_declaration") == 0)) { TSNode si = find_first_descendant_by_kind(node, "simple_identifier", CBM_DESCENDANT_MAX_DEPTH); if (!ts_node_is_null(si)) { return si; } } /* VHDL: subprogram_declaration/_definition carry the name on a nested * function_specification/procedure_specification child, via the * `function`/`procedure` field. */ if (lang == CBM_LANG_VHDL && (strcmp(kind, "subprogram_declaration") == 0 || strcmp(kind, "subprogram_definition") == 0)) { TSNode spec = cbm_find_child_by_kind(node, "function_specification"); if (ts_node_is_null(spec)) { spec = cbm_find_child_by_kind(node, "procedure_specification"); } if (!ts_node_is_null(spec)) { TSNode nm = ts_node_child_by_field_name(spec, TS_FIELD("function")); if (ts_node_is_null(nm)) { nm = ts_node_child_by_field_name(spec, TS_FIELD("procedure")); } if (!ts_node_is_null(nm)) { return nm; } } } /* Thrift / Cap'n Proto / Smithy (no `name` field): the def name is a * plain visible `identifier` child of the statement/definition node. */ if (lang == CBM_LANG_THRIFT || lang == CBM_LANG_SMITHY) { TSNode id = cbm_find_child_by_kind(node, "identifier"); if (!ts_node_is_null(id)) { return id; } } /* Cap'n Proto (FIELD_COUNT 0): name is an aliased *_identifier child. */ if (lang == CBM_LANG_CAPNP) { const char *name_kind = NULL; if (strcmp(kind, "struct") == 0 || strcmp(kind, "interface") == 0) { name_kind = "type_identifier"; } else if (strcmp(kind, "enum") == 0) { name_kind = "enum_identifier"; } else if (strcmp(kind, "method") == 0) { name_kind = "method_identifier"; } if (name_kind) { TSNode id = cbm_find_child_by_kind(node, name_kind); if (!ts_node_is_null(id)) { return id; } } } /* CMake (FIELD_COUNT 0): function(foo)/macro(foo) — the name is nested as * *_command > argument_list > argument > unquoted_argument. */ if (lang == CBM_LANG_CMAKE && (strcmp(kind, "function_def") == 0 || strcmp(kind, "macro_def") == 0)) { const char *cmd_kind = strcmp(kind, "function_def") == 0 ? "function_command" : "macro_command"; TSNode cmd = cbm_find_child_by_kind(node, cmd_kind); if (!ts_node_is_null(cmd)) { TSNode alist = cbm_find_child_by_kind(cmd, "argument_list"); if (!ts_node_is_null(alist)) { TSNode arg = cbm_find_child_by_kind(alist, "argument"); if (!ts_node_is_null(arg)) { TSNode uq = cbm_find_child_by_kind(arg, "unquoted_argument"); return ts_node_is_null(uq) ? arg : uq; } } } } /* Puppet: function_declaration name is a plain identifier/class_identifier * child (no `name` field). */ if (lang == CBM_LANG_PUPPET && (strcmp(kind, "function_declaration") == 0 || strcmp(kind, "lambda") == 0)) { TSNode id = cbm_find_child_by_kind(node, "identifier"); if (ts_node_is_null(id)) { id = cbm_find_child_by_kind(node, "class_identifier"); } if (!ts_node_is_null(id)) { return id; } } /* Assembly (GAS): a bare `label` (`foo:`) reduces with no `name` field; * its name child is aliased to `ident`. */ if (lang == CBM_LANG_ASSEMBLY && strcmp(kind, "label") == 0) { TSNode id = cbm_find_child_by_kind(node, "ident"); if (!ts_node_is_null(id)) { return id; } } /* BitBake: a shell task `do_foo() {...}` is a function_definition and a * python task `python do_foo() {...}` is an anonymous_python_function; * both carry the task name on a direct `identifier` child (no `name` * field). */ if (lang == CBM_LANG_BITBAKE && (strcmp(kind, "function_definition") == 0 || strcmp(kind, "anonymous_python_function") == 0)) { TSNode id = cbm_find_child_by_kind(node, "identifier"); if (!ts_node_is_null(id)) { return id; } } /* PKL: a classMethod/objectMethod (`function foo(): T = ...`) has no * `name` field; the name is the `identifier` inside its methodHeader * child. */ if (lang == CBM_LANG_PKL && (strcmp(kind, "classMethod") == 0 || strcmp(kind, "objectMethod") == 0)) { TSNode hdr = cbm_find_child_by_kind(node, "methodHeader"); if (!ts_node_is_null(hdr)) { TSNode id = cbm_find_child_by_kind(hdr, "identifier"); if (!ts_node_is_null(id)) { return id; } } } { TSNode r = resolve_toplevel_arrow_name(node, kind); if (!ts_node_is_null(r)) { return r; } } { TSNode r = resolve_func_name_scripting(node, lang, kind); if (!ts_node_is_null(r)) { return r; } } { TSNode r = resolve_func_name_fp(node, lang, kind, name); if (!ts_node_is_null(r)) { return r; } } { TSNode prev = node; TSNode r = resolve_func_name_c_family(&node, lang, kind); if (!ts_node_is_null(r)) { return r; } if (!ts_node_eq(prev, node)) { continue; /* template unwrapped — retry */ } } break; } /* end template depth loop */ TSNode null_node = {0}; return null_node; } // Check for export_statement ancestor (JS/TS/TSX) static bool is_js_exported(TSNode node) { return cbm_has_ancestor_kind(node, "export_statement", EXPORT_ANCESTOR_DEPTH); } // 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 || strcmp(kind, "multiline_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, TS_FIELD("body")); 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) { if (lang == CBM_LANG_GO) { const char *doc = extract_go_type_docstring(a, node, source); if (doc) { return doc; } } TSNode prev = ts_node_prev_sibling(node); if (!ts_node_is_null(prev) && is_comment_node(ts_node_type(prev))) { return extract_comment_text(a, prev, source); } if (lang == CBM_LANG_PYTHON) { return extract_python_docstring(a, node, source); } return NULL; } static TSNode find_jvm_modifiers(TSNode node, CBMLanguage lang); /* HTTP method names recognized in decorator calls (e.g., @router.post → "POST") */ static const char *decorator_method_name(const char *attr_text) { if (!attr_text) { return NULL; } /* Match the last segment after the dot: "router.post" → "post" */ const char *dot = strrchr(attr_text, '.'); const char *method = dot ? dot + SKIP_CHAR : attr_text; if (strcmp(method, "get") == 0 || strcmp(method, "Get") == 0) { return "GET"; } if (strcmp(method, "post") == 0 || strcmp(method, "Post") == 0) { return "POST"; } if (strcmp(method, "put") == 0 || strcmp(method, "Put") == 0) { return "PUT"; } if (strcmp(method, "delete") == 0 || strcmp(method, "Delete") == 0) { return "DELETE"; } if (strcmp(method, "patch") == 0 || strcmp(method, "Patch") == 0) { return "PATCH"; } if (strcmp(method, "route") == 0 || strcmp(method, "api_route") == 0) { return "ANY"; } return NULL; } /* HTTP method for a Spring/JAX-RS style annotation name (e.g. "GetMapping" → * "GET", "RequestMapping" → "ANY", JAX-RS "GET" → "GET"). Returns NULL when the * annotation is not a route-mapping annotation. */ static const char *annotation_route_method(const char *name) { if (!name) { return NULL; } if (strcmp(name, "GetMapping") == 0) { return "GET"; } if (strcmp(name, "PostMapping") == 0) { return "POST"; } if (strcmp(name, "PutMapping") == 0) { return "PUT"; } if (strcmp(name, "DeleteMapping") == 0) { return "DELETE"; } if (strcmp(name, "PatchMapping") == 0) { return "PATCH"; } if (strcmp(name, "RequestMapping") == 0) { return "ANY"; } /* JAX-RS bare-verb annotations (@GET/@POST/...) — path comes from @Path. */ if (strcmp(name, "GET") == 0 || strcmp(name, "POST") == 0 || strcmp(name, "PUT") == 0 || strcmp(name, "DELETE") == 0 || strcmp(name, "PATCH") == 0) { return name; } return NULL; } /* Extract route path + method from a decorator's AST nodes. * Works for: @app.route("/path"), @router.post("/path"), @GetMapping("/path"), * @app.get("/path", ...), etc. * * 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 */ // 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, TS_FIELD("arguments")); 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; } static bool is_route_string_kind(const char *kind) { return strcmp(kind, "string") == 0 || strcmp(kind, "string_literal") == 0 || strcmp(kind, "interpreted_string_literal") == 0; } static const char *route_path_from_string_node(CBMArena *a, TSNode node, const char *source) { if (!is_route_string_kind(ts_node_type(node))) { return NULL; } char *path = cbm_node_text(a, node, source); if (!path) { return NULL; } int plen = (int)strlen(path); if (plen >= PAIR_CHARS && (path[0] == '"' || path[0] == '\'')) { path = cbm_arena_strndup(a, path + SKIP_CHAR, (size_t)(plen - PAIR_CHARS)); } return (path && path[0] == '/') ? path : NULL; } static const char *find_route_path_literal(CBMArena *a, TSNode node, const char *source, int max_depth) { if (ts_node_is_null(node) || max_depth < 0) { return NULL; } const char *path = route_path_from_string_node(a, node, source); if (path || max_depth == 0) { return path; } uint32_t nc = ts_node_named_child_count(node); for (uint32_t i = 0; i < nc && i < DECORATOR_SCAN_LIMIT; i++) { path = find_route_path_literal(a, ts_node_named_child(node, i), source, max_depth - 1); if (path) { return path; } } return NULL; } // 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 < DECORATOR_SCAN_LIMIT; ai++) { TSNode arg = ts_node_named_child(args, ai); /* Spring/Kotlin frequently uses named or array-valued annotation args: * @RequestMapping(value = ["/internal/v1"]) * @GetMapping(path = {"/orders"}) * Walk a bounded subtree and keep the first string literal that is * path-shaped, while ignoring non-route literals such as media types. */ const char *path = find_route_path_literal(a, arg, source, CBM_DESCENDANT_MAX_DEPTH); if (path) { return path; } } return NULL; } // Find a keyword argument by name in an argument_list node and return its value child. static TSNode find_drf_kwarg_in_args(CBMArena *a, TSNode args, const char *kwarg_name, const char *source) { uint32_t nc = ts_node_named_child_count(args); for (uint32_t ai = 0; ai < nc; ai++) { TSNode child = ts_node_named_child(args, ai); if (strcmp(ts_node_type(child), "keyword_argument") != 0) continue; TSNode name_node = ts_node_child_by_field_name(child, TS_FIELD("name")); if (ts_node_is_null(name_node)) continue; char *name = cbm_node_text(a, name_node, source); if (name && strcmp(name, kwarg_name) == 0) { return ts_node_child_by_field_name(child, TS_FIELD("value")); } } TSNode null_node = {0}; return null_node; } // Try to extract a route from a Django REST Framework @action decorator on a // ViewSet method: // @action(detail=True, methods=["post"], url_path="approve") // Sets *out_path/*out_method so the downstream Route+HANDLES pipeline (Phase 2a // ensure_one_decorator_route) emits the handler->Route edge in the normal // direction. Falls back to the method name for url_path and "GET" for methods. // Known limitation: multi-method actions (methods=["get","post"]) capture only // the first method -> a single Route rather than one per method. static bool try_drf_action_decorator(CBMArena *a, TSNode dchild, const char *source, TSNode func_node, const char **out_path, const char **out_method) { TSNode fn = ts_node_child_by_field_name(dchild, TS_FIELD("function")); if (ts_node_is_null(fn)) { fn = ts_node_named_child(dchild, 0); } if (ts_node_is_null(fn)) { return false; } const char *fn_type = ts_node_type(fn); if (strcmp(fn_type, "identifier") != 0) { return false; } char *fn_text = cbm_node_text(a, fn, source); if (!fn_text || strcmp(fn_text, "action") != 0) { return false; } TSNode args = find_decorator_args(dchild); if (ts_node_is_null(args)) { return false; } const char *method = NULL; TSNode methods_val = find_drf_kwarg_in_args(a, args, "methods", source); if (!ts_node_is_null(methods_val) && strcmp(ts_node_type(methods_val), "list") == 0) { uint32_t mc = ts_node_named_child_count(methods_val); for (uint32_t mi = 0; mi < mc && !method; mi++) { TSNode item = ts_node_named_child(methods_val, mi); if (strcmp(ts_node_type(item), "string") != 0) continue; char *text = cbm_node_text(a, item, source); if (!text) continue; int tlen = (int)strlen(text); if (tlen < PAIR_CHARS || (text[0] != '"' && text[0] != '\'')) continue; char inner[CBM_SZ_16]; int ilen = tlen - PAIR_CHARS; if (ilen <= 0 || ilen >= (int)sizeof(inner)) continue; memcpy(inner, text + SKIP_CHAR, (size_t)ilen); inner[ilen] = '\0'; for (int ci = 0; inner[ci]; ci++) { if (inner[ci] >= 'a' && inner[ci] <= 'z') inner[ci] -= 32; } method = cbm_arena_strdup(a, inner); } } if (!method) { method = "GET"; } const char *segment = NULL; TSNode url_path_val = find_drf_kwarg_in_args(a, args, "url_path", source); if (!ts_node_is_null(url_path_val) && strcmp(ts_node_type(url_path_val), "string") == 0) { char *text = cbm_node_text(a, url_path_val, source); if (text) { int tlen = (int)strlen(text); if (tlen >= PAIR_CHARS && (text[0] == '"' || text[0] == '\'')) { segment = cbm_arena_strndup(a, text + SKIP_CHAR, (size_t)(tlen - PAIR_CHARS)); } } } if (!segment) { TSNode name_node = func_name_node(func_node); if (!ts_node_is_null(name_node)) { segment = cbm_node_text(a, name_node, source); } } if (!segment) { return false; } // Extract detail kwarg (default True in DRF) bool detail = true; TSNode detail_val = find_drf_kwarg_in_args(a, args, "detail", source); if (!ts_node_is_null(detail_val)) { const char *dv = ts_node_type(detail_val); if (strcmp(dv, "false") == 0) { detail = false; } } if (detail) { *out_path = cbm_arena_sprintf(a, "/{pk}/%s", segment); } else { *out_path = cbm_arena_sprintf(a, "/%s", segment); } *out_method = method; return true; } // 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, TS_FIELD("function")); 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; } /* Resolve an annotation's name node across grammars. Java exposes a `name` * field; tree-sitter-kotlin does not — its annotation name lives in a nested * type_identifier: * @Foo -> (annotation (user_type (type_identifier))) * @Foo("/x") -> (annotation (constructor_invocation (user_type (type_identifier)) * (value_arguments ...))) * Returns a null node when no name can be resolved. */ static TSNode annotation_name_node(TSNode annotation) { TSNode name = ts_node_child_by_field_name(annotation, TS_FIELD("name")); if (!ts_node_is_null(name)) { return name; } TSNode ut = cbm_find_child_by_kind(annotation, "user_type"); if (ts_node_is_null(ut)) { TSNode ci = cbm_find_child_by_kind(annotation, "constructor_invocation"); if (!ts_node_is_null(ci)) { ut = cbm_find_child_by_kind(ci, "user_type"); } } if (!ts_node_is_null(ut)) { TSNode ti = cbm_find_child_by_kind(ut, "type_identifier"); if (ts_node_is_null(ti)) { ti = cbm_find_child_by_kind(ut, "simple_identifier"); } return ti; } TSNode null_node = {0}; return null_node; } /* Resolve an annotation's argument list across grammars. Kotlin keeps the args * under a `constructor_invocation` child as a `value_arguments` node rather than * the `arguments` field / `argument_list` child that Java exposes. */ static TSNode annotation_args_node(TSNode annotation) { TSNode args = ts_node_child_by_field_name(annotation, TS_FIELD("arguments")); if (!ts_node_is_null(args)) { return args; } args = find_decorator_args(annotation); if (!ts_node_is_null(args)) { return args; } TSNode ci = cbm_find_child_by_kind(annotation, "constructor_invocation"); if (!ts_node_is_null(ci)) { return cbm_find_child_by_kind(ci, "value_arguments"); } return args; } /* Try to extract a route from a Java/JVM/Kotlin annotation node (`annotation` or * `marker_annotation`). Spring mapping annotations carry the HTTP method in the * annotation name and the path in the (optional) argument list: * @GetMapping("/orders") @RequestMapping(value="/api") @PostMapping * Returns true when the annotation is a route-mapping annotation. */ static bool try_route_from_annotation(CBMArena *a, TSNode annotation, const char *source, const char **out_path, const char **out_method) { TSNode name_node = annotation_name_node(annotation); if (ts_node_is_null(name_node)) { return false; } char *name = cbm_node_text(a, name_node, source); const char *method = annotation_route_method(name); if (!method) { return false; } TSNode args = annotation_args_node(annotation); const char *path = NULL; if (!ts_node_is_null(args)) { path = extract_route_path_from_args(a, args, source); } *out_path = path ? path : "/"; *out_method = method; return true; } /* Scan the annotation nodes nested in a JVM/C# `modifiers`/`attribute_list` * wrapper (and direct children) for a route-mapping annotation. Java/Kotlin * Spring annotations (@GetMapping, @RequestMapping, ...) live here rather than * as prev-siblings, so the prev-sibling decorator walk never sees them. */ static bool extract_route_from_annotations(CBMArena *a, TSNode func_node, const char *source, const CBMLangSpec *spec, const char **out_path, const char **out_method) { TSNode modifiers = find_jvm_modifiers(func_node, spec->language); 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) && try_route_from_annotation(a, mchild, source, out_path, out_method)) { return true; } } } /* Direct-child annotations (some grammars attach the annotation as a child * of the method node rather than under `modifiers`). */ uint32_t cc = ts_node_child_count(func_node); for (uint32_t ci = 0; ci < cc; ci++) { TSNode child = ts_node_child(func_node, ci); if (cbm_kind_in_set(child, spec->decorator_node_types) && try_route_from_annotation(a, child, source, out_path, out_method)) { return true; } } return false; } 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) { *out_path = NULL; *out_method = NULL; if (!spec->decorator_node_types || !spec->decorator_node_types[0]) { return; } TSNode prev = ts_node_prev_sibling(func_node); while (!ts_node_is_null(prev)) { if (!cbm_kind_in_set(prev, spec->decorator_node_types)) { break; } 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); if (strcmp(ts_node_type(dchild), "call") != 0) { continue; } if (try_route_from_decorator_call(a, dchild, source, out_path, out_method)) { return; } if (try_drf_action_decorator(a, dchild, source, func_node, out_path, out_method)) { return; } } /* JVM/C# annotation-form route mapping (Spring @GetMapping etc.) — the * prev-sibling itself may be the annotation node. */ if (try_route_from_annotation(a, prev, source, out_path, out_method)) { return; } prev = ts_node_prev_sibling(prev); } /* Spring/JAX-RS annotations live inside the method's `modifiers` child, not * as prev-siblings — scan there too. */ extract_route_from_annotations(a, func_node, source, spec, out_path, out_method); } static const char *join_route_paths(CBMArena *a, const char *prefix, const char *path) { if (!path || !path[0]) { return prefix; } if (!prefix || !prefix[0] || strcmp(prefix, "/") == 0) { return path; } if (strcmp(path, "/") == 0) { return prefix; } size_t plen = strlen(prefix); bool prefix_slash = prefix[plen - 1] == '/'; bool path_slash = path[0] == '/'; if (prefix_slash && path_slash) { return cbm_arena_sprintf(a, "%s%s", prefix, path + SKIP_CHAR); } if (!prefix_slash && !path_slash) { return cbm_arena_sprintf(a, "%s/%s", prefix, path); } return cbm_arena_sprintf(a, "%s%s", prefix, path); } static const char *spring_class_route_prefix(CBMArena *a, TSNode class_node, const char *source, const CBMLangSpec *spec) { const char *prefix = NULL; const char *method = NULL; if (extract_route_from_annotations(a, class_node, source, spec, &prefix, &method)) { return prefix; } return NULL; } // Extract decorator names from preceding decorator/annotation nodes // 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 the wrapper child that holds annotations/attributes for languages where // they are nested under an intermediate node rather than being a prev-sibling: // Java/Kotlin/C#/Swift → `modifiers` (contains annotation/attribute) // PHP 8 → `attribute_list` (contains attribute_group) // Returns a null node when the language has no such wrapper. static TSNode find_jvm_modifiers(TSNode node, CBMLanguage lang) { TSNode null_node = {0}; const char *wrapper = NULL; switch (lang) { case CBM_LANG_JAVA: case CBM_LANG_KOTLIN: case CBM_LANG_SWIFT: wrapper = "modifiers"; break; case CBM_LANG_CSHARP: case CBM_LANG_PHP: /* C# attributes live in an `attribute_list` child (modifiers like * `public` are separate `modifier` nodes); PHP 8 likewise nests * `attribute_group` under `attribute_list`. */ wrapper = "attribute_list"; break; default: return null_node; } TSNode w = ts_node_child_by_field_name(node, wrapper, (uint32_t)strlen(wrapper)); if (ts_node_is_null(w)) { w = cbm_find_child_by_kind(node, wrapper); } return w; } // Count direct children of `node` that are decorator/annotation nodes (used by // languages like Scala where the annotation is a direct child of the def node). static int count_child_decorators(TSNode node, const CBMLangSpec *spec) { int count = 0; uint32_t cc = ts_node_child_count(node); for (uint32_t ci = 0; ci < cc; ci++) { TSNode child = ts_node_child(node, ci); if (cbm_kind_in_set(child, spec->decorator_node_types)) { count++; } } return count; } // Collect direct-child decorator texts into result[] starting at idx. static int collect_child_decorators(CBMArena *a, TSNode node, const char *source, const CBMLangSpec *spec, const char **result, int idx, int max) { uint32_t cc = ts_node_child_count(node); for (uint32_t ci = 0; ci < cc && idx < max; ci++) { TSNode child = ts_node_child(node, ci); if (cbm_kind_in_set(child, spec->decorator_node_types)) { result[idx++] = cbm_node_text(a, child, source); } } return idx; } // 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; } 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 if (ts_node_is_named(prev)) { /* A real preceding construct ends the decorator run. Anonymous * tokens (e.g. TS `export` between `@Decorator` and the * `class_declaration`) are skipped so the decorator is still seen. */ break; } prev = ts_node_prev_sibling(prev); } TSNode modifiers = {0}; int mod_count = 0; int child_count = 0; if (count == 0) { modifiers = find_jvm_modifiers(node, lang); if (!ts_node_is_null(modifiers)) { mod_count = count_modifier_annotations(modifiers, spec); } /* Languages like Scala attach the annotation directly as a child of the * definition node (no wrapper, no prev-sibling). */ if (mod_count == 0) { child_count = count_child_decorators(node, spec); } } int total = count + mod_count + child_count; if (total == 0) { return NULL; } const char **result = (const char **)cbm_arena_alloc(a, sizeof(const char *) * (total + NULL_TERM)); if (!result) { return NULL; } int idx = 0; prev = ts_node_prev_sibling(node); while (!ts_node_is_null(prev) && idx < count) { if (cbm_kind_in_set(prev, spec->decorator_node_types)) { result[idx++] = cbm_node_text(a, prev, source); } else if (ts_node_is_named(prev)) { break; } prev = ts_node_prev_sibling(prev); } if (!ts_node_is_null(modifiers)) { idx = collect_modifier_decorators(a, modifiers, source, spec, result, idx, total); } if (child_count > 0) { idx = collect_child_decorators(a, node, source, spec, result, idx, total); } result[idx] = NULL; return result; } /* Rust: two same-named functions guarded by mutually-exclusive #[cfg(...)] * attributes both parse as distinct function_item nodes and otherwise receive * the SAME qualified_name, so the second graph upsert silently overwrites the * first and one branch is lost (#495). Fold the cfg predicate into the QN so * each cfg-gated twin gets a DISTINCT, predicate-encoding QN. Returns the * (possibly suffixed) QN; the original QN when no cfg attribute is present. */ /* Rust: mark a function as a test when it carries a test attribute (#855). * cbm's test detection is otherwise file-path-based (cbm_is_test_file: * *_test.rs / test_*), so inline #[test]/#[tokio::test] functions inside a * regular .rs file are indexed as ordinary Functions (is_test=false) and leak * past the store.c `is_test != 1` filter into graph/agent context. The * attribute_item text extract_decorators stores is the bracketed form * ("#[test]", "#[tokio::test]", "#[tokio::test(...)]", ...). */ static bool rust_def_is_test(const char *const *decorators) { if (!decorators) { return false; } for (int i = 0; decorators[i]; i++) { const char *d = decorators[i]; /* Path-qualified async/param test macros (substring match, robust to the * optional argument list and the surrounding #[ ]). */ if (strstr(d, "tokio::test") || strstr(d, "async_std::test") || strstr(d, "actix_rt::test") || strstr(d, "test_case::case")) { return true; } /* Bare #[test] / #[test(...)]: match the bracketed path exactly so we do * NOT match the unrelated #[test_case::case] (handled above) or a * hypothetical #[test_crate]. */ if (strstr(d, "#[test]") || strstr(d, "#[test(")) { return true; } } return false; } static const char *rust_cfg_qualified_name(CBMArena *a, const char *base_qn, const char *const *decorators) { if (!decorators) { return base_qn; } for (int i = 0; decorators[i]; i++) { const char *cfg = strstr(decorators[i], "cfg("); if (!cfg) { continue; } /* Build a compact predicate suffix from the cfg(...) text, dropping * whitespace and quotes so the QN stays readable and stable. */ char buf[CBM_SZ_256]; size_t bi = 0; for (const char *p = cfg; *p && bi + 1 < sizeof(buf); p++) { if (*p == ' ' || *p == '\t' || *p == '"' || *p == '\'') { continue; } buf[bi++] = *p; } buf[bi] = '\0'; return cbm_arena_sprintf(a, "%s#%s", base_qn, buf); } return base_qn; } // 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) { /* A qualified base may embed a template_type in its `name` field * (e.g. `std::vector`), so the raw text carries `<...>` args. * Strip the generic argument list to keep the bare qualified name. */ char *t = cbm_node_text(a, bc, source); if (t) { char *angle = strchr(t, '<'); if (angle) { *angle = '\0'; } } return t; } if (strcmp(bk, "template_type") == 0) { TSNode tname = ts_node_child_by_field_name(bc, TS_FIELD("name")); 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[MAX_BASES]; 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 + NULL_TERM) * 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 *) * RT_PAIR_SIZE); if (result) { result[0] = text; result[SKIP_CHAR] = 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 text from a single C# base_list named child, stripping generic args. */ static const char *extract_csharp_base_child_text(CBMArena *a, TSNode bc, const char *source) { const char *bk = ts_node_type(bc); char *text = NULL; if (strcmp(bk, "identifier") == 0 || strcmp(bk, "generic_name") == 0 || strcmp(bk, "qualified_name") == 0) { text = cbm_node_text(a, bc, source); } else { TSNode inner = ts_node_named_child(bc, 0); if (!ts_node_is_null(inner)) { text = cbm_node_text(a, inner, source); } } if (text && text[0]) { char *angle = strchr(text, '<'); if (angle) { *angle = '\0'; } return text; } return NULL; } /* Collect bases from a single base_list node into an arena-allocated array. */ static const char **collect_csharp_bases(CBMArena *a, TSNode base_list, const char *source) { const char *bases[MAX_BASES]; int base_count = 0; uint32_t bnc = ts_node_named_child_count(base_list); for (uint32_t bi = 0; bi < bnc && base_count < MAX_BASES_MINUS_1; bi++) { const char *text = extract_csharp_base_child_text(a, ts_node_named_child(base_list, bi), source); if (text) { bases[base_count++] = text; } } if (base_count == 0) { return NULL; } const char **result = (const char **)cbm_arena_alloc(a, (base_count + NULL_TERM) * sizeof(const char *)); if (!result) { return NULL; } for (int j = 0; j < base_count; j++) { result[j] = bases[j]; } result[base_count] = NULL; return result; } /* C# base_list: iterate children, find base_list node, extract bases. */ static const char **extract_csharp_base_list(CBMArena *a, TSNode node, const char *source, uint32_t count) { for (uint32_t i = 0; i < count; i++) { TSNode child = ts_node_child(node, i); if (strcmp(ts_node_type(child), "base_list") != 0) { continue; } const char **result = collect_csharp_bases(a, child, source); if (result) { return result; } } return NULL; } // Append a base name (generic args stripped) to out[] if non-empty. static void push_base_text(CBMArena *a, TSNode n, const char *source, const char **out, int out_cap, int *count) { if (*count >= out_cap) { return; } char *t = cbm_node_text(a, n, source); if (!t) { return; } char *angle = strchr(t, '<'); if (angle) { *angle = '\0'; } /* PHP qualified base may be backslash-prefixed (e.g. `\RuntimeException`); * keep the bare class name so it matches the unqualified declaration. */ char *last_bs = strrchr(t, '\\'); if (last_bs) { t = last_bs + 1; } if (t[0]) { out[(*count)++] = t; } } /* TypeScript/TSX: bases live in a `class_heritage` (class) or directly in an * `extends_type_clause` (interface). The extractor previously captured the * literal "extends"/"implements" keyword text instead of the type names. */ static int collect_ts_bases(CBMArena *a, TSNode clause, const char *source, const char **out, int out_cap, int *count) { const char *kk = ts_node_type(clause); if (strcmp(kk, "extends_clause") == 0) { /* `extends_clause` carries the superclass in its `value` field. */ TSNode v = ts_node_child_by_field_name(clause, TS_FIELD("value")); if (!ts_node_is_null(v)) { push_base_text(a, v, source, out, out_cap, count); } return *count; } if (strcmp(kk, "implements_clause") == 0 || strcmp(kk, "extends_type_clause") == 0) { /* Named children are the implemented/extended types (possibly generic). */ uint32_t nc = ts_node_named_child_count(clause); for (uint32_t i = 0; i < nc && *count < out_cap; i++) { TSNode c = ts_node_named_child(clause, i); const char *ck = ts_node_type(c); if (strcmp(ck, "type_arguments") == 0) { continue; } if (strcmp(ck, "generic_type") == 0) { TSNode nm = ts_node_child_by_field_name(c, TS_FIELD("name")); if (!ts_node_is_null(nm)) { push_base_text(a, nm, source, out, out_cap, count); continue; } } push_base_text(a, c, source, out, out_cap, count); } } return *count; } /* TypeScript: walk the class_heritage container (which holds extends_clause + * implements_clause), or handle a bare interface extends_type_clause. */ static const char **extract_ts_bases(CBMArena *a, TSNode node, const char *source) { const char *bases[MAX_BASES]; int count = 0; 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, "class_heritage") == 0) { uint32_t hc = ts_node_child_count(child); for (uint32_t j = 0; j < hc; j++) { collect_ts_bases(a, ts_node_child(child, j), source, bases, MAX_BASES_MINUS_1, &count); } } else if (strcmp(ck, "extends_type_clause") == 0) { collect_ts_bases(a, child, source, bases, MAX_BASES_MINUS_1, &count); } } if (count == 0) { return NULL; } const char **result = (const char **)cbm_arena_alloc(a, (size_t)(count + NULL_TERM) * sizeof(const char *)); if (!result) { return NULL; } for (int i = 0; i < count; i++) { result[i] = bases[i]; } result[count] = NULL; return result; } /* PHP: bases live in `base_clause` (extends) and `class_interface_clause` * (implements) child nodes; named children are `name`/`qualified_name`. */ static const char **extract_php_bases(CBMArena *a, TSNode node, const char *source) { const char *bases[MAX_BASES]; int count = 0; 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, "base_clause") != 0 && strcmp(ck, "class_interface_clause") != 0) { continue; } uint32_t cc = ts_node_named_child_count(child); for (uint32_t j = 0; j < cc && count < MAX_BASES_MINUS_1; j++) { push_base_text(a, ts_node_named_child(child, j), source, bases, MAX_BASES_MINUS_1, &count); } } if (count == 0) { return NULL; } const char **result = (const char **)cbm_arena_alloc(a, (size_t)(count + NULL_TERM) * sizeof(const char *)); if (!result) { return NULL; } for (int i = 0; i < count; i++) { result[i] = bases[i]; } result[count] = NULL; return result; } /* Kotlin: supertypes live in `delegation_specifier` children. Each holds * either a bare `user_type` (interface) or a `constructor_invocation` whose * `user_type` is the superclass. Descend to the `type_identifier`. */ static const char **extract_kotlin_bases(CBMArena *a, TSNode node, const char *source) { const char *bases[MAX_BASES]; int count = 0; uint32_t nc = ts_node_child_count(node); for (uint32_t i = 0; i < nc && count < MAX_BASES_MINUS_1; i++) { TSNode child = ts_node_child(node, i); if (strcmp(ts_node_type(child), "delegation_specifier") != 0) { continue; } /* Find the user_type (directly or under a constructor_invocation). */ TSNode ut = ts_node_named_child(child, 0); if (!ts_node_is_null(ut) && strcmp(ts_node_type(ut), "constructor_invocation") == 0) { ut = ts_node_named_child(ut, 0); } if (ts_node_is_null(ut)) { continue; } /* user_type → type_identifier (first child); strip generic args. */ TSNode ti = ut; if (strcmp(ts_node_type(ut), "user_type") == 0 && ts_node_named_child_count(ut) > 0) { ti = ts_node_named_child(ut, 0); } push_base_text(a, ti, source, bases, MAX_BASES_MINUS_1, &count); } if (count == 0) { return NULL; } const char **result = (const char **)cbm_arena_alloc(a, (size_t)(count + NULL_TERM) * sizeof(const char *)); if (!result) { return NULL; } for (int i = 0; i < count; i++) { result[i] = bases[i]; } result[count] = NULL; return result; } // Walk a field node and collect type identifier names into out[]. // Handles: direct type_identifier/generic_type/qualified_name, type_list children // (Java interfaces list), and raw text fallback (other languages). static int collect_bases_from_field(CBMArena *a, TSNode field_node, const char *source, const char **out, int out_cap) { int count = 0; const char *fk = ts_node_type(field_node); // If the field node itself is a type node, extract directly. if (strcmp(fk, "type_identifier") == 0 || strcmp(fk, "generic_type") == 0 || strcmp(fk, "qualified_name") == 0 || strcmp(fk, "scoped_type_identifier") == 0 || strcmp(fk, "user_type") == 0) { char *t = cbm_node_text(a, field_node, source); if (t) { char *angle = strchr(t, '<'); if (angle) { *angle = '\0'; } if (t[0] && count < out_cap) { out[count++] = t; } } return count; } // Walk named children: look for type identifiers or type_list/interface_type_list. uint32_t nc = ts_node_named_child_count(field_node); for (uint32_t i = 0; i < nc && count < out_cap; i++) { TSNode child = ts_node_named_child(field_node, i); const char *ck = ts_node_type(child); if (strcmp(ck, "type_identifier") == 0 || strcmp(ck, "generic_type") == 0 || strcmp(ck, "qualified_name") == 0 || strcmp(ck, "scoped_type_identifier") == 0 || strcmp(ck, "user_type") == 0 || /* Python `class C(Base)` carries the base as a bare `identifier` * inside the `superclasses` argument_list (and `attribute` for a * dotted base like `mod.Base`). Without these the raw-text fallback * below captured the whole "(Base)" field text, which never * resolved -> zero INHERITS edges for Python subclasses. */ strcmp(ck, "identifier") == 0 || strcmp(ck, "attribute") == 0) { char *t = cbm_node_text(a, child, source); if (t) { char *angle = strchr(t, '<'); if (angle) { *angle = '\0'; } if (t[0]) { out[count++] = t; } } } else if (strcmp(ck, "subscript") == 0) { /* Python parameterized base, e.g. `class S(Generic[T])`: the base * type is the `value` field of the subscript; the bracketed type * args must not leak into the stored name. */ TSNode val = ts_node_child_by_field_name(child, TS_FIELD("value")); if (ts_node_is_null(val) && ts_node_named_child_count(child) > 0) { val = ts_node_named_child(child, 0); } if (!ts_node_is_null(val)) { char *t = cbm_node_text(a, val, source); if (t && t[0]) { out[count++] = t; } } } else if (strcmp(ck, "type_list") == 0 || strcmp(ck, "interface_type_list") == 0) { // Java: super_interfaces contains type_list with multiple type_identifiers. uint32_t tlnc = ts_node_named_child_count(child); for (uint32_t ti = 0; ti < tlnc && count < out_cap; ti++) { TSNode tl_child = ts_node_named_child(child, ti); const char *tlk = ts_node_type(tl_child); if (strcmp(tlk, "type_identifier") == 0 || strcmp(tlk, "generic_type") == 0 || strcmp(tlk, "qualified_name") == 0) { char *t = cbm_node_text(a, tl_child, source); if (t) { char *angle = strchr(t, '<'); if (angle) { *angle = '\0'; } if (t[0]) { out[count++] = t; } } } } } } // Fallback: raw node text (for languages where the field node is the type name directly). if (count == 0) { char *t = cbm_node_text(a, field_node, source); if (t && t[0] && count < out_cap) { out[count++] = t; } } return count; } // Extract base class names from a class node. // Julia: a subtype declaration `Foo <: Bar` is a `binary_expression` (operator // `<:`) inside the def's `type_head`. The base is the RHS identifier. static const char **extract_julia_base_classes(CBMArena *a, TSNode node, const char *source) { TSNode th = cbm_find_child_by_kind(node, "type_head"); if (ts_node_is_null(th)) { return NULL; } TSNode inner = ts_node_named_child_count(th) > 0 ? ts_node_named_child(th, 0) : th; if (ts_node_is_null(inner) || strcmp(ts_node_type(inner), "binary_expression") != 0 || ts_node_named_child_count(inner) < 2) { return NULL; } TSNode base = ts_node_named_child(inner, ts_node_named_child_count(inner) - 1); char *bname = cbm_node_text(a, base, source); if (!bname || !bname[0]) { return NULL; } const char **result = (const char **)cbm_arena_alloc(a, 2 * sizeof(const char *)); if (!result) { return NULL; } result[0] = bname; result[1] = NULL; return result; } static const char **extract_base_classes(CBMArena *a, TSNode node, const char *source, CBMLanguage lang) { // Languages whose heritage is not exposed via a tree-sitter field need // dedicated walkers; the generic field/keyword path mis-captures them. if (lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX) { const char **ts_result = extract_ts_bases(a, node, source); if (ts_result) { return ts_result; } } if (lang == CBM_LANG_PHP) { const char **php_result = extract_php_bases(a, node, source); if (php_result) { return php_result; } } if (lang == CBM_LANG_KOTLIN) { const char **kt_result = extract_kotlin_bases(a, node, source); if (kt_result) { return kt_result; } } // Squirrel: `class Dog extends Animal` — heritage is an `identifier` child // directly following the `extends` keyword (no field). Grab it. if (lang == CBM_LANG_SQUIRREL) { uint32_t sc = ts_node_child_count(node); bool seen_extends = false; for (uint32_t i = 0; i < sc; i++) { TSNode child = ts_node_child(node, i); const char *ck = ts_node_type(child); if (strcmp(ck, "extends") == 0) { seen_extends = true; continue; } if (seen_extends && strcmp(ck, "identifier") == 0) { char *base = cbm_node_text(a, child, source); if (base && base[0]) { const char **result = (const char **)cbm_arena_alloc(a, 2 * sizeof(const char *)); if (result) { result[0] = base; result[1] = NULL; return result; } } break; } } } if (lang == CBM_LANG_JULIA) { const char **jb = extract_julia_base_classes(a, node, source); if (jb) { return jb; } } /* F#: `inherit Base(...)` appears as a `class_inherits_decl` descendant of * the type_definition; the base type is the `simple_type` it carries. */ if (lang == CBM_LANG_FSHARP) { TSNode inh = find_first_descendant_by_kind(node, "class_inherits_decl", CBM_DESCENDANT_MAX_DEPTH); if (!ts_node_is_null(inh)) { TSNode st = cbm_find_child_by_kind(inh, "simple_type"); char *bn = ts_node_is_null(st) ? NULL : cbm_node_text(a, st, source); if (bn && bn[0]) { const char **result = (const char **)cbm_arena_alloc(a, 2 * sizeof(const char *)); if (result) { result[0] = bn; result[1] = NULL; return result; } } } } /* D: `class Dog : Animal, IFoo` — class_declaration lists one `base_class` * child per base, each wrapping an identifier/qualified name. */ if (lang == CBM_LANG_DLANG) { const char *pbases[MAX_BASES]; int pc = 0; uint32_t nc = ts_node_child_count(node); for (uint32_t i = 0; i < nc && pc < MAX_BASES_MINUS_1; i++) { TSNode c = ts_node_child(node, i); if (strcmp(ts_node_type(c), "base_class") != 0) { continue; } char *bn = cbm_node_text(a, c, source); if (bn && bn[0]) { pbases[pc++] = bn; } } if (pc > 0) { const char **result = (const char **)cbm_arena_alloc(a, (pc + NULL_TERM) * sizeof(const char *)); if (result) { for (int i = 0; i < pc; i++) { result[i] = pbases[i]; } result[pc] = NULL; return result; } } } /* PowerShell: `class Dog : Animal` — class_statement lists `simple_name` * children with a `:` token separating the class name from the base name(s). * Collect every simple_name that appears AFTER the first `:` token. */ if (lang == CBM_LANG_POWERSHELL && strcmp(ts_node_type(node), "class_statement") == 0) { const char *pbases[MAX_BASES]; int pc = 0; bool seen_colon = false; uint32_t nc = ts_node_child_count(node); for (uint32_t i = 0; i < nc && pc < MAX_BASES_MINUS_1; i++) { TSNode c = ts_node_child(node, i); const char *ck = ts_node_type(c); if (strcmp(ck, ":") == 0) { seen_colon = true; continue; } if (strcmp(ck, "{") == 0) { break; /* class body begins; base list is done */ } if (seen_colon && strcmp(ck, "simple_name") == 0) { char *bn = cbm_node_text(a, c, source); if (bn && bn[0]) { pbases[pc++] = bn; } } } if (pc > 0) { const char **result = (const char **)cbm_arena_alloc(a, (pc + NULL_TERM) * sizeof(const char *)); if (result) { for (int i = 0; i < pc; i++) { result[i] = pbases[i]; } result[pc] = NULL; return result; } } } /* Pascal: declClass carries one or more `parent` fields, each a `typeref` * (`= class(TBase, IFoo)`). Collect all parent typeref identifiers. */ if (lang == CBM_LANG_PASCAL && strcmp(ts_node_type(node), "declClass") == 0) { const char *pbases[MAX_BASES]; int pc = 0; uint32_t nc = ts_node_child_count(node); for (uint32_t i = 0; i < nc && pc < MAX_BASES_MINUS_1; i++) { const char *fn = ts_node_field_name_for_child(node, i); if (!fn || strcmp(fn, "parent") != 0) { continue; } TSNode pn = ts_node_child(node, i); if (!ts_node_is_named(pn)) { continue; /* the '(' / ')' delimiters are also tagged `parent` */ } char *bn = cbm_node_text(a, pn, source); if (bn && bn[0]) { pbases[pc++] = bn; } } if (pc > 0) { const char **result = (const char **)cbm_arena_alloc(a, (pc + NULL_TERM) * sizeof(const char *)); if (result) { for (int i = 0; i < pc; i++) { result[i] = pbases[i]; } result[pc] = NULL; return result; } } } static const char *fields[] = {"superclass", "superclasses", "superinterfaces", "interfaces", "bases", "type_inheritance_clause", "delegation_specifiers", NULL}; // Collect all bases from all matching fields (fixes early-return bug and keyword-text bug). const char *bases[MAX_BASES]; int base_count = 0; 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)) { base_count += collect_bases_from_field(a, super, source, bases + base_count, MAX_BASES_MINUS_1 - base_count); } } // Some grammars expose heritage as a named child rather than a field, e.g. // Java `interface X extends A, B` → `extends_interfaces` (holds a type_list). // Without this the interface's bases were never captured. static const char *heritage_children[] = {"extends_interfaces", "super_interfaces", NULL}; uint32_t top_count = ts_node_child_count(node); for (uint32_t i = 0; i < top_count && base_count < MAX_BASES_MINUS_1; i++) { TSNode child = ts_node_child(node, i); const char *ck = ts_node_type(child); for (const char **h = heritage_children; *h; h++) { if (strcmp(ck, *h) == 0) { base_count += collect_bases_from_field(a, child, source, bases + base_count, MAX_BASES_MINUS_1 - base_count); } } } if (base_count > 0) { const char **result = (const char **)cbm_arena_alloc(a, (base_count + NULL_TERM) * sizeof(const char *)); if (result) { for (int i = 0; i < base_count; i++) { result[i] = bases[i]; } 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; } } } // C#: explicit base_list handler { const char **csharp_result = extract_csharp_base_list(a, node, source, count); if (csharp_result) { return csharp_result; } } // Fallback: search for common base class node types as children static const char *base_types[] = {"superclass", "superinterfaces", "type_inheritance_clause", "class_heritage", "delegation_specifiers", "super_interfaces", "extends_clause", "implements_clause", "argument_list", "inheritance_specifier", NULL}; return find_base_from_children(a, node, source, base_types); } // Classify class label from AST node kind static const char *class_label_for_kind(const char *kind) { if (strcmp(kind, "interface_declaration") == 0 || strcmp(kind, "interface_type") == 0 || strcmp(kind, "trait_item") == 0 || strcmp(kind, "trait_definition") == 0 || strcmp(kind, "protocol_declaration") == 0) { return "Interface"; } if (strcmp(kind, "enum_specifier") == 0 || strcmp(kind, "enum_declaration") == 0 || strcmp(kind, "enum_item") == 0) { return "Enum"; } if (strcmp(kind, "type_alias_declaration") == 0 || strcmp(kind, "type_item") == 0 || strcmp(kind, "type_alias") == 0 || strcmp(kind, "type_definition") == 0) { return "Type"; } return "Class"; } // --- Parameter type extraction --- // Builtin types we skip (not useful as USES_TYPE targets). static bool is_builtin_type(const char *name) { static const char *builtins[] = { "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "float", "float32", "float64", "double", "string", "str", "bool", "boolean", "byte", "rune", "void", "None", "any", "interface", "object", "Object", "error", "uintptr", "complex64", "complex128", "number", "bigint", "symbol", "undefined", "null", "char", "short", "long", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64", "usize", "isize", "self", "Self", "cls", "type", "Int", "Int8", "Int16", "Int32", "Int64", "UInt", "UInt8", "UInt16", "UInt32", "UInt64", "Float", "Double", "String", "Bool", "Boolean", "Byte", "Short", "Long", "Char", "Unit", "Void", "Any", "Nothing", "Dynamic", NULL}; for (const char **b = builtins; *b; b++) { if (strcmp(name, *b) == 0) { return true; } } return false; } // Clean a type name: strip *, &, [], ..., generics static char *clean_type_name(CBMArena *a, const char *raw) { if (!raw || !raw[0]) { return NULL; } const char *s = raw; // Skip leading whitespace, ":", "*", "&", "[]", "..." while (*s == ' ' || *s == '\t' || *s == ':' || *s == '*' || *s == '&' || *s == '[' || *s == ']' || *s == '.') { s++; } if (!*s) { return NULL; } // Find end: stop at <, [, or whitespace size_t len = 0; while (s[len] && s[len] != '<' && s[len] != '[' && s[len] != ' ') { len++; } if (len == 0) { return NULL; } char *result = cbm_arena_alloc(a, len + NULL_TERM); memcpy(result, s, len); result[len] = '\0'; return result; } // Extract param_names from a parameter list node. // Returns NULL-terminated arena-allocated array. // 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, TS_FIELD("name")); 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, TS_FIELD("name")); if (ts_node_is_null(nm)) { nm = ts_node_child_by_field_name(param, TS_FIELD("pattern")); } 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; if (ts_node_is_null(params)) { return NULL; } const char *names[MAX_PARAMS]; int count = 0; uint32_t nc = ts_node_child_count(params); for (uint32_t i = 0; i < nc && count < MAX_PARAMS_MINUS_1; i++) { TSNode param = ts_node_child(params, i); if (ts_node_is_null(param) || !ts_node_is_named(param)) { continue; } char *name_text = resolve_param_name(a, param, source); if (name_text && name_text[0]) { names[count++] = name_text; } } if (count == 0) { return NULL; } const char **result = (const char **)cbm_arena_alloc(a, (count + NULL_TERM) * sizeof(const char *)); for (int i = 0; i < count; i++) { result[i] = names[i]; } result[count] = NULL; return result; } // 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. // 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, TS_FIELD("type")); 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 + NULL_TERM) * 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; if (ts_node_is_null(rt_node)) { return NULL; } const char *types[MAX_RETURN_TYPES]; int count = 0; if (strcmp(ts_node_type(rt_node), "parameter_list") == 0) { extract_go_multi_return(a, rt_node, source, types, &count); } else { add_cleaned_type(a, types, &count, cbm_node_text(a, rt_node, source)); } return build_type_array(a, types, count); } // Extract param_types from a parameter list node. // Returns NULL-terminated arena-allocated array. // 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, TS_FIELD("type")); 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, TS_FIELD("type")); 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, TS_FIELD("type")); 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, TS_FIELD("type")); 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; } const char *types[MAX_PARAMS]; int count = 0; uint32_t nc = ts_node_child_count(params); for (uint32_t i = 0; i < nc && count < MAX_PARAMS_MINUS_1; i++) { TSNode param = ts_node_child(params, i); if (ts_node_is_null(param) || !ts_node_is_named(param)) { continue; } add_dedup_type(a, types, &count, resolve_param_type_text(a, param, source, lang)); } if (count == 0) { return NULL; } const char **result = (const char **)cbm_arena_alloc(a, (count + NULL_TERM) * sizeof(const char *)); for (int i = 0; i < count; i++) { result[i] = types[i]; } result[count] = NULL; return result; } // --- Function definition extraction --- // For C++/CUDA template_declaration, find the inner function_definition or declaration. static TSNode unwrap_template_inner(TSNode node, CBMLanguage lang) { TSNode inner = find_cpp_template_inner_node(node, lang); if (!ts_node_is_null(inner)) { return inner; } 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, TS_FIELD("declarator")); for (int d = 0; d < C_RETURN_WALK_DEPTH && !ts_node_is_null(decl); d++) { TSNode params = ts_node_child_by_field_name(decl, TS_FIELD("parameters")); if (!ts_node_is_null(params)) { return params; } decl = ts_node_child_by_field_name(decl, TS_FIELD("declarator")); } 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, TS_FIELD("declarator")); 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, RT_PAIR_SIZE * sizeof(const char *)); if (rt) { rt[0] = def->return_type; rt[SKIP_CHAR] = NULL; def->return_types = rt; } } break; } } } /* Compute and store the structural complexity metrics for a definition. */ static void set_def_complexity(CBMDefinition *def, TSNode body, const CBMLangSpec *spec) { cbm_complexity_t cx; cbm_compute_complexity(body, spec->branching_node_types, &cx); def->complexity = cx.cyclomatic; def->cognitive = cx.cognitive; def->loop_count = cx.loop_count; def->loop_depth = cx.loop_depth; def->max_access_depth = cx.max_access_depth; } /* Extract the bare type name from a Go method receiver node. * The receiver is a parameter_list, e.g. "(s *OrderService)" or "(s Order)". * Walks to the parameter_declaration's `type` field, unwrapping pointer_type * and generic_type, and returns the type_identifier text (e.g. "OrderService"). * Returns NULL if no type_identifier is found. */ static char *go_receiver_type_name(CBMArena *a, TSNode recv, const char *source) { uint32_t nc = ts_node_child_count(recv); for (uint32_t i = 0; i < nc; i++) { TSNode child = ts_node_child(recv, i); if (strcmp(ts_node_type(child), "parameter_declaration") != 0) { continue; } TSNode tn = ts_node_child_by_field_name(child, TS_FIELD("type")); if (ts_node_is_null(tn)) { continue; } /* Unwrap pointer_type / generic_type down to the type_identifier. */ for (int guard = 0; guard < 4 && !ts_node_is_null(tn); guard++) { const char *tk = ts_node_type(tn); if (strcmp(tk, "type_identifier") == 0) { return cbm_node_text(a, tn, source); } if (strcmp(tk, "pointer_type") == 0 || strcmp(tk, "generic_type") == 0) { /* pointer_type: child is the pointee type; generic_type has a * `type` field for the base type_identifier. */ TSNode inner = ts_node_child_by_field_name(tn, TS_FIELD("type")); if (ts_node_is_null(inner)) { inner = ts_node_named_child(tn, 0); } tn = inner; continue; } break; } } return NULL; } static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) { CBMArena *a = ctx->arena; TSNode name_node = cbm_resolve_func_name(node, ctx->language); if (ts_node_is_null(name_node)) { return; } char *name = cbm_func_name_node_text(a, name_node, ctx->source); if (!name || !name[0] || strcmp(name, "function") == 0) { return; } // Makefile special targets (.PHONY, .DEFAULT, .SUFFIXES, …) are directives, // not build-rule defs. Their leading '.' would also make cbm_fqn_compute // emit a "..PHONY" segment (a "double dot") and thus a malformed QN. Skip // any dot-prefixed Make target. if (ctx->language == CBM_LANG_MAKEFILE && name[0] == '.') { return; } TSNode func_node = unwrap_template_inner(node, ctx->language); CBMDefinition def; memset(&def, 0, sizeof(def)); def.name = name; /* Java/Go derive the module from the containing directory (package), so the * filename stem is NOT baked into the QN (Go func in myapp/db/conn.go -> * proj.myapp.db.Func, not proj.myapp.db.conn.Func). Other langs unchanged. */ def.qualified_name = cbm_fqn_compute_source_lang(a, ctx->project, ctx->rel_path, name, ctx->language); /* A free function declared inside a namespace (C++/C#/PHP) is qualified by * the namespace scope the def walk carries (enclosing_class_qn was extended * by is_namespace_scope_kind), so `ns::serialize` is `proj.file.ns.serialize` * — without this it collapses to the file scope and namespace-aware * resolution (ADL, namespace-function lookup) can never see it. Class methods * never reach here (they go through extract_class_methods), so a set * enclosing scope here is always a namespace. The out-of-line method path * below overrides this for `Ns::Cls::method` definitions. */ if (ctx->enclosing_class_qn && (ctx->language == CBM_LANG_CPP || ctx->language == CBM_LANG_CUDA)) { def.qualified_name = cbm_arena_sprintf(a, "%s.%s", ctx->enclosing_class_qn, name); } def.label = "Function"; def.file_path = ctx->rel_path; def.start_line = ts_node_start_point(node).row + TS_LINE_OFFSET; def.end_line = ts_node_end_point(node).row + TS_LINE_OFFSET; def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET); def.is_exported = cbm_is_exported(name, ctx->language); // Parameters — use func_node (inner function for templates) TSNode params = ts_node_child_by_field_name(func_node, TS_FIELD("parameters")); 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)) { params = find_c_params(func_node); } if (!ts_node_is_null(params)) { def.signature = cbm_node_text(a, params, ctx->source); def.param_names = extract_param_names(a, params, ctx->source, ctx->language); def.param_types = extract_param_types(a, params, ctx->source, ctx->language); } // Return type — use func_node (inner function for templates) static const char *rt_fields[] = {"result", "return_type", "type", NULL}; for (const char **f = rt_fields; *f; f++) { TSNode rt = ts_node_child_by_field_name(func_node, *f, (uint32_t)strlen(*f)); if (!ts_node_is_null(rt)) { def.return_type = cbm_node_text(a, rt, ctx->source); def.return_types = extract_return_types(a, rt, ctx->source, ctx->language); break; } } // 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)) { resolve_cpp_trailing_return(a, func_node, ctx->source, &def); } // Receiver (Go methods) TSNode recv = ts_node_child_by_field_name(node, TS_FIELD("receiver")); if (!ts_node_is_null(recv)) { def.receiver = cbm_node_text(a, recv, ctx->source); def.label = "Method"; /* Derive parent_class from the receiver type so DEFINES_METHOD edges * (and downstream Go IMPLEMENTS/OVERRIDE) link the method to its owning * struct/type node. The parent QN must match the type's node QN, which * is computed the same way (cbm_fqn_compute on the type name). */ char *recv_type = go_receiver_type_name(a, recv, ctx->source); if (recv_type && recv_type[0]) { /* Must match the Go type node QN (directory-based module) so the * DEFINES_METHOD edge links the method to its owning type. */ def.parent_class = cbm_fqn_compute_source_lang(a, ctx->project, ctx->rel_path, recv_type, ctx->language); } } // C++/CUDA: out-of-line method definition (`Foo::bar` in a .cc/.cpp). The // class body in the header is declaration-only, so without this the // definition is recorded as a free Function. Promote it to a Method whose QN // is scoped to its class and whose parent_class links it back (matching the // class node QN computed the same way) so DEFINES_METHOD edges resolve. if ((ctx->language == CBM_LANG_CPP || ctx->language == CBM_LANG_CUDA) && strcmp(ts_node_type(node), "function_definition") == 0) { char *scope_name = cbm_cpp_out_of_line_parent_class(a, node, ctx->source); if (scope_name && scope_name[0]) { const char *class_qn = cbm_fqn_compute(a, ctx->project, ctx->rel_path, scope_name); def.qualified_name = cbm_arena_sprintf(a, "%s.%s", class_qn, name); def.label = "Method"; def.parent_class = class_qn; } } // Pony: fun/be/new (method/constructor/ffi_method) live in pony_func_types, // so the main def-walk extracts them here as "Function"; but one declared // inside a class/actor/struct/trait/interface/primitive IS a method. Detect // the enclosing class-like ancestor and promote it to "Method" with a // parent_class link (the class name is the first identifier child — no field). if (ctx->language == CBM_LANG_PONY && def.label && strcmp(def.label, "Function") == 0 && spec->class_node_types) { for (TSNode cur = ts_node_parent(node); !ts_node_is_null(cur); cur = ts_node_parent(cur)) { if (cbm_kind_in_set(cur, spec->class_node_types)) { def.label = "Method"; TSNode cn = cbm_find_child_by_kind(cur, "identifier"); if (!ts_node_is_null(cn)) { char *cname = cbm_node_text(a, cn, ctx->source); if (cname && cname[0]) { def.parent_class = cbm_fqn_compute(a, ctx->project, ctx->rel_path, cname); } } break; } } } // Decorators + route extraction from decorator AST def.decorators = extract_decorators(a, node, ctx->source, ctx->language, spec); extract_route_from_decorators(a, node, ctx->source, spec, &def.route_path, &def.route_method); // Rust: disambiguate cfg-gated twin functions by folding the #[cfg(...)] // predicate into the QN so both branches survive the graph upsert (#495). if (ctx->language == CBM_LANG_RUST) { def.qualified_name = rust_cfg_qualified_name(a, def.qualified_name, def.decorators); def.is_test = rust_def_is_test(def.decorators); } // Docstring def.docstring = extract_docstring(a, node, ctx->source, ctx->language); // Complexity if (spec->branching_node_types && spec->branching_node_types[0]) { set_def_complexity(&def, node, spec); } // MinHash fingerprint compute_fingerprint(ctx, &def, func_node); // JS/TS export detection if (ctx->language == CBM_LANG_JAVASCRIPT || ctx->language == CBM_LANG_TYPESCRIPT || ctx->language == CBM_LANG_TSX) { if (is_js_exported(node)) { def.is_entry_point = true; } } // main is always an entry point if (strcmp(name, "main") == 0) { def.is_entry_point = true; } cbm_defs_push(&ctx->result->defs, a, def); } // --- Class definition extraction --- // Push a simple class definition (used by config language extractors). // Replace each run of whitespace in `name` with a single '-' so the value is a // well-formed QN segment. Markdown headings (e.g. "Codebase Memory") legitimately // contain spaces; embedding them verbatim in a QN makes it malformed. Returns the // original pointer when there is no whitespace to collapse. The human-readable // def.name is kept intact; only the QN segment is slugified. static const char *qn_safe_segment(CBMArena *a, const char *name) { if (!name) { return name; } bool has_ws = false; for (const char *p = name; *p; p++) { if (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') { has_ws = true; break; } } if (!has_ws) { return name; } char *out = cbm_arena_strdup(a, name); if (!out) { return name; } char *w = out; bool in_ws = false; for (char *r = out; *r; r++) { if (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') { if (!in_ws && w != out) { *w++ = '-'; } in_ws = true; } else { *w++ = *r; in_ws = false; } } *w = '\0'; return out; } 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, qn_safe_segment(a, name)); def.label = label; def.file_path = ctx->rel_path; def.start_line = ts_node_start_point(node).row + TS_LINE_OFFSET; def.end_line = ts_node_end_point(node).row + TS_LINE_OFFSET; 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 - SKIP_CHAR] == '\n' || name[len - SKIP_CHAR] == '\r' || name[len - SKIP_CHAR] == ' ')) { name[len - SKIP_CHAR] = '\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++) { TSNode child = ts_node_child(node, i); if (strcmp(ts_node_type(child), "section_name") != 0) { continue; } // The section_name node spans the whole header line including the // surrounding brackets and the trailing newline (e.g. "[database]\n"), // which would put '[' / ']' and a '\n' into the QN (malformed). Its // inner `text` child holds the bare name ("database"). TSNode text = cbm_find_child_by_kind(child, "text"); if (!ts_node_is_null(text)) { return cbm_node_text(a, text, source); } return cbm_node_text(a, child, 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 NULL; } char *name = cbm_node_text(a, id, source); if (!name || !name[0]) { return NULL; } // Append the block's quoted labels so each block gets a distinct, // query-friendly name: resource "aws_instance" "web" -> resource.aws_instance.web // rather than every resource collapsing to the bare keyword "resource" (#337). // HCL stores labels as string_lit -> template_literal children. uint32_t cc = ts_node_named_child_count(node); for (uint32_t i = 0; i < cc; i++) { TSNode ch = ts_node_named_child(node, i); if (strcmp(ts_node_type(ch), "string_lit") != 0) { continue; } TSNode lit = cbm_find_child_by_kind(ch, "template_literal"); if (ts_node_is_null(lit)) { continue; } char *label = cbm_node_text(a, lit, source); if (!label || !label[0]) { continue; } char *joined = cbm_arena_sprintf(a, "%s.%s", name, label); if (joined) { name = joined; } } return name; } // 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); // A heading is a Section (a valid label), not a Class — keep the accurate // label rather than degrade it to match a test. The markdown repro asserts // "Class"; that assertion is the inaccurate side and is flagged for review. 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); if (extract_config_class_def(ctx, node, kind)) { return; } TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name")); // ObjC: class name is first identifier child if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_OBJC) { name_node = cbm_find_child_by_kind(node, "identifier"); } // Swift and newer tree-sitter-kotlin: class/object name is a type_identifier // child (no "name" field). if (ts_node_is_null(name_node) && (ctx->language == CBM_LANG_SWIFT || ctx->language == CBM_LANG_KOTLIN)) { name_node = cbm_find_child_by_kind(node, "type_identifier"); } // Protobuf: service_name / message_name / enum_name children if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_PROTOBUF) { name_node = cbm_find_child_by_kind(node, "service_name"); if (ts_node_is_null(name_node)) { name_node = cbm_find_child_by_kind(node, "message_name"); } if (ts_node_is_null(name_node)) { name_node = cbm_find_child_by_kind(node, "enum_name"); } } // Thrift / Smithy / Pony / PKL (no `name` field): class-type defs carry the // name on a plain `identifier` child (PKL `clazz` -> `(identifier) (classBody)`). if (ts_node_is_null(name_node) && (ctx->language == CBM_LANG_THRIFT || ctx->language == CBM_LANG_SMITHY || ctx->language == CBM_LANG_PONY || ctx->language == CBM_LANG_PKL)) { name_node = cbm_find_child_by_kind(node, "identifier"); } // F#: type_definition wraps an `anon_type_defn` (or similar) whose // `type_name` child carries the type name on its own `type_name` field. if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_FSHARP) { TSNode tn = find_first_descendant_by_kind(node, "type_name", CBM_DESCENDANT_MAX_DEPTH); if (!ts_node_is_null(tn)) { TSNode id = ts_node_child_by_field_name(tn, "type_name", 9); if (ts_node_is_null(id)) { id = cbm_find_child_by_kind(tn, "identifier"); } if (!ts_node_is_null(id)) { name_node = id; } } } // D: class/struct/interface/union/enum _declaration nodes carry the name on // a plain `identifier` child (no `name` field). if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_DLANG) { name_node = cbm_find_child_by_kind(node, "identifier"); } // PowerShell: class_statement / enum_statement have no `name` field; the // name is the FIRST `simple_name` child (`class Dog : Animal { ... }`). if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_POWERSHELL) { name_node = cbm_find_child_by_kind(node, "simple_name"); } // Pascal: a class/interface body (declClass/declIntf) has no name of its // own; the type name is on the enclosing `declType`'s `name` field // (`TFoo = class ... end`). if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_PASCAL) { TSNode parent = ts_node_parent(node); if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "declType") == 0) { name_node = ts_node_child_by_field_name(parent, TS_FIELD("name")); } } // Julia (no `name` field): struct_definition / abstract_definition carry a // `type_head` child whose name is either a plain identifier (`struct Foo`) // or the LHS of a `<:` binary_expression (`struct Foo <: Bar`). if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_JULIA) { TSNode th = cbm_find_child_by_kind(node, "type_head"); if (!ts_node_is_null(th)) { TSNode inner = ts_node_named_child_count(th) > 0 ? ts_node_named_child(th, 0) : th; if (!ts_node_is_null(inner) && strcmp(ts_node_type(inner), "binary_expression") == 0 && ts_node_named_child_count(inner) > 0) { name_node = ts_node_named_child(inner, 0); /* LHS of `<:` */ } else if (!ts_node_is_null(inner) && strcmp(ts_node_type(inner), "identifier") == 0) { name_node = inner; } else { name_node = cbm_find_child_by_kind(th, "identifier"); } } } // Cap'n Proto (FIELD_COUNT 0): struct/interface name is type_identifier, enum // name is enum_identifier (aliased identifier children). if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_CAPNP) { if (strcmp(kind, "enum") == 0) { name_node = cbm_find_child_by_kind(node, "enum_identifier"); } else { name_node = cbm_find_child_by_kind(node, "type_identifier"); } } // Agda (FIELD_COUNT 0): data > data_name, record > record_name (direct child). if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_AGDA) { if (strcmp(kind, "data") == 0) { name_node = cbm_find_child_by_kind(node, "data_name"); } else if (strcmp(kind, "record") == 0) { name_node = cbm_find_child_by_kind(node, "record_name"); } } // GraphQL / Prisma (FIELD_COUNT 0): the type name is a plain direct `name` // (GraphQL) or `identifier` (Prisma) child of the type-definition node. if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_GRAPHQL) { name_node = cbm_find_child_by_kind(node, "name"); } if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_PRISMA) { name_node = cbm_find_child_by_kind(node, "identifier"); } // Puppet: class_definition / type_declaration name is a plain identifier or // class_identifier child (no `name` field). if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_PUPPET) { name_node = cbm_find_child_by_kind(node, "identifier"); if (ts_node_is_null(name_node)) { name_node = cbm_find_child_by_kind(node, "class_identifier"); } } // Smali (no `name` field): class_definition > class_directive > class_identifier. if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_SMALI) { TSNode dir = cbm_find_child_by_kind(node, "class_directive"); if (!ts_node_is_null(dir)) { name_node = cbm_find_child_by_kind(dir, "class_identifier"); } } // VHDL: name lives on a declaration-keyword-named field whose value is an // `identifier`. Map node kind -> field name. if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_VHDL) { if (strcmp(kind, "entity_declaration") == 0) { name_node = ts_node_child_by_field_name(node, TS_FIELD("entity")); } else if (strcmp(kind, "architecture_definition") == 0) { name_node = ts_node_child_by_field_name(node, TS_FIELD("architecture")); } else if (strcmp(kind, "package_declaration") == 0) { name_node = ts_node_child_by_field_name(node, TS_FIELD("package")); } else if (strcmp(kind, "component_declaration") == 0) { name_node = ts_node_child_by_field_name(node, TS_FIELD("component")); } else if (strcmp(kind, "type_declaration") == 0) { name_node = ts_node_child_by_field_name(node, TS_FIELD("type")); } } // Verilog/SystemVerilog (FIELD_COUNT 0): module/class/interface/package use a // nested simple_identifier (first descendant); type_declaration must use the // DIRECT-child simple_identifier (member/enum idents precede the typedef name). if (ts_node_is_null(name_node) && (ctx->language == CBM_LANG_VERILOG || ctx->language == CBM_LANG_SYSTEMVERILOG)) { if (strcmp(kind, "type_declaration") == 0) { name_node = cbm_find_child_by_kind(node, "simple_identifier"); } else if (strcmp(kind, "module_declaration") == 0 || strcmp(kind, "class_declaration") == 0 || strcmp(kind, "interface_declaration") == 0 || strcmp(kind, "program_declaration") == 0 || strcmp(kind, "package_declaration") == 0) { name_node = find_first_descendant_by_kind(node, "simple_identifier", CBM_DESCENDANT_MAX_DEPTH); } } // Grammar-only languages whose class/struct/type node carries the name on a // plain identifier child (no `name` field) or nested one level under a // type-binding wrapper. Class 16 fix: extract_class_def is reached (dispatch // works) but name resolution fell through, so 0 nodes were emitted. if (ts_node_is_null(name_node)) { switch (ctx->language) { case CBM_LANG_SQUIRREL: // class_declaration > identifier case CBM_LANG_DLANG: // class/struct/interface_declaration > identifier case CBM_LANG_HARE: // type_declaration > identifier case CBM_LANG_ODIN: // struct_declaration > identifier case CBM_LANG_BICEP: // resource/module_declaration > identifier name_node = cbm_find_child_by_kind(node, "identifier"); break; case CBM_LANG_POWERSHELL: // class_statement > simple_name name_node = cbm_find_child_by_kind(node, "simple_name"); break; case CBM_LANG_SWAY: // impl_item: name is the implemented type (field `type`) if (strcmp(kind, "impl_item") == 0) { name_node = ts_node_child_by_field_name(node, TS_FIELD("type")); } break; case CBM_LANG_GLEAM: // type_definition > type_name > type_identifier name_node = find_first_descendant_by_kind(node, "type_identifier", CBM_DESCENDANT_MAX_DEPTH); break; case CBM_LANG_RESCRIPT: { // type_declaration > type_binding(name=type_identifier) TSNode binding = cbm_find_child_by_kind(node, "type_binding"); if (!ts_node_is_null(binding)) { name_node = ts_node_child_by_field_name(binding, TS_FIELD("name")); if (ts_node_is_null(name_node)) { name_node = cbm_find_child_by_kind(binding, "type_identifier"); } } break; } case CBM_LANG_FSHARP: { // type_definition > *_type_defn > type_name > identifier TSNode tn = find_first_descendant_by_kind(node, "type_name", CBM_DESCENDANT_MAX_DEPTH); if (!ts_node_is_null(tn)) { name_node = ts_node_child_by_field_name(tn, TS_FIELD("type_name")); if (ts_node_is_null(name_node)) { name_node = cbm_find_child_by_kind(tn, "identifier"); } } break; } case CBM_LANG_JULIA: { // struct/abstract_definition > type_head > identifier TSNode head = cbm_find_child_by_kind(node, "type_head"); if (!ts_node_is_null(head)) { name_node = find_first_descendant_by_kind(head, "identifier", CBM_DESCENDANT_MAX_DEPTH); } break; } case CBM_LANG_TCL: { // namespace > word_list > simple_word ("eval" then name) TSNode wl = cbm_find_child_by_kind(node, "word_list"); if (!ts_node_is_null(wl)) { int seen = 0; uint32_t wc = ts_node_child_count(wl); for (uint32_t i = 0; i < wc; i++) { TSNode w = ts_node_child(wl, i); if (strcmp(ts_node_type(w), "simple_word") == 0) { if (seen == 1) { // skip "eval", take the namespace name name_node = w; break; } seen++; } } } break; } case CBM_LANG_PASCAL: { // declClass is nested; name lives on the parent declType TSNode parent = ts_node_parent(node); if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "declType") == 0) { name_node = ts_node_child_by_field_name(parent, TS_FIELD("name")); } break; } case CBM_LANG_ZIG: { // `const Foo = struct {...}`: struct/enum/union_declaration // is the value of a variable_declaration; the name is the // parent variable_declaration's identifier child. TSNode parent = ts_node_parent(node); if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "variable_declaration") == 0) { name_node = cbm_find_child_by_kind(parent, "identifier"); } break; } default: break; } } if (ts_node_is_null(name_node)) { return; } char *name = cbm_node_text(a, name_node, ctx->source); if (!name || !name[0]) { return; } // For nested classes, prefix with enclosing class QN (e.g., Outer.Inner). // Top-level classes use the language-aware module QN so Java/Go don't double // the filename stem (Java `Outer` in Outer.java -> proj.Outer, not // proj.Outer.Outer); the nested prefix then yields proj.Outer.Inner. const char *class_qn; if (ctx->enclosing_class_qn) { class_qn = cbm_arena_sprintf(a, "%s.%s", ctx->enclosing_class_qn, name); } else { class_qn = cbm_fqn_compute_source_lang(a, ctx->project, ctx->rel_path, name, ctx->language); } const char *label = class_label_for_kind(kind); // Sway/WGSL: label struct defs as "Struct" and Sway `abi` blocks as // "Interface". Scoped to these grammar-only languages so established // struct-as-"Class" labeling (C++/Cap'n Proto …) and the downstream // type/IMPLEMENTS resolvers that depend on it are unaffected. if (ctx->language == CBM_LANG_SWAY || ctx->language == CBM_LANG_WGSL) { if (strcmp(kind, "struct_item") == 0 || strcmp(kind, "struct_declaration") == 0) { label = "Struct"; } else if (strcmp(kind, "abi_item") == 0) { label = "Interface"; } } // Rust/Swift/D: a struct is a distinct kind from a class — emit the precise // "Struct" label rather than collapsing it to "Class". Scoped to these three // grammar/LSP languages. Rust's struct node is `struct_item`; D's is // `struct_declaration`. C/C++/Obj-C keep `struct_specifier` → "Class" // (a C++ struct is class-like). "Struct" is a type-like container: every // type-resolution / registry / IMPLEMENTS / LSP-registrar consumer routes // through cbm_label_is_type_like(), so a struct still resolves as a type for // its methods, fields, inheritance and impls. if (ctx->language == CBM_LANG_RUST || ctx->language == CBM_LANG_SWIFT || ctx->language == CBM_LANG_DLANG) { if (strcmp(kind, "struct_item") == 0 || strcmp(kind, "struct_declaration") == 0) { label = "Struct"; } } // Swift: tree-sitter-swift does NOT have a dedicated `struct_declaration` // node — `struct`, `class` and `actor` all parse to `class_declaration`, // distinguished only by the `declaration_kind` field (the leading keyword // token). Read that field and emit "Struct" when the keyword is `struct` // (and "Class" for `class`/`actor`, which class_label_for_kind already gives). if (ctx->language == CBM_LANG_SWIFT && strcmp(kind, "class_declaration") == 0) { TSNode dk = ts_node_child_by_field_name(node, TS_FIELD("declaration_kind")); if (!ts_node_is_null(dk)) { char *dk_text = cbm_node_text(a, dk, ctx->source); if (dk_text && strcmp(dk_text, "struct") == 0) { label = "Struct"; } } } // F#: a `type_definition` that has a primary constructor (`type Foo(...) =`) // or an `inherit` clause is an OOP class, not a plain type alias. Label it // "Class" so it is registered as a resolvable inheritance target (the graph // registry only indexes Function/Method/Class/Interface labels), letting // `inherit Base` resolve into an INHERITS edge. if (ctx->language == CBM_LANG_FSHARP && strcmp(label, "Type") == 0) { if (!ts_node_is_null(find_first_descendant_by_kind(node, "primary_constr_args", CBM_DESCENDANT_MAX_DEPTH)) || !ts_node_is_null(find_first_descendant_by_kind(node, "class_inherits_decl", CBM_DESCENDANT_MAX_DEPTH))) { label = "Class"; } } // Go type_spec: check inner type for interface/struct. A Go `type T struct // {...}` is a struct → emit the precise "Struct" label (a type-like container; // its methods/fields/embedding resolve through cbm_label_is_type_like(), and // cbm_pipeline_implements_go() collects Struct nodes too). if (strcmp(kind, "type_spec") == 0) { TSNode type_inner = ts_node_child_by_field_name(node, TS_FIELD("type")); if (!ts_node_is_null(type_inner)) { const char *inner_kind = ts_node_type(type_inner); if (strcmp(inner_kind, "interface_type") == 0) { label = "Interface"; } else if (strcmp(inner_kind, "struct_type") == 0) { label = "Struct"; } } } CBMDefinition def; memset(&def, 0, sizeof(def)); def.name = name; def.qualified_name = class_qn; def.label = label; def.file_path = ctx->rel_path; def.start_line = ts_node_start_point(node).row + TS_LINE_OFFSET; def.end_line = ts_node_end_point(node).row + TS_LINE_OFFSET; def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET); def.is_exported = cbm_is_exported(name, ctx->language); def.base_classes = extract_base_classes(a, node, ctx->source, ctx->language); def.decorators = extract_decorators(a, node, ctx->source, ctx->language, spec); def.docstring = extract_docstring(a, node, ctx->source, ctx->language); cbm_defs_push(&ctx->result->defs, a, def); if (strcmp(label, "Enum") == 0) { extract_enum_members(ctx, node, class_qn); } // Extract methods inside the class extract_class_methods(ctx, node, class_qn, spec); // Extract typed struct/class fields (for cross-file LSP type resolution) extract_class_fields(ctx, node, class_qn, spec); // Extract class-level variables (field declarations) extract_class_variables(ctx, node, spec); // C# 12 primary-constructor parameters: declared on the class line // (`class Foo(IBar bar, IBaz baz) : Base { ... }`) and bound to implicit // captured fields accessible from any instance member. Tree-sitter c-sharp // wraps them inside the hidden _class_declaration_initializer node, so the // `parameters` field on class_declaration may not always resolve directly; // iterate top-level children for parameter_list as a robust fallback. if (ctx->language == CBM_LANG_CSHARP) { TSNode primary_params = ts_node_child_by_field_name(node, TS_FIELD("parameters")); if (ts_node_is_null(primary_params)) { uint32_t total = ts_node_child_count(node); for (uint32_t i = 0; i < total; i++) { TSNode c = ts_node_child(node, i); if (!ts_node_is_null(c) && strcmp(ts_node_type(c), "parameter_list") == 0) { primary_params = c; break; } } } if (!ts_node_is_null(primary_params)) { uint32_t pcount = ts_node_child_count(primary_params); for (uint32_t k = 0; k < pcount; k++) { TSNode p = ts_node_child(primary_params, k); if (ts_node_is_null(p) || !ts_node_is_named(p)) { continue; } char *pname = resolve_param_name(a, p, ctx->source); if (!pname || !pname[0]) { continue; } char *ptype = resolve_param_type_text(a, p, ctx->source, ctx->language); if (!ptype || !ptype[0]) { continue; } CBMDefinition pdef; memset(&pdef, 0, sizeof(pdef)); pdef.name = pname; pdef.qualified_name = cbm_arena_sprintf(a, "%s.%s", class_qn, pname); pdef.label = "Field"; pdef.file_path = ctx->rel_path; pdef.parent_class = class_qn; pdef.return_type = ptype; pdef.start_line = ts_node_start_point(p).row + TS_LINE_OFFSET; pdef.end_line = ts_node_end_point(p).row + TS_LINE_OFFSET; pdef.is_exported = false; cbm_defs_push(&ctx->result->defs, a, pdef); } } } } // Find the body/members node inside a class node static TSNode find_class_body(TSNode class_node, CBMLanguage lang) { // Try field names first static const char *body_fields[] = {"body", "members", "class_body", "declaration_list", NULL}; for (const char **f = body_fields; *f; f++) { TSNode body = ts_node_child_by_field_name(class_node, *f, (uint32_t)strlen(*f)); if (!ts_node_is_null(body)) { return body; } } // Go: type_spec -> type field (interface_type or struct_type) if (lang == CBM_LANG_GO) { TSNode type_inner = ts_node_child_by_field_name(class_node, TS_FIELD("type")); if (!ts_node_is_null(type_inner)) { return type_inner; } } // ObjC: class_implementation/class_interface has no single body node // Methods are inside implementation_definition children directly if (lang == CBM_LANG_OBJC) { return class_node; // iterate children of the class node itself } // Squirrel: class_declaration has no body field — member_declaration nodes // (each wrapping a function_declaration) are direct children of the class. if (lang == CBM_LANG_SQUIRREL) { return class_node; } // Smali: field_definition nodes are direct children of class_definition (no // dedicated body node) — iterate the class node itself. if (lang == CBM_LANG_SMALI) { return class_node; } // GraphQL: object/interface fields live in a fields_definition child. if (lang == CBM_LANG_GRAPHQL) { TSNode b = cbm_find_child_by_kind(class_node, "fields_definition"); if (!ts_node_is_null(b)) { return b; } } // Prisma: model columns live in a statement_block child. Gated to Prisma so // the common "statement_block" kind can never hijack another language's // class body via the generic fallback below. if (lang == CBM_LANG_PRISMA) { TSNode b = cbm_find_child_by_kind(class_node, "statement_block"); if (!ts_node_is_null(b)) { return b; } } // Fallback: search children for known body node types static const char *body_types[] = {"class_body", "interface_body", "enum_body", "template_body", "interface_type", "struct_type", "field_declaration_list", "compound_statement", "block", "closure", "implementation_definition", NULL}; uint32_t count = ts_node_child_count(class_node); for (uint32_t i = 0; i < count; i++) { TSNode child = ts_node_child(class_node, i); const char *ck = ts_node_type(child); for (const char **t = body_types; *t; t++) { if (strcmp(ck, *t) == 0) { return child; } } } TSNode null_node = {0}; return null_node; } // 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, or the // key of an object-literal property — the Zustand "actions returned from a // factory" pattern, `{ addItem: (...) => {...} }` (#341). 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, TS_FIELD("property")); } if (strcmp(pk, "public_field_definition") == 0 || strcmp(pk, "variable_declarator") == 0) { return ts_node_child_by_field_name(parent, TS_FIELD("name")); } if (strcmp(pk, "pair") == 0) { // Object-literal property `key: () => {...}` → name is the key. return ts_node_child_by_field_name(parent, TS_FIELD("key")); } } 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)) { return name_node; } const char *ck = ts_node_type(child); if ((lang == CBM_LANG_C || lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA || lang == CBM_LANG_GLSL) && strcmp(ck, "function_definition") == 0) { return cbm_resolve_func_name(child, lang); } if (lang == CBM_LANG_GROOVY && strcmp(ck, "function_definition") == 0) { TSNode fn = ts_node_child_by_field_name(child, TS_FIELD("function")); if (!ts_node_is_null(fn)) { return fn; } return cbm_find_child_by_kind(child, "identifier"); } if (lang == CBM_LANG_DART) { return resolve_dart_method_name(child, ck); } if (lang == CBM_LANG_OBJC && strcmp(ck, "method_definition") == 0) { return cbm_find_child_by_kind(child, "identifier"); } // Pony: `fun`/`be`/`new` members are `method`/`constructor`/`ffi_method` // nodes with no `name` field; the name is the first plain `identifier` child // (mirrors the free-function case in cbm_resolve_func_name). if (lang == CBM_LANG_PONY && (strcmp(ck, "method") == 0 || strcmp(ck, "constructor") == 0 || strcmp(ck, "ffi_method") == 0)) { return cbm_find_child_by_kind(child, "identifier"); } if ((lang == CBM_LANG_SWIFT || lang == CBM_LANG_KOTLIN) && strcmp(ck, "function_declaration") == 0) { return cbm_find_child_by_kind(child, "simple_identifier"); } // Squirrel: function_declaration's name is a plain `identifier` child. if (lang == CBM_LANG_SQUIRREL && strcmp(ck, "function_declaration") == 0) { return cbm_find_child_by_kind(child, "identifier"); } if (strcmp(ck, "arrow_function") == 0) { return resolve_arrow_func_name(child); } TSNode null_node = {0}; return null_node; } // Push a single method definition static void push_method_def(CBMExtractCtx *ctx, TSNode child, TSNode class_node, const char *class_qn, const CBMLangSpec *spec, TSNode name_node) { CBMArena *a = ctx->arena; char *name = cbm_func_name_node_text(a, name_node, ctx->source); if (!name || !name[0]) { return; } const char *method_qn = cbm_arena_sprintf(a, "%s.%s", class_qn, name); CBMDefinition def; memset(&def, 0, sizeof(def)); def.name = name; def.qualified_name = method_qn; def.label = "Method"; def.file_path = ctx->rel_path; def.parent_class = class_qn; def.start_line = ts_node_start_point(child).row + TS_LINE_OFFSET; def.end_line = ts_node_end_point(child).row + TS_LINE_OFFSET; def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET); def.is_exported = cbm_is_exported(name, ctx->language); TSNode params = ts_node_child_by_field_name(child, TS_FIELD("parameters")); if (!ts_node_is_null(params)) { def.signature = cbm_node_text(a, params, ctx->source); def.param_types = extract_param_types(a, params, ctx->source, ctx->language); } // Return type (same fields as extract_func_def) { static const char *rt_fields[] = {"result", "return_type", "type", NULL}; for (const char **f = rt_fields; *f; f++) { TSNode rt = ts_node_child_by_field_name(child, *f, (uint32_t)strlen(*f)); if (!ts_node_is_null(rt)) { def.return_type = cbm_node_text(a, rt, ctx->source); break; } } } // 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)) { resolve_cpp_trailing_return(a, child, ctx->source, &def); } def.decorators = extract_decorators(a, child, ctx->source, ctx->language, spec); extract_route_from_decorators(a, child, ctx->source, spec, &def.route_path, &def.route_method); if (def.route_path && (ctx->language == CBM_LANG_JAVA || ctx->language == CBM_LANG_KOTLIN)) { const char *prefix = spring_class_route_prefix(a, class_node, ctx->source, spec); def.route_path = join_route_paths(a, prefix, def.route_path); } def.docstring = extract_docstring(a, child, ctx->source, ctx->language); if (spec->branching_node_types && spec->branching_node_types[0]) { set_def_complexity(&def, child, spec); } // MinHash fingerprint compute_fingerprint(ctx, &def, child); 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, impl_node, class_qn, spec, nm); } } } } // Extract methods inside a class body 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); if (ts_node_is_null(body)) { return; } uint32_t count = ts_node_child_count(body); for (uint32_t i = 0; i < count; i++) { TSNode child = ts_node_child(body, i); if (ts_node_is_null(child)) { continue; } if (ctx->language == CBM_LANG_OBJC && strcmp(ts_node_type(child), "implementation_definition") == 0) { extract_objc_impl_methods(ctx, child, class_qn, spec); continue; } // Squirrel wraps each class member in a member_declaration node; the // method is the inner function_declaration. Peek through the wrapper. if (ctx->language == CBM_LANG_SQUIRREL && strcmp(ts_node_type(child), "member_declaration") == 0) { TSNode inner = cbm_find_child_by_kind(child, "function_declaration"); if (!ts_node_is_null(inner)) { child = inner; } } // Python wraps @classmethod / @staticmethod / @property methods in // a decorated_definition node. Peek through it to find the inner // function_definition so we still emit a Method entry. TSNode method_node = child; if (strcmp(ts_node_type(child), "decorated_definition") == 0) { TSNode def = ts_node_child_by_field_name(child, TS_FIELD("definition")); if (ts_node_is_null(def) || !cbm_kind_in_set(def, spec->function_node_types)) { continue; } method_node = def; } // TS/JS class-field arrow functions: `handleClick = () => {...}` is a // public_field_definition whose `value` is an arrow_function (a common // React event-handler pattern). It is not in function_node_types, so it // would otherwise be dropped. Peek through to the inner arrow and take // the method name from the field's `name` child (#new_ts_class_field_arrow). if (strcmp(ts_node_type(child), "public_field_definition") == 0) { TSNode value = ts_node_child_by_field_name(child, TS_FIELD("value")); if (ts_node_is_null(value) || !cbm_kind_in_set(value, spec->function_node_types)) { continue; } TSNode fname = ts_node_child_by_field_name(child, TS_FIELD("name")); if (ts_node_is_null(fname)) { continue; } push_method_def(ctx, value, class_node, class_qn, spec, fname); continue; } if (!cbm_kind_in_set(method_node, spec->function_node_types)) { continue; } TSNode name_node = resolve_method_name(method_node, ctx->language); if (ts_node_is_null(name_node)) { continue; } push_method_def(ctx, method_node, class_node, class_qn, spec, name_node); } } // --- Rust impl block extraction --- static void extract_rust_impl(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) { CBMArena *a = ctx->arena; TSNode type_node = ts_node_child_by_field_name(node, TS_FIELD("type")); if (ts_node_is_null(type_node)) { return; } char *type_name = cbm_node_text(a, type_node, ctx->source); if (!type_name || !type_name[0]) { return; } /* Strip generic args from the implementing type: `Buffer` → `Buffer`, * `Wrapper` → `Wrapper`. The struct identity is the base name. */ { char *lt = strchr(type_name, '<'); if (lt) { *lt = '\0'; } } // Check for "impl Trait for Struct" pattern TSNode trait_node = ts_node_child_by_field_name(node, TS_FIELD("trait")); if (!ts_node_is_null(trait_node)) { char *trait_name = cbm_node_text(a, trait_node, ctx->source); /* Strip generic args from the trait: `From` → `From`, * `Index` → `Index`, `AsRef` → `AsRef`. Qualified paths * like `io::Write` / `fmt::Display` have no `<` and are preserved. */ if (trait_name) { char *lt = strchr(trait_name, '<'); if (lt) { *lt = '\0'; } } if (trait_name && trait_name[0]) { CBMImplTrait it; it.trait_name = trait_name; it.struct_name = type_name; cbm_impltrait_push(&ctx->result->impl_traits, a, it); } } const char *type_qn = cbm_fqn_compute(a, ctx->project, ctx->rel_path, type_name); // Extract methods inside impl body TSNode body = ts_node_child_by_field_name(node, TS_FIELD("body")); if (ts_node_is_null(body)) { return; } uint32_t count = ts_node_child_count(body); for (uint32_t i = 0; i < count; i++) { TSNode child = ts_node_child(body, i); if (ts_node_is_null(child)) { continue; } if (!cbm_kind_in_set(child, spec->function_node_types)) { continue; } TSNode name_node = func_name_node(child); if (ts_node_is_null(name_node)) { continue; } char *name = cbm_node_text(a, name_node, ctx->source); if (!name || !name[0]) { continue; } const char *method_qn = cbm_arena_sprintf(a, "%s.%s", type_qn, name); CBMDefinition def; memset(&def, 0, sizeof(def)); def.name = name; def.qualified_name = method_qn; def.label = "Method"; def.file_path = ctx->rel_path; def.parent_class = type_qn; def.start_line = ts_node_start_point(child).row + TS_LINE_OFFSET; def.end_line = ts_node_end_point(child).row + TS_LINE_OFFSET; def.is_exported = cbm_is_exported(name, ctx->language); TSNode params = ts_node_child_by_field_name(child, TS_FIELD("parameters")); if (!ts_node_is_null(params)) { def.signature = cbm_node_text(a, params, ctx->source); def.param_types = extract_param_types(a, params, ctx->source, ctx->language); } if (spec->branching_node_types && spec->branching_node_types[0]) { set_def_complexity(&def, child, spec); } // MinHash fingerprint compute_fingerprint(ctx, &def, child); cbm_defs_push(&ctx->result->defs, a, def); } } // --- Elixir def/defp/defmodule --- // 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, TS_FIELD("arguments")); if (ts_node_is_null(args) && ts_node_child_count(node) > SECOND_CHILD_IDX) { args = ts_node_child(node, SECOND_CHILD_IDX); } 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 + TS_LINE_OFFSET; def.end_line = ts_node_end_point(node).row + TS_LINE_OFFSET; def.is_exported = (strcmp(macro, "def") == 0 || strcmp(macro, "defmacro") == 0); cbm_defs_push(&ctx->result->defs, a, def); } // Emit Class definition for an Elixir defmodule node. Returns do_block or null. static TSNode emit_elixir_module_class(CBMExtractCtx *ctx, TSNode cur) { CBMArena *a = ctx->arena; TSNode null_node = {0}; TSNode args = elixir_call_args(cur); if (ts_node_is_null(args)) { return null_node; } TSNode name_node = ts_node_child(args, 0); if (ts_node_is_null(name_node)) { return null_node; } char *name = cbm_node_text(a, name_node, ctx->source); if (!name || !name[0]) { return null_node; } 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(cur).row + TS_LINE_OFFSET; def.end_line = ts_node_end_point(cur).row + TS_LINE_OFFSET; def.is_exported = true; cbm_defs_push(&ctx->result->defs, a, def); return cbm_find_child_by_kind(cur, "do_block"); } // Process Elixir call nodes iteratively — handles defmodule/def/defp/defmacro // without recursion between extract_elixir_call ↔ extract_elixir_module_def. static void extract_elixir_call(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) { (void)spec; TSNodeStack stack; ts_nstack_init(&stack, ctx->arena, CBM_SZ_64); ts_nstack_push(&stack, ctx->arena, node); while (stack.count > 0) { TSNode cur = ts_nstack_pop(&stack); CBMArena *a = ctx->arena; if (ts_node_child_count(cur) == 0) { continue; } TSNode callee = ts_node_child(cur, 0); if (ts_node_is_null(callee)) { continue; } char *macro = cbm_node_text(a, callee, ctx->source); if (!macro) { continue; } if (strcmp(macro, "def") == 0 || strcmp(macro, "defp") == 0 || strcmp(macro, "defmacro") == 0) { extract_elixir_func_def(ctx, cur, macro); } else if (strcmp(macro, "defmodule") == 0) { TSNode do_block = emit_elixir_module_class(ctx, cur); if (!ts_node_is_null(do_block)) { uint32_t dbc = ts_node_child_count(do_block); for (int di = (int)dbc - SKIP_CHAR; di >= 0; di--) { TSNode dchild = ts_node_child(do_block, (uint32_t)di); if (!ts_node_is_null(dchild) && strcmp(ts_node_type(dchild), "call") == 0) { ts_nstack_push(&stack, ctx->arena, dchild); } } } } } } // --- Variable extraction --- // Helper to push a Variable definition static void push_var_def(CBMExtractCtx *ctx, const char *name, TSNode node) { if (!name || !name[0] || strcmp(name, "_") == 0) { return; } CBMArena *a = ctx->arena; CBMDefinition def; memset(&def, 0, sizeof(def)); def.name = name; /* Java/Go: directory-based module (package), so a Go package-level var in * myapp/db/conn.go is proj.myapp.db.Var, matching its siblings. */ def.qualified_name = cbm_fqn_compute_source_lang(a, ctx->project, ctx->rel_path, name, ctx->language); def.label = "Variable"; def.file_path = ctx->rel_path; def.start_line = ts_node_start_point(node).row + TS_LINE_OFFSET; def.end_line = ts_node_end_point(node).row + TS_LINE_OFFSET; def.is_exported = cbm_is_exported(name, ctx->language); cbm_defs_push(&ctx->result->defs, a, def); } // Helper: extract name from a declarator chain (C/C++/ObjC) // declaration > init_declarator > declarator (may be pointer_declarator > identifier) static const char *extract_c_declarator_name(CBMArena *a, TSNode decl, const char *source) { // Try "declarator" field on the declaration TSNode declarator = ts_node_child_by_field_name(decl, TS_FIELD("declarator")); if (ts_node_is_null(declarator)) { return NULL; } // Could be init_declarator wrapping the actual declarator const char *dk = ts_node_type(declarator); if (strcmp(dk, "init_declarator") == 0) { declarator = ts_node_child_by_field_name(declarator, TS_FIELD("declarator")); if (ts_node_is_null(declarator)) { return NULL; } dk = ts_node_type(declarator); } // Unwrap pointer_declarator while (strcmp(dk, "pointer_declarator") == 0 || strcmp(dk, "reference_declarator") == 0) { declarator = ts_node_child_by_field_name(declarator, TS_FIELD("declarator")); if (ts_node_is_null(declarator)) { return NULL; } dk = ts_node_type(declarator); } if (strcmp(dk, "identifier") == 0) { return cbm_node_text(a, declarator, source); } return NULL; } // Helper: extract name from Java/C# field_declaration (declarator > name) static const char *extract_java_field_name(CBMArena *a, TSNode field, const char *source) { TSNode declarator = ts_node_child_by_field_name(field, TS_FIELD("declarator")); if (ts_node_is_null(declarator)) { // Try iterating children for variable_declarator uint32_t n = ts_node_named_child_count(field); for (uint32_t i = 0; i < n; i++) { TSNode child = ts_node_named_child(field, i); if (strcmp(ts_node_type(child), "variable_declarator") == 0) { declarator = child; break; } } } if (ts_node_is_null(declarator)) { return NULL; } TSNode name = ts_node_child_by_field_name(declarator, TS_FIELD("name")); if (!ts_node_is_null(name)) { return cbm_node_text(a, name, source); } return NULL; } /* ── Variable name extractors by language group ─────────────────── */ // 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, TS_FIELD("name")); 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); } } } } } /* Check if a tree-sitter node type is an enum member declaration. */ static bool is_enum_member_kind(const char *kind) { return strcmp(kind, "enum_member_declaration") == 0 || strcmp(kind, "enum_constant") == 0 || strcmp(kind, "enum_member") == 0 || strcmp(kind, "enum_assignment") == 0 || strcmp(kind, "enumerator") == 0; } /* Extract enum members as Variable nodes (C#, Java, TypeScript, C++). */ static void extract_enum_members(CBMExtractCtx *ctx, TSNode node, const char *class_qn) { CBMArena *a = ctx->arena; TSNode body = find_class_body(node, ctx->language); if (ts_node_is_null(body)) { return; } uint32_t mc = ts_node_named_child_count(body); for (uint32_t mi = 0; mi < mc; mi++) { TSNode member = ts_node_named_child(body, mi); if (!is_enum_member_kind(ts_node_type(member))) { continue; } TSNode mname = ts_node_child_by_field_name(member, TS_FIELD("name")); if (ts_node_is_null(mname)) { mname = cbm_find_child_by_kind(member, "identifier"); } if (ts_node_is_null(mname)) { continue; } char *member_name = cbm_node_text(a, mname, ctx->source); if (!member_name || !member_name[0]) { continue; } CBMDefinition mdef; memset(&mdef, 0, sizeof(mdef)); mdef.name = member_name; mdef.qualified_name = cbm_arena_sprintf(a, "%s.%s", class_qn, member_name); mdef.label = "Variable"; mdef.file_path = ctx->rel_path; mdef.start_line = ts_node_start_point(member).row + TS_LINE_OFFSET; mdef.end_line = ts_node_end_point(member).row + TS_LINE_OFFSET; cbm_defs_push(&ctx->result->defs, a, mdef); } } /* Resolve the identifier node from a destructure pattern child. * pair_pattern → value field; shorthand/identifier → itself; others → first named child. */ static TSNode destructure_ident(TSNode pat_child) { const char *pk = ts_node_type(pat_child); if (strcmp(pk, "shorthand_property_identifier_pattern") == 0 || strcmp(pk, "identifier") == 0) { return pat_child; } if (strcmp(pk, "pair_pattern") == 0) { return ts_node_child_by_field_name(pat_child, TS_FIELD("value")); } /* rest_pattern, assignment_pattern, etc. — first named child. */ return ts_node_named_child(pat_child, 0); } /* Emit individual Variable nodes for each destructured binding. */ static void extract_destructured_vars(CBMExtractCtx *ctx, TSNode pattern, TSNode decl, CBMArena *a) { uint32_t pc = ts_node_named_child_count(pattern); for (uint32_t pi = 0; pi < pc; pi++) { TSNode pat_child = ts_node_named_child(pattern, pi); TSNode ident = destructure_ident(pat_child); if (ts_node_is_null(ident)) { continue; } char *id_text = cbm_node_text(a, ident, ctx->source); if (id_text && id_text[0]) { push_var_def(ctx, id_text, decl); } } } /* True for `require("...")` call_expressions with a string-literal path — the * CommonJS import form that extract_imports records with local_name = the * enclosing declarator's identifier. Same string-argument kinds as * process_commonjs_require so the two stay in lockstep. */ static bool is_require_import_call(TSNode value, const char *source, CBMArena *a) { if (strcmp(ts_node_type(value), "call_expression") != 0) { return false; } TSNode fn = ts_node_child_by_field_name(value, TS_FIELD("function")); if (ts_node_is_null(fn) || strcmp(ts_node_type(fn), "identifier") != 0) { return false; } char *fn_name = cbm_node_text(a, fn, source); if (!fn_name || strcmp(fn_name, "require") != 0) { return false; } TSNode args = ts_node_child_by_field_name(value, TS_FIELD("arguments")); if (ts_node_is_null(args)) { return false; } uint32_t argc = ts_node_named_child_count(args); for (uint32_t i = 0; i < argc; i++) { const char *ak = ts_node_type(ts_node_named_child(args, i)); if (strcmp(ak, "string") == 0 || strcmp(ak, "string_literal") == 0 || strcmp(ak, "template_string") == 0) { return true; } } return false; } // 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; } bool is_require = false; TSNode value = ts_node_child_by_field_name(child, TS_FIELD("value")); 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; } is_require = is_require_import_call(value, ctx->source, a); } TSNode vname = ts_node_child_by_field_name(child, TS_FIELD("name")); if (!ts_node_is_null(vname)) { const char *nk = ts_node_type(vname); /* Destructured patterns: emit individual identifiers instead of * the raw "{A, B, C}" text as a single Variable node. */ if (strcmp(nk, "object_pattern") == 0 || strcmp(nk, "array_pattern") == 0) { extract_destructured_vars(ctx, vname, child, a); } else { /* `const foo = require('./foo')` is an import binding, not a * definition — extract_imports records local_name="foo". A * Variable node here shadows call resolution onto the alias * and orphans the imported callee (#871); ESM `import` * bindings emit no Variable either. Destructured requires * keep theirs: the import row only records the module. */ if (is_require) { continue; } 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; switch (ctx->language) { case CBM_LANG_PYTHON: { TSNode left = ts_node_child_by_field_name(node, TS_FIELD("left")); if (ts_node_is_null(left)) { break; } const char *lt = ts_node_type(left); if (strcmp(lt, "identifier") == 0) { push_var_def(ctx, cbm_node_text(a, left, ctx->source), node); } else if (strcmp(lt, "pattern_list") == 0 || strcmp(lt, "tuple_pattern") == 0 || strcmp(lt, "list_pattern") == 0) { /* Tuple/list unpacking: `x, y = f()` — emit a Variable def for each * unpacked identifier on the LHS (#new_py_tuple_unpack). */ uint32_t ln = ts_node_named_child_count(left); for (uint32_t li = 0; li < ln; li++) { TSNode part = ts_node_named_child(left, li); if (strcmp(ts_node_type(part), "identifier") == 0) { push_var_def(ctx, cbm_node_text(a, part, ctx->source), node); } } } break; } case CBM_LANG_GO: { 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, "var_spec") == 0 || strcmp(ck, "const_spec") == 0) { TSNode vname = ts_node_child_by_field_name(child, TS_FIELD("name")); if (!ts_node_is_null(vname)) { push_var_def(ctx, cbm_node_text(a, vname, ctx->source), child); } } } break; } case CBM_LANG_JAVASCRIPT: case CBM_LANG_TYPESCRIPT: 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) { push_var_def(ctx, fname, node); } break; } case CBM_LANG_CSHARP: extract_csharp_vars(ctx, node, a); break; case CBM_LANG_CPP: case CBM_LANG_C: case CBM_LANG_OBJC: { const char *vname = extract_c_declarator_name(a, node, ctx->source); if (vname) { push_var_def(ctx, vname, node); } break; } case CBM_LANG_RUST: { TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name")); if (!ts_node_is_null(name_node)) { push_var_def(ctx, cbm_node_text(a, name_node, ctx->source), node); } break; } default: break; } } // 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, TS_FIELD("variables")); 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 + SKIP_CHAR; } 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, TS_FIELD("left")); 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, TS_FIELD("left")); if (ts_node_is_null(left)) { left = ts_node_child_by_field_name(node, TS_FIELD("lhs")); } 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, TS_FIELD("left")); 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: extract_php_vars(ctx, node, a, kind); break; case CBM_LANG_LUA: extract_lua_vars(ctx, node, a); break; case CBM_LANG_RUBY: { TSNode left = ts_node_child_by_field_name(node, TS_FIELD("left")); if (!ts_node_is_null(left)) { const char *lk = ts_node_type(left); if (strcmp(lk, "identifier") == 0 || strcmp(lk, "constant") == 0) { push_var_def(ctx, cbm_node_text(a, left, ctx->source), node); } } break; } case CBM_LANG_R: extract_r_vars(ctx, node, a); break; case CBM_LANG_PERL: extract_perl_vars(ctx, node, a); break; default: break; } } // 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, TS_FIELD("name")); 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: { TSNode pattern = ts_node_child_by_field_name(node, TS_FIELD("pattern")); if (!ts_node_is_null(pattern)) { push_var_def(ctx, cbm_node_text(a, pattern, ctx->source), node); } else { TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name")); if (!ts_node_is_null(name_node)) { push_var_def(ctx, cbm_node_text(a, name_node, ctx->source), node); } } break; } case CBM_LANG_KOTLIN: { 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); } break; } case CBM_LANG_GROOVY: { TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name")); if (ts_node_is_null(name_node)) { const char *cname = extract_c_declarator_name(a, node, ctx->source); if (cname) { push_var_def(ctx, cname, node); break; } name_node = cbm_find_child_by_kind(node, "identifier"); } if (!ts_node_is_null(name_node)) { push_var_def(ctx, cbm_node_text(a, name_node, ctx->source), node); } break; } default: break; } } // 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 - SKIP_CHAR] == ' ' || name[nlen - SKIP_CHAR] == '\t')) { name[nlen - SKIP_CHAR] = '\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, TS_FIELD("key")); 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 >= PAIR_CHARS && raw[0] == '"' && raw[rlen - SKIP_CHAR] == '"') { raw[rlen - SKIP_CHAR] = '\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, TS_FIELD("property")); if (ts_node_is_null(prop)) { prop = ts_node_child_by_field_name(node, TS_FIELD("name")); } 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: { TSNode key = ts_node_child_by_field_name(node, TS_FIELD("key")); if (!ts_node_is_null(key)) { push_var_def(ctx, cbm_node_text(a, key, ctx->source), node); } break; } case CBM_LANG_TOML: { char *name = find_toml_key_name(a, node, ctx->source); if (name) { push_var_def(ctx, name, node); } break; } case CBM_LANG_JSON: extract_json_var(ctx, node, a); break; 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) { 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: { 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: { TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name")); if (!ts_node_is_null(name_node)) { push_var_def(ctx, cbm_node_text(a, name_node, ctx->source), node); } else { 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: extract_scss_var(ctx, node, a); break; default: break; } } /* ── Variable name extraction dispatcher ────────────────────────── */ static void extract_var_names(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) { (void)spec; CBMArena *a = ctx->arena; const char *kind = ts_node_type(node); switch (ctx->language) { /* Mainstream + C-family + Rust */ case CBM_LANG_PYTHON: case CBM_LANG_GO: case CBM_LANG_JAVASCRIPT: case CBM_LANG_TYPESCRIPT: case CBM_LANG_TSX: case CBM_LANG_JAVA: case CBM_LANG_CSHARP: case CBM_LANG_CPP: case CBM_LANG_C: case CBM_LANG_OBJC: case CBM_LANG_RUST: extract_vars_mainstream(ctx, node, a, kind); return; /* Dynamic/scripting */ case CBM_LANG_PHP: case CBM_LANG_LUA: case CBM_LANG_RUBY: case CBM_LANG_R: case CBM_LANG_PERL: extract_vars_dynamic(ctx, node, a, kind); return; /* JVM (non-Java) */ case CBM_LANG_SCALA: case CBM_LANG_KOTLIN: case CBM_LANG_GROOVY: extract_vars_jvm(ctx, node, a); return; /* Config + other */ case CBM_LANG_YAML: case CBM_LANG_TOML: case CBM_LANG_JSON: case CBM_LANG_INI: case CBM_LANG_ERLANG: case CBM_LANG_SQL: case CBM_LANG_BASH: case CBM_LANG_SCSS: extract_vars_config(ctx, node, a, kind); return; /* Dockerfile: `ENV K=V ...` is an env_instruction holding one or more * env_pair children, each with a `name` field; `ARG K=V` is an * arg_instruction whose name is the first unquoted_string child. The default * fallback misses both (no `name` field on the instruction, child is an * env_pair rather than a bare identifier). */ case CBM_LANG_DOCKERFILE: if (strcmp(kind, "env_instruction") == 0) { uint32_t ec = ts_node_named_child_count(node); for (uint32_t i = 0; i < ec; i++) { TSNode pair = ts_node_named_child(node, i); if (strcmp(ts_node_type(pair), "env_pair") != 0) { continue; } TSNode nm = ts_node_child_by_field_name(pair, TS_FIELD("name")); if (!ts_node_is_null(nm)) { push_var_def(ctx, cbm_node_text(a, nm, ctx->source), pair); } } } else if (strcmp(kind, "arg_instruction") == 0) { TSNode nm = ts_node_child_by_field_name(node, TS_FIELD("name")); if (ts_node_is_null(nm)) { nm = cbm_find_child_by_kind(node, "unquoted_string"); } if (!ts_node_is_null(nm)) { push_var_def(ctx, cbm_node_text(a, nm, ctx->source), node); } } return; /* .properties: `key=value` is a `property` node whose name is the `key` * child (a bare `key` kind, not an identifier or a `name` field), so the * default fallback misses it. */ case CBM_LANG_PROPERTIES: if (strcmp(kind, "property") == 0) { TSNode key = cbm_find_child_by_kind(node, "key"); if (!ts_node_is_null(key)) { push_var_def(ctx, cbm_node_text(a, key, ctx->source), node); } } return; /* go.mod: a `require_directive` wraps one or more `require_spec` children, * each `(module_path version)`. Mint one Variable per required module, * named by its module_path. The default fallback misses both (no `name` * field; child is a require_spec, not a bare identifier). */ case CBM_LANG_GOMOD: if (strcmp(kind, "require_directive") == 0 || strcmp(kind, "replace_directive") == 0) { uint32_t rc = ts_node_named_child_count(node); for (uint32_t i = 0; i < rc; i++) { TSNode req_spec = ts_node_named_child(node, i); const char *sk = ts_node_type(req_spec); if (strcmp(sk, "require_spec") != 0 && strcmp(sk, "replace_spec") != 0) { continue; } TSNode mp = cbm_find_child_by_kind(req_spec, "module_path"); if (!ts_node_is_null(mp)) { push_var_def(ctx, cbm_node_text(a, mp, ctx->source), req_spec); } } } return; default: break; } /* Default fallback: name field → C-declarator → first identifier */ TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name")); if (!ts_node_is_null(name_node)) { push_var_def(ctx, cbm_node_text(a, name_node, ctx->source), node); return; } const char *cname = extract_c_declarator_name(a, node, ctx->source); if (cname) { push_var_def(ctx, cname, 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), "identifier") == 0) { push_var_def(ctx, cbm_node_text(a, child, ctx->source), node); return; } } } // Iterative variable walker for config languages with nested structure. // Used by YAML, TOML, INI, JSON. static void walk_variables_iter(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec) { TSNodeStack stack; ts_nstack_init(&stack, ctx->arena, CBM_SZ_256); ts_nstack_push(&stack, ctx->arena, root); while (stack.count > 0) { TSNode node = ts_nstack_pop(&stack); uint32_t count = ts_node_child_count(node); for (int i = (int)count - SKIP_CHAR; i >= 0; i--) { TSNode child = ts_node_child(node, (uint32_t)i); if (ts_node_is_null(child)) { continue; } if (cbm_kind_in_set(child, spec->variable_node_types)) { /* parent of `child` is `node` (we're iterating its children) — * pass it directly to avoid the O(n) ts_node_parent rescan. */ if (cbm_is_module_level_p(node, ctx->language)) { extract_var_names(ctx, child, spec); } } const char *ck = ts_node_type(child); if (strcmp(ck, "document") == 0 || strcmp(ck, "block_node") == 0 || strcmp(ck, "block_mapping") == 0 || strcmp(ck, "stream") == 0 || strcmp(ck, "table") == 0 || strcmp(ck, "table_array_element") == 0 || strcmp(ck, "section") == 0 || strcmp(ck, "object") == 0 || strcmp(ck, "array") == 0 || strcmp(ck, "pair") == 0 || strcmp(ck, "element") == 0 || strcmp(ck, "content") == 0) { ts_nstack_push(&stack, ctx->arena, child); } } } } // True if the file's basename is values.yaml / values.yml (Helm values, #338). static bool is_helm_values_file(const char *rel) { if (!rel) { return false; } const char *b = strrchr(rel, '/'); b = b ? b + 1 : rel; return strcmp(b, "values.yaml") == 0 || strcmp(b, "values.yml") == 0; } // Extract ONLY top-level keys of a YAML document (no leaf explosion). Used for // Helm values.yaml so each chart's tunables surface as a handful of structured // Variables instead of one node per nested leaf (#338). static void extract_yaml_toplevel_keys(CBMExtractCtx *ctx, TSNode root) { CBMArena *a = ctx->arena; // Descend stream -> document -> block_node down to the first block_mapping. TSNode bm = {0}; TSNode cur = root; for (int depth = 0; depth < 6 && ts_node_is_null(bm); depth++) { uint32_t n = ts_node_child_count(cur); TSNode next = {0}; bool have_next = false; for (uint32_t i = 0; i < n; i++) { TSNode ch = ts_node_child(cur, i); const char *ck = ts_node_type(ch); if (strcmp(ck, "block_mapping") == 0) { bm = ch; break; } if (!have_next && (strcmp(ck, "document") == 0 || strcmp(ck, "block_node") == 0)) { next = ch; have_next = true; } } if (!ts_node_is_null(bm) || !have_next) { break; } cur = next; } if (ts_node_is_null(bm)) { return; } uint32_t n = ts_node_named_child_count(bm); for (uint32_t i = 0; i < n; i++) { TSNode pair = ts_node_named_child(bm, i); if (strcmp(ts_node_type(pair), "block_mapping_pair") != 0) { continue; } TSNode key = ts_node_child_by_field_name(pair, TS_FIELD("key")); if (!ts_node_is_null(key)) { push_var_def(ctx, cbm_node_text(a, key, ctx->source), pair); } } } static void extract_variables(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec) { if (!spec->variable_node_types || !spec->variable_node_types[0]) { return; } // Helm values.yaml: only top-level keys, not the per-leaf flood. if (ctx->language == CBM_LANG_YAML && is_helm_values_file(ctx->rel_path)) { extract_yaml_toplevel_keys(ctx, root); return; } // Config languages with nested structure: use recursive walk if (ctx->language == CBM_LANG_YAML || ctx->language == CBM_LANG_TOML || ctx->language == CBM_LANG_INI || ctx->language == CBM_LANG_JSON) { walk_variables_iter(ctx, root, spec); return; } // `root` is the file's root node (the sole caller passes ctx->root), so it // is the parent of every top-level child and the module-level check is // invariant across the loop — hoist it out (and it's now O(1) via _p). // NB: this short-circuit relies on `root` always being the file root; if a // future caller passes a non-root node, restore the per-child check. if (!cbm_is_module_level_p(root, ctx->language)) { return; } // Iterate top-level children via a cursor (O(n) total) instead of // ts_node_child(root, i) per index, which is O(i) each → O(n²) on files // with tens of thousands of top-level statements (e.g. generated/fixture // files like TS's reallyLargeFile.ts, 583K lines). TSTreeCursor cursor = ts_tree_cursor_new(root); if (ts_tree_cursor_goto_first_child(&cursor)) { do { TSNode child = ts_tree_cursor_current_node(&cursor); if (cbm_kind_in_set(child, spec->variable_node_types)) { extract_var_names(ctx, child, spec); continue; } // Unwrap wrapper nodes: expression_statement, export_statement, statement const char *ck = ts_node_type(child); if (strcmp(ck, "expression_statement") == 0 || strcmp(ck, "export_statement") == 0 || strcmp(ck, "statement") == 0) { // Check inner named children for variable types uint32_t nc = ts_node_named_child_count(child); for (uint32_t j = 0; j < nc; j++) { TSNode inner = ts_node_named_child(child, j); if (cbm_kind_in_set(inner, spec->variable_node_types)) { extract_var_names(ctx, inner, spec); } } // Also check if the wrapper itself is a variable type (e.g., PHP // expression_statement) if (cbm_kind_in_set(child, spec->variable_node_types)) { extract_var_names(ctx, child, spec); } } } while (ts_tree_cursor_goto_next_sibling(&cursor)); } ts_tree_cursor_delete(&cursor); } // 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. // 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, TS_FIELD("declarator")); for (int depth = 0; depth < C_RETURN_WALK_DEPTH && !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, TS_FIELD("declarator")); 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, TS_FIELD("declarator")); if (ts_node_is_null(name_node)) { name_node = ts_node_child_by_field_name(child, TS_FIELD("name")); } 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, TS_FIELD("declarator")); if (!ts_node_is_null(inner)) { return inner; } TSNode null_node = {0}; return null_node; } return name_node; } /* Schema/grammar languages whose field node carries the field name on a plain * child (no C-style `declarator`/`type` field), so the generic field path below * skips them. Emit a "Field" def (with optional return_type) and return true if * handled. GraphQL: field_definition (name)(type:named_type); Prisma: * column_declaration (identifier)(column_type); Smali: field_definition * (field_identifier)(field_type). */ static bool extract_schema_field(CBMExtractCtx *ctx, TSNode child, const char *class_qn) { CBMArena *a = ctx->arena; TSNode name_node = {0}; TSNode type_node = {0}; if (ctx->language == CBM_LANG_GRAPHQL) { name_node = ts_node_child_by_field_name(child, TS_FIELD("name")); if (ts_node_is_null(name_node)) { name_node = cbm_find_child_by_kind(child, "name"); } type_node = ts_node_child_by_field_name(child, TS_FIELD("type")); } else if (ctx->language == CBM_LANG_PRISMA) { name_node = cbm_find_child_by_kind(child, "identifier"); type_node = cbm_find_child_by_kind(child, "column_type"); } else if (ctx->language == CBM_LANG_SMALI) { name_node = cbm_find_child_by_kind(child, "field_identifier"); type_node = cbm_find_child_by_kind(child, "field_type"); } else { return false; } if (ts_node_is_null(name_node)) { return true; // language matched but no name → nothing to emit } char *name = cbm_node_text(a, name_node, ctx->source); if (!name || !name[0]) { return true; } CBMDefinition def; memset(&def, 0, sizeof(def)); def.name = name; def.qualified_name = cbm_arena_sprintf(a, "%s.%s", class_qn, name); def.label = "Field"; def.file_path = ctx->rel_path; def.parent_class = class_qn; if (!ts_node_is_null(type_node)) { def.return_type = cbm_node_text(a, type_node, ctx->source); } def.start_line = ts_node_start_point(child).row + TS_LINE_OFFSET; def.end_line = ts_node_end_point(child).row + TS_LINE_OFFSET; def.is_exported = cbm_is_exported(name, ctx->language); cbm_defs_push(&ctx->result->defs, a, def); return true; } 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]) { return; } TSNode body = find_class_body(class_node, ctx->language); if (ts_node_is_null(body)) { return; } CBMArena *a = ctx->arena; uint32_t count = ts_node_named_child_count(body); for (uint32_t i = 0; i < count; i++) { TSNode child = ts_node_named_child(body, i); if (!cbm_kind_in_set(child, spec->field_node_types)) { continue; } if (is_func_ptr_field(child)) { continue; } /* Schema/grammar languages (GraphQL/Prisma/Smali) carry the field name on * a plain child rather than a C-style declarator/type field; handle them * up front so the generic "type"-field path below doesn't skip them. */ if (extract_schema_field(ctx, child, class_qn)) { continue; } /* Locate the field's "type" + name node. Two shapes: * - direct (Java/Go/Rust/C/C++): * field_declaration .type=identifier .declarator=variable_declarator(.name) * - nested (C#): * field_declaration > variable_declaration(.type=identifier, * variable_declarator(.name)) * For the nested case, the child has no "type" field directly. Detect by * walking named children for a variable_declaration. */ TSNode type_node = ts_node_child_by_field_name(child, TS_FIELD("type")); TSNode name_node = ts_node_is_null(type_node) ? (TSNode){0} : resolve_field_name_node(child); if (ts_node_is_null(type_node)) { uint32_t cnc = ts_node_named_child_count(child); for (uint32_t k = 0; k < cnc; k++) { TSNode inner = ts_node_named_child(child, k); if (strcmp(ts_node_type(inner), "variable_declaration") != 0) { continue; } type_node = ts_node_child_by_field_name(inner, TS_FIELD("type")); /* Find first variable_declarator child for the name. */ uint32_t nc = ts_node_named_child_count(inner); for (uint32_t j = 0; j < nc; j++) { TSNode vd = ts_node_named_child(inner, j); if (strcmp(ts_node_type(vd), "variable_declarator") == 0) { TSNode nm = ts_node_child_by_field_name(vd, TS_FIELD("name")); if (!ts_node_is_null(nm)) { name_node = nm; break; } } } break; } } if (ts_node_is_null(type_node)) { continue; } char *type_text = cbm_node_text(a, type_node, ctx->source); if (!type_text || !type_text[0]) { continue; } if (ts_node_is_null(name_node)) { continue; } char *name = cbm_node_text(a, name_node, ctx->source); if (!name || !name[0]) { continue; } const char *field_qn = cbm_arena_sprintf(a, "%s.%s", class_qn, name); CBMDefinition def; memset(&def, 0, sizeof(def)); def.name = name; def.qualified_name = field_qn; def.label = "Field"; def.file_path = ctx->rel_path; def.parent_class = class_qn; def.return_type = type_text; def.start_line = ts_node_start_point(child).row + TS_LINE_OFFSET; def.end_line = ts_node_end_point(child).row + TS_LINE_OFFSET; def.is_exported = cbm_is_exported(name, ctx->language); cbm_defs_push(&ctx->result->defs, a, def); } } // Extract class-level variables (field declarations inside class bodies) static void extract_class_variables(CBMExtractCtx *ctx, TSNode class_node, const CBMLangSpec *spec) { if (!spec->variable_node_types || !spec->variable_node_types[0]) { return; } TSNode body = find_class_body(class_node, ctx->language); if (ts_node_is_null(body)) { return; } uint32_t count = ts_node_named_child_count(body); for (uint32_t i = 0; i < count; i++) { TSNode child = ts_node_named_child(body, i); if (cbm_kind_in_set(child, spec->variable_node_types)) { extract_var_names(ctx, child, spec); } } } // --- Module node + main walk --- // Iterative walk_defs — explicit stack with enclosing class context per frame. typedef struct { TSNode node; const char *enclosing_class_qn; // saved context for class nesting } walk_defs_frame_t; /* #668: walk_defs previously used a fixed `walk_defs_frame_t stack[4096]` — a * single ~160 KB C-stack frame. That overflowed small thread stacks (the * pre-2026-03 Windows 1 MB main thread) on the definitions pass, and its * `top < 4096` push guards SILENTLY DROPPED every top-level definition past * 4096. Use a growable heap stack instead: a tiny initial footprint that doubles * on demand, bounded by a generous, env-configurable ceiling that WARNs (once) * rather than dropping — so a file with thousands of top-level defs is fully * extracted, and a pathological one degrades to a warned, bounded skip instead * of an OOM or a stack overflow. */ typedef struct { walk_defs_frame_t *data; int top; int cap; const char *path; // for the WARN when the ceiling is hit (may be NULL) bool warned; } wd_stack_t; // Generous safety ceiling (frames), env-overridable via CBM_WALK_DEFS_MAX. // Realistic files never approach this; it only bounds a pathological/adversarial // file so extraction degrades to a warned skip rather than unbounded memory. static int wd_stack_max(void) { const char *e = getenv("CBM_WALK_DEFS_MAX"); if (e) { int v = atoi(e); if (v > 0) { return v; } } return 8 * 1024 * 1024; // 8M frames (~320 MB) default } static void wd_push(wd_stack_t *s, TSNode node, const char *enclosing_qn) { if (s->top >= s->cap) { int ncap = s->cap ? s->cap * 2 : 256; if (ncap > wd_stack_max()) { if (!s->warned) { char lim[24]; snprintf(lim, sizeof(lim), "%d", wd_stack_max()); cbm_log_warn("extract.walk_defs_capped", "limit", lim, "path", s->path ? s->path : ""); s->warned = true; } return; // bounded: stop growing (warned, not silent) } walk_defs_frame_t *nd = safe_realloc(s->data, (size_t)ncap * sizeof(walk_defs_frame_t)); if (!nd) { /* OOM — safe_realloc already freed the old buffer. Bail cleanly: drop * pending frames so the walk_defs loop drains and exits without a NULL * deref; extraction keeps whatever was already emitted. */ s->data = NULL; s->cap = 0; s->top = 0; return; } s->data = nd; s->cap = ncap; } s->data[s->top++] = (walk_defs_frame_t){node, enclosing_qn}; } /* Push all children of `node` in REVERSE order (so they pop in source order) * using a LINEAR traversal. Index-based ts_node_child(i) is O(i) per call in * tree-sitter, so a reverse index loop is O(n^2) per node — a file whose * root has ~580k flat siblings (ms-typescript's reallyLargeFile.ts fourslash * fixture) needed ~1.7e11 child-iterator steps and hung extraction for * hours; the supervisor then killed the silent worker as a hang and * quarantined innocent files off the stale marker. Collect the children * FORWARD with a TSTreeCursor (O(1) amortized per step) into a scratch * buffer, then push in reverse. Small nodes keep the direct index loop — * no cursor/buffer overhead on the overwhelmingly common case. */ enum { WD_CURSOR_MIN_CHILDREN = 64 }; /* Collect all `cc` children of `node` linearly via a TSTreeCursor into a * malloc'd array (caller frees). Returns NULL for small nodes and on OOM — * the caller then uses indexed ts_node_child access, which is fine (and * cheaper) at small child counts and merely quadratic-but-correct on OOM. */ static TSNode *wd_collect_children(TSNode node, uint32_t cc) { if (cc < WD_CURSOR_MIN_CHILDREN) { return NULL; } TSNode *buf = (TSNode *)malloc((size_t)cc * sizeof(TSNode)); if (!buf) { return NULL; } TSTreeCursor cur = ts_tree_cursor_new(node); uint32_t got = 0; if (ts_tree_cursor_goto_first_child(&cur)) { do { buf[got++] = ts_tree_cursor_current_node(&cur); } while (got < cc && ts_tree_cursor_goto_next_sibling(&cur)); } ts_tree_cursor_delete(&cur); if (got != cc) { /* Defensive: cursor and child_count disagree — fall back to indexed. */ free(buf); return NULL; } return buf; } static void wd_push_children_reverse(wd_stack_t *s, TSNode node, const char *enclosing_qn) { uint32_t cc = ts_node_child_count(node); if (cc == 0) { return; } TSNode *kids = wd_collect_children(node, cc); for (int i = (int)cc - SKIP_CHAR; i >= 0; i--) { wd_push(s, kids ? kids[i] : ts_node_child(node, (uint32_t)i), enclosing_qn); } free(kids); } // Push nested class nodes from a class body container onto the defs stack. // Iteratively walks into wrapper nodes (field_declaration, template_declaration). static void push_nested_class_nodes(TSNode body, const CBMLangSpec *spec, wd_stack_t *s, const char *enclosing_qn, CBMArena *arena) { TSNodeStack nc_stack; ts_nstack_init(&nc_stack, arena, NESTED_CLASS_STACK_CAP); ts_nstack_push(&nc_stack, arena, body); while (nc_stack.count > 0) { TSNode cur = ts_nstack_pop(&nc_stack); uint32_t nc = ts_node_child_count(cur); /* Linear child access for wide class bodies (see wd_collect_children). */ TSNode *kids = wd_collect_children(cur, nc); for (int i = (int)nc - SKIP_CHAR; i >= 0; i--) { TSNode child = kids ? kids[i] : ts_node_child(cur, (uint32_t)i); if (cbm_kind_in_set(child, spec->class_node_types)) { wd_push(s, child, enclosing_qn); } else { const char *ck = ts_node_type(child); if (strcmp(ck, "field_declaration") == 0 || strcmp(ck, "template_declaration") == 0 || strcmp(ck, "declaration") == 0) { ts_nstack_push(&nc_stack, arena, child); } } } free(kids); } } // 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). /* A namespace contributes a QN segment so a symbol declared in `namespace ns` * is `proj.file.ns.sym`, not a top-level `proj.file.sym`. Without the namespace * in the QN, namespace-aware resolution (C++ ADL) is starved: a bare call * collapses to the file scope and resolves directly instead. Unlike a class, a * namespace emits no def of its own — it only extends the enclosing scope for * its members. C#/PHP need the same treatment paired with their LSP resolvers * (a def-only change breaks their existing namespace handling), done separately. */ static bool is_namespace_scope_kind(CBMLanguage lang, const char *kind) { if (lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA) { return strcmp(kind, "namespace_definition") == 0; } return false; } static const char *compute_class_qn(CBMExtractCtx *ctx, TSNode node, const char *saved_enclosing) { TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name")); 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); } /* Top-level: language-aware module so Java/Go don't double the * filename stem (matches extract_class_def above). */ return cbm_fqn_compute_source_lang(ctx->arena, ctx->project, ctx->rel_path, cname, ctx->language); } } 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, wd_stack_t *s, const char *new_enclosing, CBMArena *arena) { 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 || // Groovy class bodies are a `closure` node; routing through the // nested-class path keeps methods from being re-walked (and thus // double-extracted) as top-level functions. Gated to Groovy so other // grammars that also name a node "closure" are unaffected. (strcmp(ck, "closure") == 0 && spec->language == CBM_LANG_GROOVY)) { push_nested_class_nodes(child, spec, s, new_enclosing, arena); return; } } // No body found — push all children directly for (int ci = (int)nc - SKIP_CHAR; ci >= 0; ci--) { wd_push(s, ts_node_child(node, (uint32_t)ci), new_enclosing); } } // ASCII case-insensitive equality vs a lowercase literal (CFML attribute names // are case-insensitive, e.g. NAME / Name / name). static bool ascii_ci_equals(const char *s, const char *lower_lit) { if (!s) { return false; } for (; *s && *lower_lit; s++, lower_lit++) { if (tolower((unsigned char)*s) != (unsigned char)*lower_lit) { return false; } } return *s == '\0' && *lower_lit == '\0'; } // CFML tag function: ... (#38). The // cf_function_tag node has no `name` field — the name lives in a cf_attribute // child (name="foo"). Emit a Function node named after that attribute value. static void extract_cfml_function_tag(CBMExtractCtx *ctx, TSNode node) { CBMArena *a = ctx->arena; char *name = NULL; uint32_t cc = ts_node_named_child_count(node); for (uint32_t i = 0; i < cc && !name; i++) { TSNode ch = ts_node_named_child(node, i); if (strcmp(ts_node_type(ch), "cf_attribute") != 0) { continue; } TSNode an = cbm_find_child_by_kind(ch, "cf_attribute_name"); if (ts_node_is_null(an)) { continue; } char *aname = cbm_node_text(a, an, ctx->source); if (!ascii_ci_equals(aname, "name")) { continue; } TSNode val = cbm_find_child_by_kind(ch, "quoted_cf_attribute_value"); if (ts_node_is_null(val)) { val = cbm_find_child_by_kind(ch, "cf_attribute_value"); } if (ts_node_is_null(val)) { continue; } TSNode inner = cbm_find_child_by_kind(val, "attribute_value"); name = cbm_node_text(a, ts_node_is_null(inner) ? val : inner, 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 + TS_LINE_OFFSET; def.end_line = ts_node_end_point(node).row + TS_LINE_OFFSET; def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET); def.is_exported = true; cbm_defs_push(&ctx->result->defs, a, def); } // Helm / Go template named-template definition: {{ define "chart.fullname" }} ... // {{ end }} (#338). The name is a string-literal child of define_action. Emit a // Function node so `include`/`template` references resolve to it via CALLS. static void extract_gotemplate_define(CBMExtractCtx *ctx, TSNode node) { CBMArena *a = ctx->arena; TSNode s = cbm_find_child_by_kind(node, "interpreted_string_literal"); if (ts_node_is_null(s)) { return; } char *raw = cbm_node_text(a, s, ctx->source); if (!raw) { return; } size_t len = strlen(raw); if (len >= 2 && (raw[0] == '"' || raw[0] == '`')) { raw = cbm_arena_strndup(a, raw + 1, len - 2); // strip surrounding quotes } if (!raw || !raw[0]) { return; } CBMDefinition def; memset(&def, 0, sizeof(def)); def.name = raw; def.qualified_name = cbm_fqn_compute(a, ctx->project, ctx->rel_path, raw); def.label = "Function"; def.file_path = ctx->rel_path; def.start_line = ts_node_start_point(node).row + TS_LINE_OFFSET; def.end_line = ts_node_end_point(node).row + TS_LINE_OFFSET; def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET); def.is_exported = true; cbm_defs_push(&ctx->result->defs, a, def); } // Janet (janet-simple S-expression grammar): definitions are generic `par_tup_lit` // forms whose head `sym_lit` is a def keyword — `(defn foo [] 1)` -> "foo". There // is no dedicated def node type, so gate on the head keyword and pull the name // from the second named child (also a `sym_lit`). static bool janet_is_def_head(const char *t) { if (!t) { return false; } static const char *heads[] = {"defn", "defn-", "defmacro", "defmacro-", "varfn", "fn", "def", "def-", "var", "var-", "defstruct", "deftype", "defprotocol", NULL}; for (int i = 0; heads[i]; i++) { if (strcmp(t, heads[i]) == 0) { return true; } } return false; } static void extract_janet_def(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), "sym_lit") != 0) { return; } char *head_text = cbm_node_text(a, head, ctx->source); if (!janet_is_def_head(head_text)) { return; } TSNode name_node = ts_node_named_child(node, 1); if (strcmp(ts_node_type(name_node), "sym_lit") != 0) { return; } char *name = cbm_node_text(a, name_node, ctx->source); if (!name || !name[0]) { return; } bool is_class = strcmp(head_text, "defstruct") == 0 || strcmp(head_text, "deftype") == 0 || strcmp(head_text, "defprotocol") == 0; 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 = is_class ? "Class" : "Function"; def.file_path = ctx->rel_path; def.start_line = ts_node_start_point(node).row + TS_LINE_OFFSET; def.end_line = ts_node_end_point(node).row + TS_LINE_OFFSET; def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET); def.is_exported = true; cbm_defs_push(&ctx->result->defs, a, def); } // Languages that use the C preprocessor and therefore have #define macros. static bool is_c_preprocessor_lang(CBMLanguage lang) { return lang == CBM_LANG_C || lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA || lang == CBM_LANG_GLSL || lang == CBM_LANG_OBJC || lang == CBM_LANG_ISPC; } // C/C++ preprocessor macros become Macro nodes (#375): // #define SIMPLE 1 -> preproc_def // #define FN(x) (2 * (x)) -> preproc_function_def // The name is the `name` field; a function-like macro's parameter list is kept // as the signature. The macro body (a preproc_arg) is not descended into. static void extract_c_macro_def(CBMExtractCtx *ctx, TSNode node) { CBMArena *a = ctx->arena; TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name")); 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 = "Macro"; def.file_path = ctx->rel_path; def.start_line = ts_node_start_point(node).row + TS_LINE_OFFSET; def.end_line = ts_node_end_point(node).row + TS_LINE_OFFSET; def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET); def.is_exported = true; // macros have no translation-unit scoping — globally visible TSNode params = ts_node_child_by_field_name(node, TS_FIELD("parameters")); if (!ts_node_is_null(params)) { def.signature = cbm_node_text(a, params, ctx->source); } cbm_defs_push(&ctx->result->defs, a, def); } // Clojure/Racket/Scheme: definitions are macro forms inside a generic `list` // node — these grammars have no dedicated def node. Detect a definition head // symbol and pull the name from the following form: // (defn foo [] ...) / (define (foo) ...) / (define foo ...) -> "foo". static bool lisp_is_def_head(const char *t) { if (!t) { return false; } static const char *heads[] = {"defn", "defn-", "def", "defmacro", "defmulti", "defmethod", "defprotocol", "defrecord", "deftype", "definterface", "defonce", // Clojure "define", "define-syntax", "define-values", "define-syntax-rule", "define-struct", "define-record-type", "define/contract", // Scheme/Racket "struct", // Racket struct NULL}; for (int i = 0; heads[i]; i++) { if (strcmp(t, heads[i]) == 0) { return true; } } return false; } static void extract_lisp_def(CBMExtractCtx *ctx, TSNode node) { CBMArena *a = ctx->arena; if (ts_node_named_child_count(node) < 2) { return; } char *head = cbm_node_text(a, ts_node_named_child(node, 0), ctx->source); if (!lisp_is_def_head(head)) { return; } TSNode target = ts_node_named_child(node, 1); const char *tk = ts_node_type(target); TSNode name_node = target; // (define (foo args) ...) — the name is the head symbol of the nested list. if ((strcmp(tk, "list") == 0 || strcmp(tk, "list_lit") == 0) && ts_node_named_child_count(target) > 0) { name_node = ts_node_named_child(target, 0); } if (ts_node_is_null(name_node)) { return; } char *name = cbm_node_text(a, name_node, ctx->source); if (!name || !name[0]) { return; } /* struct/record/type defining forms produce a type node, not a callable * (Racket `(struct point ...)`, Clojure `(defrecord ...)`, etc.). */ const char *lisp_label = "Function"; if (strcmp(head, "struct") == 0 || strcmp(head, "define-struct") == 0 || strcmp(head, "define-record-type") == 0 || strcmp(head, "defrecord") == 0 || strcmp(head, "deftype") == 0) { lisp_label = "Struct"; } else if (strcmp(head, "definterface") == 0 || strcmp(head, "defprotocol") == 0) { lisp_label = "Interface"; } 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 = lisp_label; def.file_path = ctx->rel_path; def.start_line = ts_node_start_point(node).row + TS_LINE_OFFSET; def.end_line = ts_node_end_point(node).row + TS_LINE_OFFSET; def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET); def.is_exported = true; cbm_defs_push(&ctx->result->defs, a, def); } /* Kotlin ERROR-node class recovery. * * The vendored fwcd tree-sitter-kotlin (commit 93bfeee) fails to parse two * member-modifier constructs and wraps the WHOLE enclosing class in a single * `ERROR` node, so the outer class is never recognized as a `class_declaration` * and disappears from the graph: * * class MyClass { companion object : Factory() { ... } } // anon companion w/ * delegation class Tree { inner class Node : BaseNode() { ... } } // inner + * delegation * * Inside the ERROR node the tokens are still present as a flat child list: * `class`/`object` keyword token → simple_identifier/type_identifier (name) * → optional `:` then one or more `delegation_specifier` siblings (bases). * * Recover each named class/object declaration from that flat sequence and emit a * Class definition (with bases) so it is discoverable. Strictly additive and * gated to Kotlin ERROR nodes: an ERROR region is already a broken parse, so * recovering names from it cannot regress a correct parse. Anonymous declarations * (e.g. a `companion object` with no name) are skipped — there is nothing to emit. */ static void recover_kotlin_error_classes(CBMExtractCtx *ctx, TSNode err_node) { CBMArena *a = ctx->arena; uint32_t cc = ts_node_child_count(err_node); for (uint32_t i = 0; i < cc; i++) { TSNode kw = ts_node_child(err_node, i); const char *kwt = ts_node_type(kw); /* Anonymous `class` / `object` keyword token starts a declaration. */ if (strcmp(kwt, "class") != 0 && strcmp(kwt, "object") != 0) { continue; } /* The name is the next child, when it is an identifier token. */ if (i + 1 >= cc) { continue; } TSNode name_node = ts_node_child(err_node, i + 1); const char *nt = ts_node_type(name_node); if (strcmp(nt, "simple_identifier") != 0 && strcmp(nt, "type_identifier") != 0) { /* Anonymous declaration (e.g. `companion object :`) — nothing to emit. */ continue; } char *name = cbm_node_text(a, name_node, ctx->source); if (!name || !name[0]) { continue; } const char *class_qn; if (ctx->enclosing_class_qn) { class_qn = cbm_arena_sprintf(a, "%s.%s", ctx->enclosing_class_qn, name); } else { class_qn = cbm_fqn_compute(a, ctx->project, ctx->rel_path, name); } /* Collect bases from any `delegation_specifier` siblings that follow the * name (until the class body `{` or the next class/object keyword). */ const char *bases[MAX_BASES]; int bcount = 0; for (uint32_t j = i + 2; j < cc && bcount < MAX_BASES_MINUS_1; j++) { TSNode sib = ts_node_child(err_node, j); const char *st = ts_node_type(sib); if (strcmp(st, "{") == 0 || strcmp(st, "class") == 0 || strcmp(st, "object") == 0) { break; } if (strcmp(st, "delegation_specifier") != 0) { continue; } /* delegation_specifier → user_type (directly or under * constructor_invocation) → type_identifier; strip generic args. */ TSNode ut = ts_node_named_child(sib, 0); if (!ts_node_is_null(ut) && strcmp(ts_node_type(ut), "constructor_invocation") == 0) { ut = ts_node_named_child(ut, 0); } if (ts_node_is_null(ut)) { continue; } TSNode ti = ut; if (strcmp(ts_node_type(ut), "user_type") == 0 && ts_node_named_child_count(ut) > 0) { ti = ts_node_named_child(ut, 0); } push_base_text(a, ti, ctx->source, bases, MAX_BASES_MINUS_1, &bcount); } CBMDefinition def; memset(&def, 0, sizeof(def)); def.name = name; def.qualified_name = class_qn; def.label = "Class"; def.file_path = ctx->rel_path; def.start_line = ts_node_start_point(name_node).row + TS_LINE_OFFSET; def.end_line = ts_node_end_point(err_node).row + TS_LINE_OFFSET; def.is_exported = cbm_is_exported(name, ctx->language); if (bcount > 0) { const char **result = (const char **)cbm_arena_alloc(a, (size_t)(bcount + NULL_TERM) * sizeof(const char *)); if (result) { for (int k = 0; k < bcount; k++) { result[k] = bases[k]; } result[bcount] = NULL; def.base_classes = result; } } cbm_defs_push(&ctx->result->defs, a, def); } } static void walk_defs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec, int depth_unused) { (void)depth_unused; wd_stack_t s = {0}; s.path = ctx->rel_path; wd_push(&s, root, ctx->enclosing_class_qn); while (s.top > 0) { walk_defs_frame_t frame = s.data[--s.top]; TSNode node = frame.node; ctx->enclosing_class_qn = frame.enclosing_class_qn; const char *kind = ts_node_type(node); /* Kotlin: recover class/object declarations the grammar lost inside an * ERROR node (companion-object-with-delegation, inner-class-with-base). * Additive — fall through so child descent still visits any well-formed * subtrees nested in the error region. */ if (ctx->language == CBM_LANG_KOTLIN && strcmp(kind, "ERROR") == 0) { recover_kotlin_error_classes(ctx, node); } if (ctx->language == CBM_LANG_ELIXIR && strcmp(kind, "call") == 0) { extract_elixir_call(ctx, node, spec); continue; } if (is_c_preprocessor_lang(ctx->language) && (strcmp(kind, "preproc_def") == 0 || strcmp(kind, "preproc_function_def") == 0)) { // Gated to full/advanced index modes — macros dominate extraction on // macro-dense codebases (e.g. the Linux kernel). See #375. if (cbm_macro_extraction_enabled()) { extract_c_macro_def(ctx, node); } continue; // the macro body is a preproc_arg — nothing more to extract } if (ctx->language == CBM_LANG_CFML && strcmp(kind, "cf_function_tag") == 0) { extract_cfml_function_tag(ctx, node); // cf_function_tag is in cfml_func_types (for call-scope attribution), // but its name lives in a cf_attribute, not a `name` field — so the // generic extract_func_def below must NOT also run on it (it would // resolve a null name and, for grammars where the kind has a `name` // field, double-mint). Push children so nested tags/defs are still // traversed, then skip the generic func path. wd_push_children_reverse(&s, node, frame.enclosing_class_qn); continue; } if (ctx->language == CBM_LANG_GOTEMPLATE && strcmp(kind, "define_action") == 0) { extract_gotemplate_define(ctx, node); // define_action is in gotemplate_func_types (for call-scope // attribution), but its `name` field is a quoted string literal — the // generic extract_func_def below would double-mint a def whose name // still carries the quotes. Push children so nested defines are still // traversed, then skip the generic func path. wd_push_children_reverse(&s, node, frame.enclosing_class_qn); continue; } if ((ctx->language == CBM_LANG_CLOJURE || ctx->language == CBM_LANG_RACKET || ctx->language == CBM_LANG_SCHEME) && (strcmp(kind, "list") == 0 || strcmp(kind, "list_lit") == 0)) { extract_lisp_def(ctx, node); // fall through: descend into children so nested defs are captured too } if (ctx->language == CBM_LANG_JANET && strcmp(kind, "par_tup_lit") == 0) { extract_janet_def(ctx, node); // fall through: descend so nested defs inside the form are captured too } /* WIT: world-scoped `export name: func(...)` / `import name: func(...)` * are export_item/import_item nodes (added to wit_func_types). But an * export/import can also reference a non-function type (e.g. an * interface) — only treat it as a Function when it actually contains a * func_type; otherwise descend so its inner body is still traversed. */ if (ctx->language == CBM_LANG_WIT && (strcmp(kind, "export_item") == 0 || strcmp(kind, "import_item") == 0) && ts_node_is_null( find_first_descendant_by_kind(node, "func_type", CBM_DESCENDANT_MAX_DEPTH))) { wd_push_children_reverse(&s, node, frame.enclosing_class_qn); continue; } if (cbm_kind_in_set(node, spec->function_node_types)) { if (!is_template_class_node(node, ctx->language)) { extract_func_def(ctx, node, spec); // Most languages stop here. JS/TS (and Wolfram) descend into the // function body so NESTED named definitions are also captured — // e.g. arrow methods of an object literal returned from a factory // (the Zustand actions-slice pattern, #341). Anonymous nested // arrows have no resolvable name and are skipped. // Ada subprograms nest (a procedure body's declarative part can // contain inner subprogram bodies); descend so the nested defs // are captured and same-file calls to them resolve to a CALLS edge. bool descend_into_func = (ctx->language == CBM_LANG_WOLFRAM || ctx->language == CBM_LANG_TYPESCRIPT || ctx->language == CBM_LANG_JAVASCRIPT || ctx->language == CBM_LANG_TSX || ctx->language == CBM_LANG_ADA); if (!descend_into_func) { continue; } } } if (ctx->language == CBM_LANG_RUST && strcmp(kind, "impl_item") == 0) { extract_rust_impl(ctx, node, spec); continue; } /* A namespace extends the enclosing scope (so members are QN-qualified by * it) without being a def itself. Push its children (its declaration_list * body and any nested namespaces) under the extended scope so each member * is walked normally — functions AND classes, unlike a class body which * routes methods through extract_class_methods. Do NOT emit a def or run * the class/func paths on the namespace node itself. */ if (is_namespace_scope_kind(ctx->language, kind)) { const char *new_enclosing = compute_class_qn(ctx, node, frame.enclosing_class_qn); wd_push_children_reverse(&s, node, new_enclosing); continue; } if (cbm_kind_in_set(node, spec->class_node_types)) { extract_class_def(ctx, node, spec); const char *new_enclosing = compute_class_qn(ctx, node, frame.enclosing_class_qn); push_class_body_children(node, spec, &s, new_enclosing, ctx->arena); continue; } /* Default descent — THE hot loop: a file whose root has hundreds of * thousands of flat siblings (580k comment lines in ms-typescript's * reallyLargeFile.ts) lands here every visit, so linear child * collection is mandatory (see wd_push_children_reverse). */ wd_push_children_reverse(&s, node, frame.enclosing_class_qn); } free(s.data); } void cbm_extract_definitions(CBMExtractCtx *ctx) { const CBMLangSpec *spec = cbm_lang_spec(ctx->language); if (!spec) { return; } CBMArena *a = ctx->arena; // Create module node (always first definition) CBMDefinition mod; memset(&mod, 0, sizeof(mod)); mod.name = ctx->rel_path; // will be refined by Go layer mod.qualified_name = ctx->module_qn; mod.label = "Module"; mod.file_path = ctx->rel_path; mod.start_line = FIRST_LINE; mod.end_line = ts_node_end_point(ctx->root).row + TS_LINE_OFFSET; mod.is_exported = true; mod.is_test = ctx->result->is_test_file; cbm_defs_push(&ctx->result->defs, a, mod); // Walk AST for function/class definitions walk_defs(ctx, ctx->root, spec, 0); // Extract module-level variables extract_variables(ctx, ctx->root, spec); }